基于dubbo的分布式应用中的统一异常处理

背景
目前在做的项目使用 dubbo 作为分布式服务框架,新项目开发过程中遇到一个问题:provider 端抛出了自定义的业务异常,而 consumer 接收到的却是 RpcException,原来的业务异常(包括异常栈)被包装到了 message 中,导致 consumer 不能正确获取 provider 想要提供的 message。

出现这个问题,有几个前提:

自定义的异常
自定义的业务异常继承 RuntimeException,属于运行期/非检查异常
自定义的业务异常在二方包/三方包中,不在 provider 提供的API包中
先说结论
基于上面的几个前提,可以发现自定义的异常可能会在 consumer 接收时反序列化失败,因为这个异常不一定在 consumer中存在。而dubbo为了避免这种情况出现,使用 ExceptionFilter 过滤器对异常进行了包装,转换为 RuntimeException 提供给 consumer 。

最后我利用 Spring AOP 拦截掉provider所有异常,将异常包装成Response(一个自定义的返回值对象POJO)返回给consumer,规避掉了 dubbo 的处理。

ExceptionFilter - dubbo 的异常处理源码

@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;
    }

    @Override
    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();

                    // 检查异常,直接抛出
                    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;
                    }

                    // 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.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;
        }
    }

}

重点的话,就在:

// 否则,包装成RuntimeException抛给客户端
return new RpcResult(new RuntimeException(StringUtils.toString(exception)));
为了避免反序列化失败,dubbo这样处理也是合理的,但确实和目前项目的应用情况有冲突,要如何解决这个问题?

常规来说有如下方法:
将该异常的包名以java.或者javax. 开头
不符合规范,排除

异常继承Exception
自定义的业务异常本身属于 RuntimeException, 排除

异常类和接口类在同一jar包里
较大的项目一般都会有一些common包,定义好异常类型,使用二方包的方式引用,所以也不适用,排除

provider的api明确写明throws XxxException
作为服务端,不应显式抛出异常给客户的进行处理,排除

实现 dubbo 的 filter,自定 provider 的异常处理逻辑
可以在原有逻辑基础上增加:

if("自定义异常的包路径".equals(exceptionFile)){
    return result;
}

可行,但是逻辑中写死了异常的路径,而且也不能保证后期其他 consumer,会引用这个二方包。

最终方案
结合项目的实际情况,我们项目中 provider 提供的接口返回值全部包装一层再提供出去,这样的好处是服务和服务之间的交互使用的数据对象都是一样的,便于使用,这个对象比较常规:

public class Response<T> implements Serializable {
    private static final long serialVersionUID = -1L;
    private boolean success;
    private T result;
    private String error;
    
    ...
}

我觉得Response中增加 private String code; 用于标记错误类型,会更好一些
基于这个前提,我想利用 Spring AOP 拦截掉provider所有异常,将异常包装成Response对外提供,规避掉 dubbo 的处理。如果是业务异常利用 error 提供简单异常信息给外部,如果非业务异常只提示服务调用失败,具体错误输出到日志,再通过kibana等日志平台查看。

代码:

/**
 * 服务层异常处理器
 * <p>
 * Created by XuRui on 2018/5/28.
 */
@Component
@Aspect
@Slf4j
public class ServiceExceptionHandle {

    /**
     * 指定返回值为Response类型的Service
     */
    @Pointcut(value = "execution(public com.xurui.youth.dto.Response com.xurui.youth.service..*Service*.*(..))")
    private void servicePointcut() {
        // Do nothing just pointcut
    }

    /**
     * 异常处理切面
     * 将异常包装为Response,避免dubbo进行包装
     *
     * @param pjp 处理点
     * @return 返回异常处理结果
     */
    @Around("servicePointcut()")
    public Object doAround(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        try {
            return pjp.proceed();
            // 业务自定义异常
        } catch (ServiceException | JsonResponseException | ServiceResponseException e) {
            processException(pjp, args, e);
            return Response.fail(e.getMessage());
        } catch (Exception e) {
            processException(pjp, args, e);
            return Response.fail("服务调用失败");
        } catch (Throwable throwable) {
            processException(pjp, args, throwable);
            return Response.fail("系统异常");
        }
    }

    /**
     * 处理异常
     *
     * @param joinPoint 切点
     * @param args      参数
     * @param throwable 异常
     */
    private void processException(final ProceedingJoinPoint joinPoint, final Object[] args, Throwable throwable) {
        String inputParam = "";
        if (args != null && args.length > 0) {
            StringBuilder sb = new StringBuilder();
            for (Object arg : args) {
                sb.append(",");
                sb.append(arg);
            }
            inputParam = sb.toString().substring(1);
        }
        log.warn("\n 方法: {}\n 入参: {} \n 错误信息: {}", joinPoint.toLongString(), inputParam, Throwables.getStackTraceAsString(throwable));
    }
}

但是这个方式因为拦截掉了service的异常,所以如果多个service存在事务传递的情况,会导致事务失效,因为我们这个项目要求事务单独抽出一层manage来做,所以没有问题。

但是也可以优化:新定义一个切点,声明任何持有@Transactional注解的方法,将这部分需要事务的方法单独处理,最终代码如下:

/**
 * 服务层异常处理器
 * <p>
 * Created by XuRui on 2018/5/28.
 */
@Component
@Aspect
@Slf4j
public class ServiceExceptionHandle {

    /**
     * 返回值类型为Response的Service
     */
    @Pointcut(value = "execution(public com.xurui.youth.dto.Response com.xurui.youth.service..*Service*.*(..))")
    private void servicePointcut() {
    }

    /**
     * 任何持有@Transactional注解的方法
     */
    @Pointcut(value = "@annotation(org.springframework.transaction.annotation.Transactional)")
    private void transactionalPointcut() {
    }

    /**
     * 异常处理切面
     * 将异常包装为Response,避免dubbo进行包装
     *
     * @param pjp 处理点
     * @return Object
     */
    @Around("servicePointcut() && !transactionalPointcut()")
    public Object doAround(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        try {
            return pjp.proceed();
        } catch (ServiceException | JsonResponseException | ServiceResponseException e) { // 业务自定义异常
            processException(pjp, args, e);
            return Response.fail(e.getMessage());
        } catch (Exception e) {
            processException(pjp, args, e);
            return Response.fail("服务调用失败");
        } catch (Throwable throwable) {
            processException(pjp, args, throwable);
            return Response.fail("系统异常");
        }
    }

    /**
     * 任何持有@Transactional注解的方法异常处理切面
     * 将自定义的业务异常转为RuntimeException:
     * 1.规避dubbo的包装,让customer可以正常获取message
     * 2.抛出RuntimeException使事务可以正确回滚
     * 其他异常不处理
     *
     * @param pjp 处理点
     * @return Object
     */
    @Around("servicePointcut() && transactionalPointcut()")
    public Object doTransactionalAround(ProceedingJoinPoint pjp) throws Throwable {
        try {
            return pjp.proceed();
        } catch (ServiceException | JsonResponseException | ServiceResponseException e) {
            // dubbo会将异常捕获进行打印,这里就不打印了
            // processException(pjp, args, e);
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 处理异常
     *
     * @param joinPoint 切点
     * @param args      参数
     * @param throwable 异常
     */
    private void processException(final ProceedingJoinPoint joinPoint, final Object[] args, Throwable throwable) {
        String inputParam = "";
        if (args != null && args.length > 0) {
            StringBuilder sb = new StringBuilder();
            for (Object arg : args) {
                sb.append(",");
                sb.append(arg);
            }
            inputParam = sb.toString().substring(1);
        }
        log.warn("\n 方法: {}\n 入参: {} \n 错误信息: {}", joinPoint.toLongString(), inputParam, Throwables.getStackTraceAsString(throwable));
    }
}
  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值