前言
现状分析
目前在使用dubbo框架进行微服务部署时,常常在provider服务端发生异常时,无法在consumer消费端查看到具体的异常信息,而在consumer端获取异常信息时,服务端返回的具体异常信息往往被包装成UndeclaredThrowableException和InvocationTargetException这两种异常信息返回。
以下说明分析中出现的dubbo源码版本均为2.6.5。
问题解决思路
- 首先先分析dubbo的provider端是如何处理在程序处理过程中发生的异常,通过集成工具debug跑项目找到dubbo具体处理异常的类为ExceptionFilter,这个类实现了Filter接口,此接口只有invoke方法,跟进源代码查看具体处理异常的逻辑:
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
Result result = invoker.invoke(invocation);
1.判断是否有异常并且没有实现GenericService接口
if (result.hasException() && GenericService.class != invoker.getInterface()) {
try {
Throwable exception = result.getException();
// 2.directly throw if it's checked exception
if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
return result;
}
// 3.directly throw if the exception appears in the signature
try {
Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
Class<?>[] exceptionClassses = method.getExceptionTypes();
for (Class<?> exceptionClass : exceptionClassses) {
if (exception.getClass().equals(exceptionClass)) {
return result;
}
}
} catch (NoSuchMethodException e) {
return result;
}
//4. for the exception not found in method's signature, print ERROR message in server's log.
logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
+ ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
+ ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);
//5. directly throw if exception class and interface class are in the same jar file.
String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
return result;
}
//6. directly throw if it's JDK exception
String className = exception.getClass().getName();
if (className.startsWith("java.") || className.startsWith("javax.")) {
return result;
}
//7. directly throw if it's dubbo exception
if (exception instanceof RpcException) {
return result;
}
//8. otherwise, wrap with RuntimeException and throw back to the client
return new RpcResult(new RuntimeException(StringUtils.toString(exception)));
} catch (Throwable e) {
logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost()
+ ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
+ ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
return result;
}
}
return result;
} catch (RuntimeException e) {
logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
+ ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
+ ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
throw e;
}
}
1). 如果service有实现GenericService,有异常直接抛出;
2). 如果是checked异常,直接抛出;
3). 如果在方法签名上有声明,直接抛出;
4). 未在方法签名上定义的异常,在服务器端打印error日志;
5). 如果异常类和接口类在同一jar包里,直接抛出;
6). 如果是JDK自带的异常,直接抛出;
7). 如果是Dubbo本身的异常,直接抛出;
8). 除了以上情况,会包装成RuntimeException抛给客户端;
所以,Dubbo项目中要正确捕获业务异常,而不是简简单单打印一行错误信息,可以通过如下任意一种方式实现:
1). Provider实现GenericService接口;
2). 使用受检异常(继承Exception);
3). 自定义异常需要在接口声明;
4). 把异常放到api的jar包中,供provider和Consumer共享,也就是自定义一个Exception,继承RuntimeException并实现Serializable接口(因为将异常从provider端传递给consumer端是经过RPC传递的);
5). 将异常类定义放在包名以java或者javax开头下;
本人在项目中的处理方式如下
1). 自定义一个异常;
public class DefineNoticeException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 1L;
public DefineNoticeException() {
}
public DefineNoticeException(String msg) {
super(msg);
}
public DefineNoticeException(Throwable cause) {
super(cause);
}
public DefineNoticeException(String message, Throwable cause) {
super(message, cause);
}
}
2). 定义一个异常捕获注解;
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ExceptionHandler {
}
3). 通过定义切面处理有加异常注解的方法捕获异常;
public class DefineExceptionHandler {
/**
* service层的RuntimeException统一处理器
* 可以将RuntimeException分装成DefineExceptionHandler抛给调用端处理
* 或自行处理
*
* @param exception
*/
@AfterThrowing(throwing = "exception", pointcut = "@annotation(com.test.common.facade.annotation.ExceptionHandler)")
public void afterThrow(Throwable exception) {
if (exception instanceof RuntimeException) {
log.error("异常信息[{}]", exception.getMessage());
throw new DefineNoticeException(exception);
}
exception.printStackTrace();
}
}
通过以上步骤,consumer端就可以打印provider端具体发生的异常信息了;
但是,如果我们还需要在consumer端进行全局的捕获异常时,还需要注意;
2.如果项目中使用的体系是spring,那么在Spring mvc框架中,我们就会使用@ControllerAdvice注解和@ExceptionHandler注解进行抛出异常的全局捕获,并统一格式返回给前端,具体实现源码如下:
@Slf4j
@Component
@ControllerAdvice
public class UnifiedExceptionHandler {
@ResponseBody
@ExceptionHandler(value =DefineNoticeException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> defineException(Exception e) {
log.error("test接口捕获处理,异常信息:[{}]", e.getMessage());
Map<String, Object> returnMap = new HashMap<>();
returnMap.put("isSuccess", "9999");
returnMap.put("errorMSG", "出现业务异常");
return returnMap;
}
}
但,这种实现情况下,客户端还是无法捕获之前在provider端自定义的异常!
3.通过debug跟踪consumer源码了解到:
1). 使用dubbo框架在consumer端调用provider端的服务,是使用动态代理来实现调用具体服务;
2). 通过查看dubbo实现动态代理的源码,可以看到具体的实现方法会把抛出的异常封装成UndeclaredThrowableException和InvocationTargetException两种类型抛出,所以无法捕获自定义的异常;
3). Dubbo实现动态代码的源码是这个类ReferenceAnnotationBeanPostProcessor;
4). 分析源码
我们需要修改以上截图红色mark的源码,修改直接捕获并抛出异常,具体实现如下:
通过以上修改就可以直接捕获provider端产生的自定义异常