Dubbo处理下游返回的自定义异常

前言

原本的 nacos + feign 项目改造成了 nacos + dubbo
发现原本的自定义业务异常 BusinessException 被转换成了 RuntimeException 返回到上游调用方

这里会先介绍我使用的处理方案
然后贴了一部分网上介绍的方案

1. 我的处理方案

采用自定义拦截器,重新 set 返回的 Exception

1.1 定义 Provider 拦截器
这里给了个 order 值,在 ExceptionFilter 处理前,优先执行自定义的 provider拦截器


import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.common.logger.ErrorTypeAwareLogger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.ReflectUtils;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.rpc.*;
import org.apache.dubbo.rpc.service.GenericService;

import java.lang.reflect.Method;
import java.util.Map;

import static org.apache.dubbo.common.constants.LoggerCodeConstants.CONFIG_FILTER_VALIDATION_EXCEPTION;

/**
 * 自定义异常拦截器
 *
 * @author weiheng
 **/
@Activate(group = CommonConstants.PROVIDER, order = Integer.MAX_VALUE)
public class DubboProviderExceptionFilter implements Filter, Filter.Listener {
    private ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(DubboProviderExceptionFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        return invoker.invoke(invocation);
    }

    @Override
    public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
        if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
            try {
                Throwable exception = appResponse.getException();

                if (exception instanceof BusinessException) {
                    // 如果是自定义业务异常,则直接返回到上游
                    logger.info("BusinessException 异常 -> " + exception.getMessage());
                    BusinessException bizException = (BusinessException) exception;
                    Map<String, String> attach = appResponse.getAttachments();
                    attach.put(HttpConstant.RESULT_CODE, bizException.getCode().toString());
                    attach.put(HttpConstant.RESULT_MSG, bizException.getMessage());
                    return;
                }
                // directly throw if it's checked exception
                if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
                    return;
                }
                // directly throw if the exception appears in the signature
                try {
                    Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
                    Class<?>[] exceptionClasses = method.getExceptionTypes();
                    for (Class<?> exceptionClass : exceptionClasses) {
                        if (exception.getClass().equals(exceptionClass)) {
                            return;
                        }
                    }
                } catch (NoSuchMethodException e) {
                    return;
                }

                // for the exception not found in method's signature, print ERROR message in server's log.
                logger.error(CONFIG_FILTER_VALIDATION_EXCEPTION, "", "", "Got unchecked and undeclared exception which called by " + RpcContext.getServiceContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);

                // directly throw if exception class and interface class are in the same jar file.
                String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
                String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
                if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
                    return;
                }
                // directly throw if it's JDK exception
                String className = exception.getClass().getName();
                if (className.startsWith("java.") || className.startsWith("javax.")) {
                    return;
                }
                // directly throw if it's dubbo exception
                if (exception instanceof RpcException) {
                    return;
                }

                // otherwise, wrap with RuntimeException and throw back to the client
                appResponse.setException(new RuntimeException(StringUtils.toString(exception)));
            } catch (Throwable e) {
                logger.warn(CONFIG_FILTER_VALIDATION_EXCEPTION, "", "", "Fail to ExceptionFilter when called by " + RpcContext.getServiceContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
            }
        }
    }

    @Override
    public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
        logger.error(CONFIG_FILTER_VALIDATION_EXCEPTION, "", "", "Got unchecked and undeclared exception which called by " + RpcContext.getServiceContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
    }

    // For test purpose
    public void setLogger(ErrorTypeAwareLogger logger) {
        this.logger = logger;
    }
}

1.2 定义 consumer 拦截器

import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.common.logger.ErrorTypeAwareLogger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.rpc.*;

import java.util.Map;

import static org.apache.dubbo.common.constants.LoggerCodeConstants.CONFIG_FILTER_VALIDATION_EXCEPTION;

/**
 * 自定义异常拦截器
 *
 * @author weiheng
 **/
@Activate(group = CommonConstants.CONSUMER)
public class DubboConsumerExceptionFilter implements Filter, Filter.Listener {
    private ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(DubboConsumerExceptionFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        return invoker.invoke(invocation);
    }

    @Override
    public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
        Map<String, String> attach = appResponse.getAttachments();
        String code = attach.get(HttpConstant.RESULT_CODE);
        String msg = attach.get(HttpConstant.RESULT_MSG);
        if (!StringUtils.isBlank(code)) {
            appResponse.setException(new BusinessException(Integer.parseInt(code), msg));
        }
    }

    @Override
    public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
        logger.error(CONFIG_FILTER_VALIDATION_EXCEPTION, "", "", "Got unchecked and undeclared exception which called by " + RpcContext.getServiceContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
    }

    // For test purpose
    public void setLogger(ErrorTypeAwareLogger logger) {
        this.logger = logger;
    }
}

1.3 在 META-INFO/dubbo 目录下添加Filter
在这里插入图片描述

文件名:org.apache.dubbo.rpc.Filter
内容如下:

dubboProviderException=com.mea.pay.common.filter.DubboProviderExceptionFilter
dubboConsumerException=com.mea.pay.common.filter.DubboConsumerExceptionFilter

最后亲测OK,下游服务提供方的自定义异常 BusinessException 可以被上游消费端直接获取到
参考资料:
Dubbo官网Filter扩展教程
gihub码友方案

再往下是网络搜到到的一些相关知识,仅供参考

2. dubbo是如何处理异常的?

dubbo是通过ExceptionFilter过滤器来对异常做处理的,处理结果的时候会执行ExceptionFilter中的onResponse()方法。

package org.apache.dubbo.rpc.filter;

import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.ReflectUtils;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.rpc.Filter;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.rpc.service.GenericService;

import java.lang.reflect.Method;

@Activate(group = CommonConstants.PROVIDER)
public class ExceptionFilter implements Filter, Filter.Listener {
    private Logger logger = LoggerFactory.getLogger(ExceptionFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        return invoker.invoke(invocation);
    }

    @Override
    public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
         //是否存在异常
        if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
            try {
                Throwable exception = appResponse.getException();

                // directly throw if it's checked exception
                // 1.如果是checked异常,直接抛出
                if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
                    return;
                }
                // directly throw if the exception appears in the signature
                try {
                    // 2.如果异常有在方法签名上声明,直接抛出
                    Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
                    Class<?>[] exceptionClasses = method.getExceptionTypes();
                    for (Class<?> exceptionClass : exceptionClasses) {
                        if (exception.getClass().equals(exceptionClass)) {
                            return;
                        }
                    }
                } catch (NoSuchMethodException e) {
                    return;
                }

                // 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.getServiceContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);

                // directly throw if exception class and interface class are in the same jar file.
                // 3.查看异常是否和接口是在同一个jar包文件下,如果是,直接抛出异常
                String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
                String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
                if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
                    return;
                }
                // directly throw if it's JDK exception
                //4.如果是在java包或者javax包下的异常也直接抛出
                String className = exception.getClass().getName();
                if (className.startsWith("java.") || className.startsWith("javax.")) {
                    return;
                }
                // directly throw if it's dubbo exception
                //5.如果是RpcException 那么也直接抛出
                if (exception instanceof RpcException) {
                    return;
                }

                // otherwise, wrap with RuntimeException and throw back to the client
                //6.其他的情况都包装成RuntimeException抛出
                appResponse.setException(new RuntimeException(StringUtils.toString(exception)));
            } catch (Throwable e) {
                logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getServiceContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
            }
        }
    }
}

从源码中我们可以看到对异常的处理会分几种情况处理

  1. 如果是checked的异常,那么会直接抛出
  2. 如果异常有在接口方法上有显示的声明,那么直接抛出
  3. 如果异常类和接口是在同一个jar包文件下,直接抛出异常
  4. 如果是在java包或者javax包下的异常也直接抛出
  5. 如果是RpcException,那么直接抛出异常
  6. 其他情况下会把异常包装成RuntimeException,然后抛出

我们自己定义的BaseException只满足情况6,所以会包装成一个RunTimeException然后抛出。

3. 如何解决这个问题

根据上文的源码分析,我们可以知道只要满足1-5条件中的一个即可,我们的异常就不会被dubbo包装成RuntimeException了。

条件1:如果是受检异常那么会直接抛出. 但是我们自定义的异常肯定是继承RuntimeException的,这个可以pass了。
条件2:如果异常有在接口方法上有声明,那么会直接将异常抛出. 这个应该是行的通的,我们可以尝试。
条件3:如果异常类和接口是在同一个jar包文件下,直接抛出异常. 如果把异常放在api中定义那么也是可以达到我们的预期效果的。
条件4:如果是java包或者javax包下的Exception,直接抛出。明显这个我们也可以pass了,我们自己定义的异常不会写在java包或者javax包下。
条件5:如果是RpcException,那么会直接抛出。我们自己自定义的Exception不会继承Dubbo的RpcException,所以这个也pass。

所以我们得出的解决方法有以下几个:
1.在方法签名上声明抛出的异常
2.将异常定义在api的jar包中。
3.因为dubbo处理异常是交给ExceptionFilter来处理的,我们只需要自定义一个Filter,将自定义的Filter加入到dubbo的Filter链中,并且移除原来的ExceptionFilter,这样就可以利用我们自定义的Filter来处理Exception了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值