基于dubbo的分布式应用中的统一异常处理

转载:基于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 常规来说有如下方法:

  1. 将该异常的包名以java.或者javax. 开头 不符合规范,排除

  2. 异常继承Exception 自定义的业务异常本身属于 RuntimeException, 排除

  3. 异常类和接口类在同一jar包里 较大的项目一般都会有一些common包,定义好异常类型,使用二方包的方式引用,所以也不适用,排除

  4. providerapi明确写明throws XxxException 作为服务端,不应显式抛出异常给客户的进行处理,排除

  5. 实现 dubbofilter,自定 provider 的异常处理逻辑 可以在原有逻辑基础上增加:

    可行,但是逻辑中写死了异常的路径,而且也不能保证后期其他 consumer,会引用这个二方包。

4. 最终方案

结合项目的实际情况,我们项目中 provider 提供的接口返回值全部包装一层再提供出去,这样的好处是服务和服务之间的交互使用的数据对象都是一样的,便于使用,这个对象比较常规:

我觉得Response中增加 private String code; 用于标记错误类型,会更好一些 基于这个前提,我想利用 Spring AOP 拦截掉provider所有异常,将异常包装成Response对外提供,规避掉 dubbo 的处理。如果是业务异常利用 error 提供简单异常信息给外部,如果非业务异常只提示服务调用失败,具体错误输出到日志,再通过kibana等日志平台查看。

代码:

但是这个方式因为拦截掉了service的异常,所以如果多个service存在事务传递的情况,会导致事务失效,因为我们这个项目要求事务单独抽出一层manage来做,所以没有问题。

但是也可以优化:新定义一个切点,声明任何持有@Transactional注解的方法,将这部分需要事务的方法单独处理,最终代码如下:

最后更新于

这有帮助吗?