本菇凉最近发现一个很奇特的问题,基于SpringCloud的微服务项目下与DDD领域设计模型的架构下,难免会存在服务间大量相互Feign调用的场景,但是当一些模块开发人员进行个体服务开发时,走进异常流程会直接抛出基于RuntimeException的封装好的异常信息给到前端,这原本是非常常见的做法,但是基于DDD领域模型的设计思想下,经常性的需要对后端各服务进行接口聚合封装,提供统一的API入口,分离领域与api,那么原先已开发好的子服务的流程Exception,经过这一层Feign的封装聚合调用,FeignClient无法感知到下层服务抛出的指定http状态码code和msg。遇到异常则直接响应如下
{
"timestamp": "2017-12-27 15:01:53",
"status": 500,
"error": "Internal Server Error",
"exception": xxxxxxxx,
"message": "Request processing failed; nested exception is {\"code\":1000, \"message\":\"test Exception\"}",
"path": xxxxx
}
异常链无法向上层逐一递交,除了微服务无状态的特性外,还有更重要的原因:Hystrix熔断器。
首先来看一下最最简单的Hystrix断路机制的傻瓜逻辑
当服务A发出Feign请求时,首先会通过gateway路由到目标调用服务B上,若B服务在调用接口时发生抛出异常,则httpstatus状态码会返回一个非200的值,Hystrix熔断器针对这样的Feign响应,会统一处理为500,INTER_ERROR,响应msg中也会提示由服务A调服务B时,发生了异常。
Hystrix提供了两种处理服务B发生异常时候的降级响应
- 服务A调用层Controller指定Feign异常处理方法
@Controller
public class HystrixDemo {
@HystrixCommand(fallbackMethod="getFallback")
@GetMapping("/testException")
protected String testHystrix() throws Exception {
return feignDemoClient.testException();
}
/**
* 对应异常处理方法
* @param e
* @return
*/
protected String getFallback(Throwable e) {
return "服务熔断响应";
}
}
但此种方法需要在api层里,没写一个接口,就要写上对应的异常处理降级方法,容易产生代码膨胀,非常不推荐,维护起来代码也很恶心。
2、FeignClient指定fallback方法
@FeignClient(value = "myClient",url="http://127.0.0.1:36664",
fallbackFactory = FeignDemoClientFallBack.class)
public interface FeignDemoClient {
@GetMapping("/xxx/feignDemo/testException")
ResponseEntity testException();
}
@Slf4j
@Component
public class FeignDemoClientFallBack implements FallbackFactory<FeignDemoClient> {
@Override
public FeignDemoClient create(Throwable throwable) {
return new FeignDemoClient() {
@Override
public ResponseEntity testException() {
return new ResponseEntity<>("服务降级响应", HttpStatus.OK);
}
};
}
}
此种办法在FeignClient客户端指定当调用该Feign接口时,出现异常的回调处理,推荐使用。
异常处理
Hystrix熔断机制,是为了确保Feign调用时发生异常而能够响应正常reponse的措施,所以传递下层服务exception异常,需要使用一定的转化措施,比如统一接口响应格式,各服务的交叉调用、聚合分组,都需要一定的规约形式。
一下是总结的几种处理异常的方式
- 不开启熔断的情况下,feign.hystrix.enable = false,可以捕获到FeignException异常进行解析,比如使用@RestControllerAdvice,用于定义ExceptionHandler,处理抛出的异常字符。
try {
if(FeignException.class.isAssignableFrom(ex.getClass()) && ex.getMessage().contains("code") && ex.getMessage().contains("status") && ex.getMessage().contains("message") ) {
return handleRemoteExceptions(fromFeignClient,ex);
}
} catch (Exception e) {
log.error("远程调用异常处理失败",e);
}
也可以使用Feign异常拦截器进行处理
/**
* feign异常拦截器,当从feign抛出异常时走这个对象.
*/
@Configuration
@Slf4j
public class FeignClientErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
logger.info("feign client response:", response);
String body = null;
try {
body = Util.toString(response.body().asReader());
} catch (IOException e) {
logger.error("feign.IOException", e);
}
if (response.status() >= 400 && response.status() <= 500) {
throw Exceptions.badRequestParams(body);
}
return errorStatus(methodKey, response);
}
}
虽然服务端方法返回为ResponseEntity,但出现4xx,5xx这些异常时,还是会抛出一个FeignException的异常,而这对于服务来说,向前端抛出的还是5xx的服务端异常,这不是我们希望看到的。
try {
ResponseEntity<?> body = productClient.add(name);
if (body.getStatusCode().equals(HttpStatus.OK)) {
return "test";
} else {
return body.getBody().toString();
}
} catch (FeignException ex) {
return ex.getMessage();
}
- 开启熔断机制的情况下,FeignException这个异常会被消化,取而代之的是HystrixRuntimeException异常抛出,如果再针对FeignException异常处理,是不能捕获到该类型异常的。
/**
* 处理服务间调用熔断异常解析
* @param fromFeignClient
* @param ex
* @return
*/
private ResponseEntity<?> handleHystrixRuntimeException(boolean fromFeignClient, HystrixRuntimeException ex) {
String orinalExceptionMessage = ex.getCause().getMessage();
if(orinalExceptionMessage.contains("你定义的异常基类")){
String temp = new StringBuilder(orinalExceptionMessage).reverse().toString();
int index = temp.indexOf("{[");
String rest = temp.substring(0,index+2);
String result = new StringBuilder(rest).reverse().toString();
log.info("异常结果:"+result);
//抛出异常码
JSONArray jsonArray = JSONArray.parseArray(result);
if(jsonArray!=null && jsonArray.size()>0){
JSONObject jsonObject = jsonArray.getJSONObject(0);
int httpStatus = Integer.valueOf(jsonObject.get("status").toString());
return ResponseEntity.status(extractHttpStatus(httpStatus)).body(jsonObject.toJSONString());
}
}
return null ;
}
总结
总之,Hystrix熔断器下的降级处理,就是需要当服务提供者无法正确响应时,给出一个可供调用的、能够返回httpStats.OK的处理,不然再前台调用端感知到的服务状态即为500,无论微服务中间层的交叉调用有多么微小。若不刻意对这样的熔断降级处理写fallback方法,尤其是当整个微服务体系下的服务数量较多时或者业务流程存在并发性处理或级联性处理时,很容易因为中间地段的exception难以走完流程、难以追踪与收集错误。
最后附上链接:
服务熔断与服务降级的区别
https://blog.csdn.net/weixin_46792649/article/details/106642958?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param