一:dubbo处理业务异常存在的问题
我们写一个简单的demo来说明问题。
1.首先我们定义一个自定义异常和一个api接口
package com.example.exception;
public class BaseException extends RuntimeException{
public BaseException(String message){
super(message);
}
}
api:
public interface HelloService {
String hello(String msg);
}
2.在发布的服务中抛出自定义的BaseException
@DubboService
public class HelloServiceImpl implements HelloService {
@Override
public String hello(String msg) {
throw new BaseException("业务异常");
}
}
3.消费者调用服务
@SpringBootTest
class DubboConsumerApplicationTests {
@DubboReference(check = false,timeout = 10000)
private HelloService helloService;
@Test
void test(){
System.out.println(helloService.hello("world"));
}
}
照理消费者调用的结果应该是抛出我们自定义的业务异常对吧?但是结果并不是,而是抛出了RuntimeException。
为什么会这样呢?首先我们需要了解dubbo是如何处理异常的
注:本文基于dubbo的版本是3.0.2.1
二:dubbo是如何处理异常的?
dubbo是通过ExceptionFilter
过滤器来对异常做处理的,处理结果的时候会执行ExceptionFilter中的onResponse()方法。
package org.apache.dubbo.rpc.filter;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.ReflectUtils;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.rpc.Filter;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.rpc.service.GenericService;
import java.lang.reflect.Method;
@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();
// directly throw if it's checked exception
// 1.如果是checked异常,直接抛出
if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
return;
}
// directly throw if the exception appears in the signature
try {
// 2.如果异常有在方法签名上声明,直接抛出
Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
Class<?>[] exceptionClasses = method.getExceptionTypes();
for (Class<?> exceptionClass : exceptionClasses) {
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.getServiceContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);
// directly throw if exception class and interface class are in the same jar file.
// 3.查看异常是否和接口是在同一个jar包文件下,如果是,直接抛出异常
String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
return;
}
// directly throw if it's JDK exception
//4.如果是在java包或者javax包下的异常也直接抛出
String className = exception.getClass().getName();
if (className.startsWith("java.") || className.startsWith("javax.")) {
return;
}
// directly throw if it's dubbo exception
//5.如果是RpcException 那么也直接抛出
if (exception instanceof RpcException) {
return;
}
// otherwise, wrap with RuntimeException and throw back to the client
//6.其他的情况都包装成RuntimeException抛出
appResponse.setException(new RuntimeException(StringUtils.toString(exception)));
} catch (Throwable e) {
logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getServiceContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
}
}
}
}
从源码中我们可以看到对异常的处理会分几种情况处理
1.如果是checked的异常,那么会直接抛出
2.如果异常有在接口方法上有显示的声明,那么直接抛出
3.如果异常类和接口是在同一个jar包文件下,直接抛出异常
4.如果是在java包或者javax包下的异常也直接抛出
5.如果是RpcException,那么直接抛出异常
6.其他情况下会把异常包装成RuntimeException,然后抛出
我们自己定义的BaseException只满足情况6,所以会包装成一个RunTimeException然后抛出。
三 如何解决这个问题
根据上文的源码分析,我们可以知道只要满足1-5条件中的一个即可,我们的异常就不会被dubbo包装成RuntimeException了。
条件1:如果是受检异常那么会直接抛出. 但是我们自定义的异常肯定是继承RuntimeException的,这个可以pass了。
条件2:如果异常有在接口方法上有声明,那么会直接将异常抛出. 这个应该是行的通的,我们可以尝试。
api:
public interface HelloService {
String hello(String msg) throws BaseException;
}
消费者消费结果:
可以看到结果是符合我们的预期的,抛出了我们的业务异常。
条件3:如果异常类和接口是在同一个jar包文件下,直接抛出异常. 如果把异常放在api中定义那么也是可以达到我们的预期效果的。
条件4:如果是java包或者javax包下的Exception,直接抛出。明显这个我们也可以pass了,我们自己定义的异常不会写在java包或者javax包下。
条件5:如果是RpcException,那么会直接抛出。我们自己自定义的Exception不会继承Dubbo的RpcException,所以这个也pass。
所以我们得出的解决方法有以下几个:
1.在方法签名上声明抛出的异常
2.将异常定义在api的jar包中。
3.因为dubbo处理异常是交给ExceptionFilter来处理的,我们只需要自定义一个Filter,将自定义的Filter加入到dubbo的Filter链中,并且移除原来的ExceptionFilter,这样就可以利用我们自定义的Filter来处理Exception了。
1.将ExceptionFilter中的代码复制到一个新的类中。
2.在新的类中加入对我们自定义异常的处理逻辑
3.在resource目录下新建一个META-INF/dubbo的文件夹,然后在dubbo目录下新建一个名为org.apache.dubbo.rpc.Filter
的文件,写入键值对,key是自定义的,value就是自己新定义的Filter的全类名。
dubboExceptionFilter=com.example.dubbo.provider.filter.DubboExceptionFilter
4.在application.properties文件中配置,加入我们自己的Filter并且移除原先的ExceptionFilter。
dubbo.provider.filter=dubboExceptionFilter,-exception
需要注意的是配置的filter的值必须和org.apache.dubbo.rpc.Filter文件中的key保持一致,而-exception代表的是去除原先的ExceptionFilter,这部分逻辑是在ConfigValidationUtils中的checkMultiExtension()方法中实现的,感兴趣的同学可以去看下逻辑,这里就不说了。
简单测试一下:
可以看到也是达到了我们预期的效果。
四:dubbo为什么要这么设计
我们可以想一下,如果我们自定义的异常只是在服务端存在,而消费端没有,那么消费端消费的时候自定义的异常是序列化不了的。但是我想说的是这种情况或许直接抛出ClassNotFoundException更能让人接受吧。
五:总结
本篇文章对dubbo如何处理业务异常存在的问题进行了分析,并且给出了几种解决方案,如果还有什么疑问,欢迎在下方留言,另外如果文章对你有所帮助,那么点个赞再走吧。