Dubbo如何正确捕获业务异常

    笔者所在的公司,项目正在重构,从一个SpringBoot项目往Dubbo上迁移,但在拆分后发现一个问题,服务消费者(后文用Consumer代替)无法正确捕获服务提供者(后文用provider代替)所抛出的非受检查异常。在未拆分之前,项目都是打成一个jar包运行,service层未处理的unchecked异常,在controller层捕获到后可以正常打印出异常的堆栈信息,方便开发人员快速定位哪行代码抛得异常,但是service和controller分离后,service层部署在provider端,controller部署在Consumer端,发现,如果provider抛了一个unchecked异常,例如NPE、 / by zero,发现不管在provider端还是在custom端都没有很清晰得打印出异常堆栈信息。比如:在provider端抛一个/ by zero异常端,在Consumer端只会打印一行异常信息

Dubbo如何正确捕获业务异常_第1张图片

对,只有简简单单一行,无论怎样也不会定位出异常发生得位置,经过一翻百度,才知道Dubbo对抛出得异常用一个ExceptionFilter的类进行拦截,下面一起来分析这段源代码:

@Activate(
    group = {"provider"}
)
public class ExceptionFilter implements Filter {
    private final Logger logger;

    public ExceptionFilter() {
        this(LoggerFactory.getLogger(ExceptionFilter.class));
    }

    public ExceptionFilter(Logger logger) {
        this.logger = logger;
    }

    public Result invoke(Invoker invoker, Invocation invocation) throws RpcException {
        try {
            //1
            Result result = invoker.invoke(invocation);
            //2
            if (result.hasException() && GenericService.class != invoker.getInterface()) {
                try {
                    //3
                    Throwable exception = result.getException();
                    //4
                    if (!(exception instanceof RuntimeException) && exception instanceof Exception) {
                        return result;
                    } else {
                        try {
                            //5
                            Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
                            Class[] exceptionClassses = method.getExceptionTypes();
                            Class[] arr$ = exceptionClassses;
                            int len$ = exceptionClassses.length;

                            for(int i$ = 0; i$ < len$; ++i$) {
                                Class exceptionClass = arr$[i$];
                                //6
                                if (exception.getClass().equals(exceptionClass)) {
                                    return result;
                                }
                            }
                        } catch (NoSuchMethodException var11) {
                            return result;
                        }

                        this.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);
                        //7
                        String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
                        String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
                        if (serviceFile != null && exceptionFile != null && !serviceFile.equals(exceptionFile)) {
                            String className = exception.getClass().getName();
                            if (!className.startsWith("java.") && !className.startsWith("javax.")) {
                                return (Result)(exception instanceof RpcException ? result : new RpcResult(new RuntimeException(StringUtils.toString(exception))));
                            } else {
                                return result;
                            }
                        } else {
                            return result;
                        }
                    }
                } catch (Throwable var12) {
                    this.logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + var12.getClass().getName() + ": " + var12.getMessage(), var12);
                    return result;
                }
            } else {
                return result;
            }
        } catch (RuntimeException var13) {
            this.logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + var13.getClass().getName() + ": " + var13.getMessage(), var13);
            throw var13;
        }
    }
}

如上代码所示,当产生异常时,会执行一个invoke方法对异常对于进行处理。

1、异常对象封装在Result中,invoker是service类中方法的一个代理(Dubbo中Consumer端调用provider端的方法时,其实调用的是一个代理方法,在此处不多介绍)

2、如果service接口没有实现GenericService接口,向下走

3、获取异常对象exception

4、如果异常是checked类型的异常,直接返回Result

5、如果在定义service的时候,throws了异常

6、如果service产生的的异常与service接口throws的异常一致时,返回Result

7、获取invoker接口所在的文件名serviceFile和产生的异常所在的文件名exceptionFile,如果异常包名不是以java.开头或javax.开头(不是JDK中的异常),将异常封装成一个RpcResult返回,否则直接返回。

 

所以,Dubbo项目中要正确捕获业务异常,而不是简简单单打印一行错误信息,要这样做:

方法1: 将该异常的包名以"java.或者"javax. " 开头

方法2: 使用受检异常(继承Exception)

方法3:不用异常,使用错误码

方法4:把异常放到api的jar包中,供provider和Consumer共享,也就是自定义一个Exception,继承RuntimeException并实现Serializable接口(因为将异常从provider端传递给consumer端是经过RPC传递的),目前,我是这么处理公司的业务异常的。

方法5:provider实现GenericService接口

方法6:service的接口throws 一个自定义异常,在service中throw一个该异常给provider,如下(RpcRuntime Exception是一个自定义异常)

@Override
    public String selectByDeleteFlag(String name) throws RpcRuntimeException{

        try{
            int i = 1 / 0;
        }catch (RpcRuntimeException e){
            throw  e;
        }
        return "aaa";
    }

如果满足上面方法中的一个,在custom端便可以打印出正常的异常堆栈信息,方便开发者快速定位异常,在公司中,我是通过上述第四种方式处理的,自定义异常,代码如下:

在api的jar包中自定义异常RpcRuntimeException并打jar包,继承RuntimeException实现Serializable接口

public class RpcRuntimeException extends RuntimeException implements Serializable {
    public RpcRuntimeException() {
    }

    public RpcRuntimeException(String msg) {
        super(msg);
    }

    public RpcRuntimeException(Throwable cause) {
        super(cause);
    }

    public RpcRuntimeException(String message, Throwable cause) {
        super(message, cause);
    }
}

在provider端用aop实现一个全局异常处理器,将service产生的RuntimeException包装成一个RpcRuntimeException并throws给调用端,也就是Consumer端。

@Aspect
@Component
public class RpcRuntimeExceptionHandler {

    private final Logger logger = LoggerFactory.getLogger(RpcRuntimeExceptionHandler.class);

    /**
     * service层的RuntimeException统一处理器
     * 可以将RuntimeException分装成RpcRuntimeException抛给调用端处理
     * 或自行处理
     * @param exception
     */
    @AfterThrowing(throwing="exception",pointcut="execution(* com.banma.tongye.service.*.*(..))")
    public void afterThrow(Throwable exception){
        if(exception instanceof  RuntimeException){
            logger.error(exception.getMessage());
            throw new RpcRuntimeException(exception);
        }
        exception.printStackTrace();
    }
}

这样在Consumer端就可以正常打印异常堆栈信息了。

 

好了,本文给大家分享的内容就是这些,在接下来的时间里,我会陆续为大家分享java后端技术,敬请期待,加作者微信或关注作者的微信公众号,可以得到更多后端技术。

你可能感兴趣的:(Java,架构,高并发)