背景
我们的项目使用了Dubbo进行不同系统服务间的调用,当服务端发生异常时,我们希望把异常传递给消费端,由消费端对异常进行捕获并处理。但在实际使用中,发现以往的异常处理在dubbo服务中并不能奏效。例如,自定义异常类BizException继承RuntimeException,当服务端抛出这个异常时,消费端并不能捕获它。
//自定义BizException CLass BizException extends RuntimeException{ ... } //服务端提供方法 public void provider(){ ...//抛出BizException } //消费端 public void consumer(){ try{ provider();//dubbo调用provider方法 } catch(BizException){ ...//捕获异常处理 } }
Dubbo异常处理机制
当Dubbo的provider端抛出异常(Throwable),会被provider端的ExceptionFilter拦截到,执行以下invoke方法: (源码位置:com.alibaba.dubbo.rpc.filter.ExceptionFilter)

/* * Copyright 1999-2011 Alibaba Group. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.alibaba.dubbo.rpc.filter; import java.lang.reflect.Method; import com.alibaba.dubbo.common.Constants; import com.alibaba.dubbo.common.extension.Activate; import com.alibaba.dubbo.common.logger.Logger; import com.alibaba.dubbo.common.logger.LoggerFactory; import com.alibaba.dubbo.common.utils.ReflectUtils; import com.alibaba.dubbo.common.utils.StringUtils; import com.alibaba.dubbo.rpc.Filter; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; import com.alibaba.dubbo.rpc.Result; import com.alibaba.dubbo.rpc.RpcContext; import com.alibaba.dubbo.rpc.RpcException; import com.alibaba.dubbo.rpc.RpcResult; import com.alibaba.dubbo.rpc.service.GenericService; /** * ExceptionInvokerFilter * <p> * 功能: * <ol> * <li>不期望的异常打ERROR日志(Provider端)<br> * 不期望的日志即是,没有的接口上声明的Unchecked异常。 * <li>异常不在API包中,则Wrap一层RuntimeException。<br> * RPC对于第一层异常会直接序列化传输(Cause异常会String化),避免异常在Client出不能反序列化问题。 * </ol> * * @author william.liangf * @author ding.lid */ @Activate(group = Constants.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 { Result result = invoker.invoke(invocation); if (result.hasException() && GenericService.class != invoker.getInterface()) { try { Throwable exception = result.getException(); // 如果是checked异常,直接抛出 if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) { return result; } // 在方法签名上有声明,直接抛出 try { Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes()); Class<?>[] exceptionClassses = method.getExceptionTypes(); for (Class<?> exceptionClass : exceptionClassses) { if (exception.getClass().equals(exceptionClass)) { return result; } } } catch (NoSuchMethodException e) { return result; } // 未在方法签名上定义的异常,在服务器端打印ERROR日志 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); // 异常类和接口类在同一jar包里,直接抛出 String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface()); String exceptionFile = ReflectUtils.getCodeBase(exception.getClass()); if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){ return result; } // 是JDK自带的异常,直接抛出 String className = exception.getClass().getName(); if (className.startsWith("java.") || className.startsWith("javax.")) { return result; } // 是Dubbo本身的异常,直接抛出 if (exception instanceof RpcException) { return result; } // 否则,包装成RuntimeException抛给客户端 return new RpcResult(new RuntimeException(StringUtils.toString(exception))); } catch (Throwable e) { logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e); return result; } } return result; } catch (RuntimeException e) { logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e); throw e; } } }
1、调用结果有异常且未实现GenericService接口,进入后续判断逻辑,否则直接返回结果
2、不是RuntimeException类型的异常,并且是受检Checked异常(继承Exception),直接抛出
3、在方法签名上声明了异常,直接抛出
4、未在方法签名上定义的异常,在服务端打印ERROR日志
5、异常类和接口类在同一jar包里,直接抛出
6、JDK自带的异常(以java.或javax.开头)直接抛出
7、Dubbo本身的异常,直接抛出(RpcException)
8、不满足上述条件时会被包装成RuntimeException抛给consumer端
由此可见,我们定义在公共包中的BizException在provider端抛出的时候会被Dubbo的异常过滤包装成RuntimeException抛给consumer端,而consumer端捕获的是BizException,所以并不会捕获到它。
解决方法
1、provider端实现GenericService接口:
缺点:需要自己实现invoke方法。
2、在provider端方法签名上显式声明异常:
public void providerFunctionA(String str) throws BizException
缺点:代码侵入性高,基本所有接口都要写。
3、将自定义异常的包名以java.或javax.开头
package java.common.core Class BizException extends RuntimeException{ ... }
缺点:不满足项目命名规范。
4、自定义异常声明为受检异常:
//继承Exception Class BizException1 extends Exception{ ... } Class BizException2 extends Exception{ ... } //服务端方法中抛出 function() throws BizException1,BizException2{ ... }
缺点:项目规模扩大后,受检异常过多会难以处理且同2情况代码侵入性高。
5、将异常类与接口类放在同一个jar包中。
最终方案
这里采用方案5(将异常类与接口类放在同一个jar包中)作为最终方案,注意使用过程中应避免存在链式调用。
1、在公共包中自定义父类异常,继承RuntimeException
package com.xxx.common.core Class BizException extends RuntimeExceptio{ ... }
2、各个provider端定义子类异常,继承BizException并与接口放在同个工程中
//放于服务端facade接口工程中 package com.xxx.user.facade.exceptions //子类异常 Class UserBizException extends BizException{ ... }
3、各个provider端方法抛出自己模块的子类异常
package com.xxx.user.facade.service.impl Class ProviderServiceImpl{ //服务提供方法实现类 public void provider(){ ... throw UserBizException.XXXXXX;//抛出子类异常 ... } }
4、consumer端统一捕获父类异常BizException
//消费端方法 void consumer(){ try{ providerService.provider();//调用服务端方法 } catch(BizException e){ handleException();//异常捕获处理 } }
至此,provider端抛出的自定义异常可传递到consumer端被捕获处理。但是在使用中我们还会发现一个问题,那就是当provider端抛出其他的非自定义异常(例如Dubbo自身的RpcException、系统其它运行时异常等)时,consumer端并没能捕获到进行后续处理。所以,这里添加一个拦截器来确保各个provider端抛出的异常都是自己模块下的自定义异常。
5、新增一个aop切面
<bean id="userBizExceptionFilter" class="com.xxx.user.facade.exceptions.UserBizExceptionFilter" /> <aop:config> <aop:aspect ref="userBizExceptionFilter"> <aop:pointcut id="userBizExceptionAspect" expression="execution(* com.xxx.user.facade.service..*.*(..))"/> <aop:after-throwing pointcut-ref="userBizExceptionAspect" method="afterThrowing" throwing="e"/> </aop:aspect> </aop:config>
6、拦截器,发生其他异常时封装成模块自定义异常抛出
package com.xxx.user.facade.exceptions Class UserBizExceptionFilter implements ThrowAdvice{ public void afterThrowing(Exception e) throws Throwable{ //业务异常时直接抛出 if(e instanceof UserBizException){ throw (UserBizException) e; } //dubbo服务异常时封装成子类异常抛出 else if (e instanceof RpcException){ throw UserBizException.XXXX; } //其它系统异常时封装成子类异常抛出 else{ throw UserBizException.XXXX; } } }