基于dubbo的分布式应用中的统一异常处理
1. 背景
目前在做的项目使用
dubbo作为分布式服务框架,新项目开发过程中遇到一个问题:provider端抛出了自定义的业务异常,而consumer接收到的却是RpcException,原来的业务异常(包括异常栈)被包装到了message中,导致consumer不能正确获取provider想要提供的message。
出现这个问题,有几个前提:
自定义的异常。
自定义的业务异常继承
RuntimeException,属于运行期/非检查异常。自定义的业务异常在二方包/三方包中,不在
provider提供的API包中。
2. 先说结论
基于上面的几个前提,可以发现自定义的异常可能会在 consumer 接收时反序列化失败,因为这个异常不一定在 consumer中存在。而dubbo为了避免这种情况出现,使用 ExceptionFilter 过滤器对异常进行了包装,转换为 RuntimeException 提供给 consumer 。
最后我利用 Spring AOP 拦截掉provider所有异常,将异常包装成Response(一个自定义的返回值对象POJO)返回给consumer,规避掉了 dubbo 的处理。
3. ExceptionFilter - dubbo 的异常处理源码
@Activate(group = Constants.PROVIDER)
public class ExceptionFilter implements Filter {
private final Logger logger;
public ExceptionFilter() {
this(LoggerFactory.getLogger(ExceptionFilter.class));
}
public ExceptionFilter(Logger logger) {
this.logger = logger;
}
@Override
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();
// 检查异常,直接抛出
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;
}
// 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);
// 异常类和接口类在同一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这样处理也是合理的,但确实和目前项目的应用情况有冲突,要如何解决这个问题?
3.1 常规来说有如下方法:
将该异常的包名以
java.或者javax.开头 不符合规范,排除异常继承Exception 自定义的业务异常本身属于
RuntimeException, 排除异常类和接口类在同一jar包里 较大的项目一般都会有一些common包,定义好异常类型,使用二方包的方式引用,所以也不适用,排除
provider的api明确写明throws XxxException作为服务端,不应显式抛出异常给客户的进行处理,排除实现
dubbo的filter,自定provider的异常处理逻辑 可以在原有逻辑基础上增加:可行,但是逻辑中写死了异常的路径,而且也不能保证后期其他
consumer,会引用这个二方包。
4. 最终方案
结合项目的实际情况,我们项目中 provider 提供的接口返回值全部包装一层再提供出去,这样的好处是服务和服务之间的交互使用的数据对象都是一样的,便于使用,这个对象比较常规:
我觉得Response中增加 private String code; 用于标记错误类型,会更好一些 基于这个前提,我想利用 Spring AOP 拦截掉provider所有异常,将异常包装成Response对外提供,规避掉 dubbo 的处理。如果是业务异常利用 error 提供简单异常信息给外部,如果非业务异常只提示服务调用失败,具体错误输出到日志,再通过kibana等日志平台查看。
代码:
但是这个方式因为拦截掉了service的异常,所以如果多个service存在事务传递的情况,会导致事务失效,因为我们这个项目要求事务单独抽出一层manage来做,所以没有问题。
但是也可以优化:新定义一个切点,声明任何持有@Transactional注解的方法,将这部分需要事务的方法单独处理,最终代码如下:
最后更新于
这有帮助吗?