dubbo 自定义异常处理方案

传统的MVC异常处理

在传统的 SpringMVC 开发的时候,层次结构经常是这样的。

View — Controller — Service

在这样的架构层次上做自定义业务异常处理,一般的方案是在用 Spring 的统一异常处理机制,即@ControllerAdvice加上@ExceptionHandler来捕获自己感兴趣的异常。如自定义异常如下:

public class BaseQingChaException extends RuntimeException {
    private String code;

    public BaseQingChaException(String code, String message) {
        super(message);
        this.code = code;
    }

    public BaseQingChaException(String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }

    public BaseQingChaException(String code, Throwable cause) {
        super(cause);
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

然后定一个一个全局处理异常的类:

@ControllerAdvice
@ResponseBody
@Slf4j
public class ExceptionHandleCenter {

    @ExceptionHandler(BaseQingChaException.class)
    public ResponseVO<Void> baseQingChaException(BaseQingChaException exception) {
        log.error(exception.getMessage(), exception);
        return ResponseUtils.error(exception.getCode(), exception.getMessage());
    }
}

这样在业务处理中自定义的异常会被捕获到,然后通过统一的展现形式给到前端。

dubbo 异常处理遇到的问题

dubbo 是一个分布式 rpc 调用框架,一般的架构是这样的:

View — Controller — rpc — Api — Service

在 Controller 中通过服务发现获取到服务地址,然后进行 rpc 调用。这时候的业务处理在远程,所以自定义异常也在远程,此时在调用方进行统一异常处理会出现无法捕获到异常的问题。原因是 dubbo 在处理异常的时候,考虑到序列化的问题,会将异常进行处理然后通过 rpc 传输到调用方。正是因为 dubbo 的处理,才导致在调用方的统一异常处理器中无法捕获该异常。

dubbo 的异常处理源码解析

dubbo 处理异常的类是org.apache.dubbo.rpc.filter.ExceptionFilter

@Activate(group = CommonConstants.PROVIDER)
public class ExceptionFilter implements Filter, Filter.Listener {
    private Logger logger = LoggerFactory.getLogger(ExceptionFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        return invoker.invoke(invocation);
    }

    @Override
    public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
        if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
            try {
                Throwable exception = appResponse.getException();

                // 如果不是RuntimeException并且是Exception,那么直接抛出
                if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
                    return;
                }
                // 如果在方法签名上声明了该异常,那么直接抛出
                try {
                    Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
                    Class<?>[] exceptionClassses = method.getExceptionTypes();
                    for (Class<?> exceptionClass : exceptionClassses) {
                        if (exception.getClass().equals(exceptionClass)) {
                            return;
                        }
                    }
                } catch (NoSuchMethodException e) {
                    return;
                }

                // 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;
                }
                // JDK 异常直接抛出
                String className = exception.getClass().getName();
                if (className.startsWith("java.") || className.startsWith("javax.")) {
                    return;
                }
                // dubbo 异常直接抛出
                if (exception instanceof RpcException) {
                    return;
                }

                // 其它的所有异常都用 RuntimeException 包装好返回给客户端
                appResponse.setException(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);
            }
        }
    }

    @Override
    public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
        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);
    }

    // For test purpose
    public void setLogger(Logger logger) {
        this.logger = logger;
    }
}

通过这个源码可以得出一下结论:

  1. 如果不是 RuntimeException 并且是 Exception,那么直接抛出。
  2. 如果在方法签名上声明了该异常,那么直接抛出。
  3. 如果异常类和接口类在同一个jar文件里,那么直接抛出。
  4. JDK 异常直接抛出。
  5. dubbo 异常直接抛出。
  6. 1-5点以外的所有异常都会被dubbo处理成 RuntimeException。


所以这就解释了为什么在统一异常处理器中无法捕获 BaseQingChaException 异常了,因为调用方收到的是一个 RuntimeException 异常。那么如果非要解决这个问题怎么办?

解决dubbo自定义异常处理的方案

还是通过上面的六点结论来进行分析。

  1. 可以将BaseQingChaException改为即成自 Exception。但是这样要在每一个方法上都要声明抛出该异常,很麻烦。
  2. 在方法签名上主动声明该异常,很麻烦。
  3. 将异常类和接口在一起打包。根本不可能,因为dubbo是一个分布式框架。
  4. 继承 RpcException 异常,成为 dubbo 的孙子异常。RpcException 类太复杂,不适合自定义。
  5. 修改源码,把自定义异常抛出。可行性是可以的,就是有点麻烦。
  6. 把 ExceptionFilter 从 dubbo 调用 filter 链中去掉。dubbo 需要这个类肯定有自己的作用,去掉这个之后在 consumer 中就无法追踪问题了,这个方案太幼稚。

以上6种方案都能解决问题,但都不是最好的解决方案。我这里推荐一种异常调包法。
**

异常调包法

新建一个自定义异常包装类

public class BaseQingChaWrapperException extends Exception {
    private BaseQingChaException cause;

    public BaseQingChaWrapperException(BaseQingChaException cause) {
        super(cause);
        this.cause = cause;
    }

    @Override
    public String getMessage() {
        return cause.getMessage();
    }

    public String getCode() {
        return cause.getCode();
    }
}

这个类继承自Exception,接收BaseQingChaException实例。


新建一个 dubbo filter 。

@Activate(group = CommonConstants.PROVIDER)
public class QingChaExceptionFilter implements Filter {
    private final Logger logger = LoggerFactory.getLogger(QingChaExceptionFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        Result appResponse = invoker.invoke(invocation);
        if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
            try {
                Throwable exception = appResponse.getException();
                if (exception instanceof BaseQingChaException) {
                    appResponse.setException(new BaseQingChaWrapperException((BaseQingChaException) exception));
                }
            } catch (Throwable e) {
                logger.warn("Fail to QingChaExceptionFilter when called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
            }
        }
        return appResponse;
    }
}

当 dubbo filter 调用链执行的时候,发现有自定义BaseQingChaException异常,那么将该异常包装成BaseQingChaWrapperException,这样 dubbo 原生的 ExceptionFilter 就会抛出该异常不做任何处理。
然后修改 consumer 中的统一异常处理器,将捕获BaseQingChaException改为捕获BaseQingChaWrapperException

@ControllerAdvice
@ResponseBody
@Slf4j
public class ExceptionHandleCenter {

    @ExceptionHandler(BaseQingChaWrapperException.class)
    public ResponseVO<Void> baseQingChaException(BaseQingChaWrapperException exception) {
        log.error(exception.getMessage(), exception);
        return ResponseUtils.error(exception.getCode(), exception.getMessage());
    }
}

通过调包异常,可以避免 dubbo 对自定义异常的封装,“完美解决”了这个问题。


该方法只是我想出来的一种方案,如果有问题或者有更好的方案,欢迎交流。

你可能感兴趣的:(分布式,Spring)