NIUCLOUD是一款SaaS管理后台框架多应用插件+云编译。上千名开发者、服务商正在积极拥抱开发者生态。欢迎开发者们免费入驻。一起助力发展! 广告
## 一、远程服务之间异常该如何传递? * 如果是单体应用,控制层调用服务层代码,服务层调用持久层代码。异常在下层拦截转换成自定义异常向上层抛出,从而实现异常的传递。控制层或全局配置统一异常处理,将异常转换为前端用户可以理解的信息。 * 微服务应用之间的调用是使用HTTP协议来互相访问的,那么该如何有效的传递异常信息的渠道呢? * 渠道一:HTTP的状态码,如:200表示请求成功,400表示非系统异常(用户输入参数等),500表示系统内部异常等等。 * 渠道二:HTTP的Response Body,在Response Body中包含异常信息。 ## 二、远程服务异常传递的问题演示 为了让为大家更直观的感受,我们来做一个实验,感受一下远程服务异常传递问题。 ![](https://img.kancloud.cn/65/c3/65c3e838f7c3803aae4cee483051bb1a_1060x510.png) * 调整**服务提供者**aservice-sms的代码,加入服务降级的功能。我们人为制造一个一个程序异常:被除数为0。所以产生服务降级,会执行commonFallbackMethod方法。 ~~~ public AjaxResponse commonFallbackMethod() { return AjaxResponse.error(CustomExceptionType.SYSTEM_ERROR, "系统繁忙,请稍后再试!"); } ~~~ 代码中的error方法实际上做了几件事情: 1. 设置 AjaxResponse.isOk = false 2. 设置 AjaxResponse.code = 500 (基于CustomExceptionType.SYSTEM\_ERROR。我自定义的异常分类) 3. 设置 AjaxResponse.message = "系统繁忙,请稍后再试!" ![](https://img.kancloud.cn/95/aa/95aa17e7c9709e0e4d656d2031f7a20e_927x616.png) * 在**服务调用者**aservice-rbac我们把smsService(FeignClient)远程服务调用的结果AjaxResponse打印出来 ![](https://img.kancloud.cn/31/3f/313ff7c4a7c590ad58b7f7c4cd4ee3c0_1302x578.png) * 然后访问aservice-rbac的“/sysuser/pwd/reset”服务,结果如下: * 密码修改成功了,即:数据库操作成功了。即:`sysUserMapper.updateByPrimaryKeySelective`操作成功。 ![](https://img.kancloud.cn/b6/ed/b6edb76618edbf540ea9db9a6ceaa81a_242x132.png) * 但是远程服务短信发送失败了(因为我们在aservice-sms被调用接口中定义了程序异常:被除数为0,并且执行fallback函数)。服务提供者aservice-sms的fallback返回的数据如下: ![](https://img.kancloud.cn/2c/95/2c95c2ed844c73238bdb33853728a51c_1161x80.png) 这显然不是我们希望看到的结果。如果只是发短信失败还不是非常要紧,如果是购物网站,订单服务成功了,账务服务失败了,这个影响就大了!我们期望看到的结果是:**要么都成功,要么都失败**! ## 三、重点理解一下服务提供者的响应数据 问自己几个问题: ![](https://img.kancloud.cn/2c/95/2c95c2ed844c73238bdb33853728a51c_1161x80.png) * 这个数据是运行时异常么?不是,它只是数据,起不到数据库事务回滚回滚的作用。我们要非常明确的一点是:**只有运行时异常才会导致数据库事务回滚,业务异常数据是不会导致数据库事务回滚的。** * 这个数据中的code:500是Http状态码么?也不是,500代表的是远程系统服务运行出现异常,**是我自己定义的**。当然你可以认为2或者3表示远程服务运行异常,但这样不好,谁能记住呢?(我自定义的AjaxResponse的code字段的含义与HTTP状态码含义一致,好记!) * 这条数据响应的实际HTTP状态码是什么?是200-299其中的一个,因为我们成功的接收到了远程服务fallback函数响应的数据。成功的HTTP请求状态码都是200-299。 所以远程服务降级之后返回的结果是: * HTTP协议的正常响应结果(200-299,HTTP协议规范) * 业务上的异常数据(AjaxResponse.code = 500,我自己根据HTTP协议规范定义的业务结果状态码) 也就是说,我们介绍了服务异常传递的两个渠道:一是HTTP状态码,二是HTTP的Response Body。**目前我们只能使用第二种渠道传递异常!** ## 四、使用HTTP的Response Body传递异常(最简单的方式) 所以针对以上的异常传递不到位导致的问题,最简单的处理方式就是:我们在接收到远程服务的响应结果Response Body(对于我们的项目是AjaxResponse)后,判断其内部的状态信息。如果状态信息是业务失败,throw new 自定义异常抛出,触发数据库回滚! 1. AjaxResponse.isOk = false 2. AjaxResponse.code = 500 3. AjaxResponse.message = "系统繁忙,请稍后再试!" ![](https://img.kancloud.cn/e0/15/e015dd249bf4257f4f9edeb075e4d091_1307x245.png) ## 五、使用HTTP状态码传递异常(优化方式,符合RESTful风格) 目前很多应用都采用RESTful风格的接口,特点就是 * 看HTTP方法就知道动作,如:GET表示查询、POST表示修改、DELETE表示删除 * 看URL就知道操作的资源。比如:用GET请求/dogs资源,是查询所有的狗狗数据。 * 看HTTP返回的状态码,就知道动作的结果。如:200表示成功、400表示一些输入参数错误等、500表示系统内部错误。如果严格遵照这一项,**我们有必要让HTTP响应结果的状态码与业务的运行结果AjaxResponse的code统一!** 方法就是实现ResponseBodyAdvice接口:对项目的所有的Controller的JSON类型数据响应结果进行二次封装,然后再返回给服务调用端端。 ![](https://img.kancloud.cn/2c/95/2c95c2ed844c73238bdb33853728a51c_1161x80.png) * 统一数据响应格式为AjaxResponse(可以自定义)。 * 指定HTTP协议状态码status code = 业务运行结果AjaxResponse.code,前提是自定义的AjaxResponse的code字段的含义与HTTP状态码含义一致。 ~~~ response.setStatusCode(HttpStatus.valueOf( ((AjaxResponse) body).getCode()) ); ~~~ 有了这样一层封装,**服务调用端就能根据HTTP状态码判断服务提供者的响应数据是否异常**。完整实现如下: ~~~ @Component @ControllerAdvice public class GlobalResponseAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class converterType) { //return returnType.hasMethodAnnotation(ResponseBody.class); return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { //对于JSON类型的响应数据 if(selectedContentType.equalsTypeAndSubtype( MediaType.APPLICATION_JSON)){ if(body instanceof AjaxResponse){ //如果Controller返回值body的数据类型是AjaxResponse(body instanceof AjaxResponse) //就将body直接返回 response.setStatusCode(HttpStatus.valueOf( ((AjaxResponse) body).getCode()) //将业务异常状态码赋值给HTTP状态码 ); return body; }else{ //如果Controller返回值body的数据类型不是AjaxResponse, //就将body封装为AjaxResponse类型返回,总之要统一数据响应的类型 AjaxResponse ajaxResponse = AjaxResponse.success(body); response.setStatusCode(HttpStatus.valueOf( ajaxResponse.getCode()) //将业务异常状态码赋值给HTTP状态码 ); return AjaxResponse.success(body); } } return body; } } ~~~