Dubbo用了这么久,异常处理的最佳实践是什么?
Editor's Note
通过问题背景,结合源码分析,得出最佳实践
The following article is from Java课代表 Author Java课代表
1 背景
在日常业务开发过程中,我们为了让业务代码更健壮,遇到错误时返回的提示更友好,一般会自定义一些业务异常。根据业务需要,分为自定义受检异常和非受检异常
知识点回顾
Exception
类及其子类,但不包括RuntimeException
的子类,统称为受检异常。如果方法执行过程中有可能抛出此类异常,必须在方法签名上声明
RuntimeException
类及其子类,统称为非受检异常。如果方法执行过程中有可能抛出此类异常,可以不必在方法签名上声明
课代表所负责的项目使用SpringCloudAlibaba
落地了微服务,开发中组内兄弟遇到一个问题:Dubbo RPC
调用时,provider
抛出的一个业务类非受检异常,consumer
接到时却是RuntimeException
并且message
被和堆栈信息拼接到了一起。
2 问题复现
Dubbo
微服务中,provider
分为api
和service
,consumer
只需要引入 api
从注册中心调用service
实例即可。
当service
中抛出一个自定义的非受检异常,且其相应api
包中没有这个异常类时,就会出现异常被包装为RuntimeException
的情况。
其实问题分析到这里,基本就有眉目了:Dubbo
是一个RPC
框架,客户端调用的都是远程方法,参数和返回值都是经过序列化和反序列化为字节数组传输的。consumer
必须认识这个异常才能反序列化成功。
很明显,我们抛的这个异常 Dubbo
认为consumer
不认识,为了避免反序列化失败,从而对异常进行了包装。
下面结合源码阐述Dubbo
的异常处理机制。
3 源码分析
Dubbo
远程调用的异常由ExceptionFilter
类处理
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
Result result = invoker.invoke(invocation);
if (result.hasException() && GenericService.class != invoker.getInterface()) {
try {
Throwable exception = result.getException();
// 如果是checked异常,直接抛出
if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {
return result;
}
// 在方法签名上有声明,直接抛出
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;
}
// 未在方法签名上定义的异常,在服务器端打印ERROR日志
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);
// 异常类和接口类在同一jar包里,直接抛出
String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){
return result;
}
// 是JDK自带的异常,直接抛出
String className = exception.getClass().getName();
if (className.startsWith("java.") || className.startsWith("javax.")) {
return result;
}
// 是Dubbo本身的异常,直接抛出
if (exception instanceof RpcException) {
return result;
}
// 否则,包装成RuntimeException抛给客户端
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;
}
通过源码可以看到,该类的主要功能是返回接口抛出的异常,Dubbo
将其定义为如下几种情况:
如果是 checked
异常,直接抛出在方法签名上有声明,直接抛出 不符合1,2 的被认为是错误,会打印error日志,并尝试如下处理:
异常类和接口类在同一个 jar
包里,直接抛出是 JDK
自带的异常,直接抛出是 Dubbo
本身的异常,直接抛出否则,包装成 RuntimeException
抛给客户端
事实上Dubbo
作为RPC
框架已经把各种抛异常的情况都考虑全了,最后如果Dubbo
认为consumer
不认识这个异常还会包装成RuntimeException
兜底,防止反序列化失败。
如果发生了consumer
找不到provider
所抛异常的这种情况,不客气地讲,一定是开发者的问题,把这个归罪于Dubbo 那可就太冤枉它了!
4 最佳实践
Dubbo
官网->Dubbo 2.7->用户文档->服务化最佳实践 中有如下描述:
分包
建议将服务接口、服务模型、服务异常等均放在 API 包中,因为服务模型和异常也是 API 的一部分,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP)。
所以,符合Dubbo
最佳实践的provider-api
中应该包含服务接口包,服务模型包,服务异常包。所有service
中用到的异常,都应该在api
包中声明,这样consumer
调用时才会符合Dubbo 要求的:
异常类和接口类在同一个
jar
包里,直接抛出
从而避免被Dubbo
包装成RuntimeException
抛给客户端。
所以,针对文章开头遇到的问题,我们只需要把provider-service
中抛出自定义的非受检异常 在provider-api
中定义,同时在相应的方法上throw
出来就可以了,这样既可以防止被Dubbo
包装,也不会因为方法签名中没声明异常而导致Dubbo
报error
错误。而且,因为是非受检异常,所以也不强制客户端对方法进行try catch
。
一个可参考的分包实践:
+- scr
|
+- demo
|
+- domain (业务域内传输数据用的 DTO)
|
+- service (API 中 service 接口的实现类)
|
+- exception (业务域中的自定义异常)
5 弯路
如果 Google 关键字 [Dubbo 异常处理],你会发现几乎所有文章都是下面这几个思路:
自定义一个 ExceptionFilter
让Dubbo
使用,兼容自己的业务异常类在provider 端写个AOP拦截所有异常自己处理 把 unchecked
异常改为checked
异常
当然,上面这些方法完全可以解决问题,但这是不是有杀鸡用牛刀的意思?
明明是代码开发不规范,没有遵循最佳实践,却要强行归罪于底层框架。Dubbo
在努力做得通用,而上面的处理方式却在让代码变得紧耦合。
总结问题本质:Dubbo
在认为consumer
找不到异常类时,为了防止发生反序列化失败,对异常进行了一层包装。针对这一实质,我们用最简单、高效,影响最小的办法解决就可以了。
课代表相信读者结合Dubbo
异常处理的源码,应该会有自己的判断。
6 反思
遇事不决问Google,多数情况下我们遇到的问题都会搜到答案,对于同样一个问题,解决的方法可能多种多样,我们需要做的是找到问题的本质,举一反三,根据自己业务的实际情况选择最合适的解决方案。
切勿盲从,须知:尽信书不如无书。
2021-07-12
2021-07-07
2021-07-05
2021-07-05
2021-06-28