本文共 6741 字,大约阅读时间需要 22 分钟。
笔者所在的公司,项目正在重构,从一个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对抛出得异常用一个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@Componentpublic 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后端技术,敬请期待,加作者微信或关注作者的微信公众号,可以得到更多后端技术。