在传统的 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 是一个分布式 rpc 调用框架,一般的架构是这样的:
View — Controller — rpc — Api — Service
在 Controller 中通过服务发现获取到服务地址,然后进行 rpc 调用。这时候的业务处理在远程,所以自定义异常也在远程,此时在调用方进行统一异常处理会出现无法捕获到异常的问题。原因是 dubbo 在处理异常的时候,考虑到序列化的问题,会将异常进行处理然后通过 rpc 传输到调用方。正是因为 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;
}
}
通过这个源码可以得出一下结论:
所以这就解释了为什么在统一异常处理器中无法捕获 BaseQingChaException 异常了,因为调用方收到的是一个 RuntimeException 异常。那么如果非要解决这个问题怎么办?
还是通过上面的六点结论来进行分析。
BaseQingChaException
改为即成自 Exception。但是这样要在每一个方法上都要声明抛出该异常,很麻烦。以上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 对自定义异常的封装,“完美解决”了这个问题。
该方法只是我想出来的一种方案,如果有问题或者有更好的方案,欢迎交流。