本文已参与「新人创作礼」活动.一起开启掘金创作之路。
Feign调用之RuntimeException子异常透传实战
一、背景
- 在单体应用架构中,有时为了方便,在调用函数的深处出现了错误,希望不要沿着调用路径原路返回,而是希望直接通过抛出RuntimeException子异常的方式,直接统一处理异常并返回给客户端。
- 在Dubbo调用中也存在上述情况。
- 本文介绍了在微服务Feign调用链中从底层微服务抛出一个特定RuntimeException,并沿微服务调用链往外传递该异常的方法。
二、原理
调用过程解析:
(1) Consumer通过feign调用provider
(2) Provider执行过程中抛出BusinessException异常
(3) Provider的ExceptionHandler拦截到该异常,并返回HTTP状态码为510的响应,并序列化异常信息
(4) Consumer的Feign调用ErrorDecoder,解析后抛出HystrixBadRequestException
(5) Consumer的ExceptionHandler拦截到该异常,并返回HTTP状态码为510的响应,并序列化异常信息
注:在Consumer的FeignErrorDecoder中只有抛出HystrixBadRequestException,Hystrix才不会走fallback,其它异常都会走fallback流程,并计算调用错误一次(失败率累计到阈值会导致熔断)。hystrix的熔断和其它相关概念原理见本微服务吧Hystrix相关文档。
三、准备工作
Provider微服务
Consumer微服务
Consul注册发现服务中心
四、实战
- 实现Provider和Consumer都用到的ExceptionHandler
package cn.ucmed.otaku.healthcare.exception.handler;
import cn.ucmed.common.rubikexception.BusinessException;
import com.alibaba.fastjson.JSONObject;
import com.netflix.hystrix.exception.HystrixBadRequestException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import cn.ucmed.rubik.api.Result;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
@Slf4j
public class MyExceptionHandler {
// @ExceptionHandler(Throwable.class)
// @ResponseStatus(value = HttpStatus.OK)
// @ResponseBody
// public ResponseEntity handleUnexpectedServerError(Throwable ex) {
//
// log.error("ApiExceptionHandler.handleUnexpectedServerError", ex);
// return getErrorResponse("目前业务繁忙,请您稍后!");
// }
@ExceptionHandler(BusinessException.class)
public ResponseEntity handleBusinessException(BusinessException be) {
log.error("MyExceptionHandler.handleBusinessException", be);
return getErrorResponse(be.getMessage(), be.getCode());
}
@ExceptionHandler(HystrixBadRequestException.class)
public ResponseEntity HystrixBadRequestException(HystrixBadRequestException hbre) {
log.error("MyExceptionHandler.HystrixBadRequestException", hbre);
Result res = toResult(hbre);
if (res == null || res.getMsg() == null) {
throw hbre;
}
return getErrorResponse((String)res.getMsg(), res.getCode());
}
private ResponseEntity<Result> getErrorResponse(String message) {
Result result = new Result();
result.setMsg(message);
result.setCode(500);
return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
}
private ResponseEntity<Result> getErrorResponse(String message, Integer code) {
Result result = new Result();
result.setMsg(message);
result.setCode(code);
return new ResponseEntity<>(result, HttpStatus.NOT_EXTENDED);
}
private Result toResult(HystrixBadRequestException hbre) {
if (hbre == null) {
return null;
}
JSONObject obj = null;
obj = JSONObject.parseObject(hbre.getMessage());
Integer code = obj.getInteger("code");
if (code == null){
return null;
}
String message = obj.getString("msg");
if(message == null){
return null;
}
return new Result(code.intValue(), message);
}
}
2. 实现Consumer用到的FeignErrorDecoder
package cn.ucmed.otaku.healthcare.exception.handler;
import com.alibaba.fastjson.JSONObject;
import com.netflix.hystrix.exception.HystrixBadRequestException;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Slf4j
public class FeignErrorDecoder extends ErrorDecoder.Default {
@Override
public Exception decode(String s, Response response) {
log.info("FeignErrorDecoder.decode(): method key: " + s);
HystrixBadRequestException hbre = decodeBusinessException(response);
if (hbre != null) {
log.info("FeignErrorDecoder.decode(): receive bussiness exception! return HystrixBadRequestException");
return hbre;
}
log.info("FeignErrorDecoder.decode(): return super.decode()");
return super.decode(s, response);
}
private HystrixBadRequestException decodeBusinessException(Response response){
String body = getBody(response);
if ( body == null ){
return null;
}
JSONObject obj = null;
obj = JSONObject.parseObject(body);
Integer code = obj.getInteger("code");
if (code == null){
return null;
}
String message = obj.getString("msg");
if(message == null){
return null;
}
return new HystrixBadRequestException(body);
}
private String getBody(Response response){
if( response == null || response.body() == null ) {
return null;
}
try {
return Util.toString(response.body().asReader());
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
3. 测试
(1)在Provider中模拟exception
package com.example.exceptionprovider;
import cn.ucmed.common.rubikexception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
@RestController
@Slf4j
public class ProviderController {
private final AtomicLong counter = new AtomicLong(new Random().nextInt(1000));
@Value("${server.port}")
private int port;
@RequestMapping("/test")
public ProviderPOJO test(@RequestParam(value = "name", defaultValue = "provider") String name) throws IOException {
if ("be".equalsIgnoreCase(name)) {
throw new BusinessException(1,"simulation business exception");
} else if ("re".equalsIgnoreCase(name)) {
throw new RuntimeException("runtime exception");
} else if ("io".equalsIgnoreCase(name)) {
throw new IOException("io exception");
}
ProviderPOJO providerPOJO = new ProviderPOJO(counter.incrementAndGet(), name, port);
log.info("ProviderController:test(): " + providerPOJO);
return providerPOJO;
}
}
(2)运行Consul,Provider,Consumer
(3)正常请求,浏览器访问: http://localhost:8090/test?name=normal
{"id":367,"name":"normal","port":8086,"providerPOJO":{"id":787,"name":"normal","port":8085}}
(4)模拟BussinessException请求,浏览器访问:http://localhost:8090/test?name=be
{"code":1,"data":null,"msg":"test BusinessException"}
(5)模拟其它Exception请求,浏览器访问:http://localhost:8090/test?name=re, 或http://localhost:8090/test?name=io
{"id":372,"name":"re","port":8086,"providerPOJO":null}
注:这一步模拟非BusinessException,feign的hystrix最终将按fallback流程处理
- 实际项目建议
(1)FeignErrorDecoder和MyExceptionHandler实现可以放到公共jar中,被所有微服务依赖
(2)在具体微服务中启用上述实现,如果微服务包名和上述实现类的包名不同,需要自定义扫描包名:
@SpringBootApplication(scanBasePackages={"com.example.*", "cn.ucmed.otaku.healthcare.exception.handler"})
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ExceptionProviderApplication.class, args);
}
}
其中com.example.*表示provider包名,cn.ucmed.otaku.healthcare.exception.handler表示FeignErrorDecoder和MyExceptionHandler类所在包名
(3)在微服务实现中,抛出BusinessException要谨慎,毕竟java对exception的处理效率比正常处理要低,一般应避免滥用。
(4)一般而言要禁止随意在微服务层抛出除BusinessException外其它exception,也就是说微服务中抛出的非BusinessException需要在本微服务中Catch并处理,因为在微服务层抛出的非BusinessException低效,如果不处理最终将走到Consumer的feign的fallback流程,并计算一次错误,累计错误可能会导致熔断效果。