Dubbo业务异常要怎么处理

672 阅读5分钟

一: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。

1647786924(1).png

为什么会这样呢?首先我们需要了解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;
}    

消费者消费结果:

1647788304(1).png

可以看到结果是符合我们的预期的,抛出了我们的业务异常。

条件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.在新的类中加入对我们自定义异常的处理逻辑

1647789789(1).png

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()方法中实现的,感兴趣的同学可以去看下逻辑,这里就不说了。

简单测试一下:

1647788304(1).png

可以看到也是达到了我们预期的效果。

四:dubbo为什么要这么设计

我们可以想一下,如果我们自定义的异常只是在服务端存在,而消费端没有,那么消费端消费的时候自定义的异常是序列化不了的。但是我想说的是这种情况或许直接抛出ClassNotFoundException更能让人接受吧。

五:总结

本篇文章对dubbo如何处理业务异常存在的问题进行了分析,并且给出了几种解决方案,如果还有什么疑问,欢迎在下方留言,另外如果文章对你有所帮助,那么点个赞再走吧。