记录一次dubbo不能正常抛出特定异常
BUG场景
今天同事的代码中出现一个问题,让我帮忙排查一下。原代码大致如下
dubbo服务消费者:
1 @Resource 2 private IPayWayService payWayService; 3 4 @RequestMapping(value = "/add", method = RequestMethod.POST) 5 @ApiResponses(value = {@ApiResponse(code = 200, message = "请求成功")}) 6 @ApiOperation(value = "/add", notes = "新增通道") 7 public Result<Boolean> addPayWay( @RequestBody PayWayDto payWayDto) { 8 logger.info("请求新增通道接口 payWayDto:{}",payWayDto); 9 try{ 10 TransactionResult<Boolean> result =payWayService.addPayWay(payWayDto); 11 return new Result<>(result.getCode(),result.getMessage()); 12 }catch (PaymentException pe){ 13 logger.info("请求新增通道接口异常:error:{}",pe); 14 return new Result<>(ResultCode.C500.code,pe.getMessage()); 15 }catch (RuntimeException re){ 16 logger.info("error:",re.getMessage()); 17 return new Result<>(ResultCode.C500.code,re.getMessage()); 18 }catch (Exception e){ 19 logger.info("error:",e.getMessage()); 20 return new Result<>(ResultCode.C500.code,e.getMessage()); 21 } 22 }
dubbo服务提供者:
1 @Override 2 @Transactional(rollbackFor = Exception.class) 3 public TransactionResult<Boolean> addPayWay(PayWayDto payWayDto){ 4 logger.info("新增通道表信息 payWayDto:{}",payWayDto); 5 6 try{ 7 doSomething(); 8 return TransactionResult.newSuccess(Boolean.TRUE); 9 }catch (Exception e){ 10 if(e instanceof DuplicateKeyException){ 11 logger.info("新增通道失败,唯一主机冲突 error:{}",e.getMessage()); 12 throw new PaymentException(ResultCode.C500.getCode(),"新增通道失败!通道已经存在"); 13 } 14 throw new PaymentException(ResultCode.C500.getCode(),e.getMessage()); 15 } 16 }
问了同事的意图,他希望如果提供方抛出PaymentException的时候,服务方能够捕获到对应PaymentException。然而,在上面的代码中,消费者捕获不到PaymentException,只能捕获到RuntimeException。看到这个问题,因为没有这方面的经验,第一时间也是懵逼。不过问题不大,毕竟遇到这种情况,不懂的问题慢慢查就好。
BUG定位
自己写了个测试,发现我的代码居然能正常捕获到PaymentException。一时间也没发现有啥不同,就开启debug模式,反正遇到RPC的问题,第一时间怀疑dubbo就对了。开始翻阅代码,直到翻阅到 ExceptionFilter 这个类,发现了问题所在。
1 public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { 2 try { 3 Result result = invoker.invoke(invocation); 4 if (result.hasException() && GenericService.class != invoker.getInterface()) { 5 try { 6 Throwable exception = result.getException(); 7 8 // 如果是checked异常,直接抛出 9 if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) { 10 return result; 11 } 12 // 在方法签名上有声明,直接抛出 13 try { 14 Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes()); 15 Class<?>[] exceptionClassses = method.getExceptionTypes(); 16 for (Class<?> exceptionClass : exceptionClassses) { 17 if (exception.getClass().equals(exceptionClass)) { 18 return result; 19 } 20 } 21 } catch (NoSuchMethodException e) { 22 return result; 23 } 24 25 // 未在方法签名上定义的异常,在服务器端打印ERROR日志 26 logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() 27 + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() 28 + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception); 29 30 // 异常类和接口类在同一jar包里,直接抛出 31 String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface()); 32 String exceptionFile = ReflectUtils.getCodeBase(exception.getClass()); 33 if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){ 34 return result; 35 } 36 // 是JDK自带的异常,直接抛出 37 String className = exception.getClass().getName(); 38 if (className.startsWith("java.") || className.startsWith("javax.")) { 39 return result; 40 } 41 // 是Dubbo本身的异常,直接抛出 42 if (exception instanceof RpcException) { 43 return result; 44 } 45 46 // 否则,包装成RuntimeException抛给客户端 47 return new RpcResult(new RuntimeException(StringUtils.toString(exception))); 48 } catch (Throwable e) { 49 logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost() 50 + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() 51 + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e); 52 return result; 53 } 54 } 55 return result; 56 } catch (RuntimeException e) { 57 logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() 58 + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() 59 + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e); 60 throw e; 61 } 62 }
这里可以看到,dubbo服务方处理自己抛出异常的时候会进行区别对待,checked 异常、方法上有抛出的异常、异常类和接口在同一个jar包里的、JDK自带的、dubbo自带的异常 都是直接抛出,剩余的异常全都封装为Runtime抛出。同事的代码里,异常类和接口类没有放在同一个jar包,所以dubbo会将其封装为RunTimeException抛出。
思考总结
问题很快就找到了,迅速解决问题后,dubbo这么写的原因是什么呢?或者说dubbo不怎么写会怎么样呢?
ExceptionFilter 类是在dubbo提供者中执行的,用于对处理服务方内部异常。先假设dubbo不这么处理,会发生什么呢? 当提供者抛出异常的时候,如果消费者不能识别该异常,将无法进行正常的反序列化,导致程序错误。所以上面特殊处理的多种异常都是服务提供者能确定消费者能够正常反序列化的前提下才将该异常抛出,否则都包装成RunTimeException抛出。
因此dubbo上述代码是考虑到消费者无法识别异常的情况下,做的一项安全处理。