自定义头部 -->

Dubbo 异常处理的正确姿势

Dubbo 异常处理的正确姿势

写在前面 dubbo在Provider端抛出时候, 自定义的请求在特定情况下是会被转化为RuntimeException 抛出, 可能很多情况下, 会不符合我们预期的要求

源码

Dubbo 的异常处理是通过 ExceptionFilter 实现的

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.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.ListenableFilter;
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;


/**
 * ExceptionInvokerFilter
 * <p>
 * 功能:
 * <ol>
 * <li>不期望的异常打ERROR日志(Provider端)<br>
 *     不期望的日志即是,没有的接口上声明的Unchecked异常。
 * <li>异常不在API包中,则Wrap一层RuntimeException。<br>
 *  RPC对于第一层异常会直接序列化传输(Cause异常会String化),避免异常在Client出不能反序列化问题。
 * </ol>
 *
 */
@Activate(group = CommonConstants.PROVIDER)
public class ExceptionFilter extends ListenableFilter {

    public ExceptionFilter() {
        super.listener = new ExceptionListener();
    }

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

    static class ExceptionListener implements Listener {

        private Logger logger = LoggerFactory.getLogger(ExceptionListener.class);

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

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

                    // 未在方法签名上定义的异常,在服务器端打印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;
                    }
                    // 是JDK自带的异常,直接抛出
                    String className = exception.getClass().getName();
                    if (className.startsWith("java.") || className.startsWith("javax.")) {
                        return;
                    }
                    // 是Dubbo本身的异常,直接抛出
                    if (exception instanceof RpcException) {
                        return;
                    }

                    // 否则,包装成RuntimeException抛给客户端
                    appResponse.setException(new RuntimeException(StringUtils.toString(exception)));
                    return;
                } 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;
                }
            }
        }

        @Override
        public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
            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);
        }

        public void setLogger(Logger logger) {
            this.logger = logger;
        }
    }
}

从上面我们可以看出,dubbo的处理方式主要是:

  1. 如果provider实现了GenericService接口,直接抛出
  2. 如果是checked异常,直接抛出
  3. 在方法签名上有声明,直接抛出
  4. 异常类和接口类在同一jar包里,直接抛出
  5. 是JDK自带的异常,直接抛出
  6. 是Dubbo本身的异常,直接抛出
  7. 否则,包装成RuntimeException抛给客户端

如何正确捕获业务异常

有多种方法可以解决这个问题,每种都有优缺点,这里不做详细分析,仅列出供参考:

  1. 将该异常的包名以"java.或者"javax. " 开头
  2. 使用受检异常(继承Exception)
  3. 不用异常,使用错误码
  4. 把异常放到provider-api的jar包中
  5. 判断异常message是否以XxxException.class.getName()开头(其中XxxException是自定义的业务异常)
  6. provider实现GenericService接口
  7. provider的api明确写明throws XxxException,发布provider(其中XxxException是自定义的业务异常)
  8. 实现dubbo的filter,自定义provider的异常处理逻辑(方法可参考之前的文章给dubbo接口添加白名单——dubbo Filter的使用)

实现自定的dubbo Exception Filter

DubboExceptionFilter

首先我们拷贝org.apache.dubbo.rpc.filter.ExceptionFilter的源码, 稍微做点改动

package com.barm.archetypes.server.filter;

import com.barm.common.domain.enums.ResultEnum;
import com.barm.common.exceptions.ProviderException;
import com.barm.common.exceptions.ProviderInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
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 javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.lang.reflect.Method;

/**
 * @description DubboExceptionFilter
 * @author Allen
 * @version 1.0.0
 * @create 2020/3/16 22:38
 * @e-mail allenalan@139.com
 * @copyright 版权所有 (C) 2020 allennote
 */
@Slf4j
@Activate(group = CommonConstants.PROVIDER)
public class DubboExceptionFilter extends ListenableFilter {

    public DubboExceptionFilter() {
        super.listener = new CurrExceptionListener();
    }

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

    static class CurrExceptionListener extends ExceptionListener {

        @Override
        public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {

            // 发生异常,并且非泛化调用
            if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
                try {
                Throwable exception = appResponse.getException();
                log.error("exception error: ", exception);
                // 1 如果是 ProviderException 异常,直接返回
                if (exception instanceof ProviderException) {
                    return;
                }

                // 2 构建Provider 信息
                ProviderInfo providerInfo = buildProviderInfo(invocation);
                // 3 如果是参数校验的 ConstraintViolationException 异常,则封装返回
                if (exception instanceof ConstraintViolationException) {
                    appResponse.setException(new ProviderException(ResultEnum.INVALID_REQUEST_PARAM_ERROR, providerInfo, this.violationMsg((ConstraintViolationException) exception)));
                    return;
                }
                appResponse.setException(new ProviderException(ResultEnum.RPC_ERROR, providerInfo, StringUtils.toString(exception)));
                return;
            } catch (Throwable e) {
                log.warn("Fail to DubboExceptionFilter when called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
                return;
            }
            }

        }

        // 将 ConstraintViolationException 转换成 ProviderException
        private String violationMsg(ConstraintViolationException ex) {
            // 拼接错误
            StringBuilder detailMessage = new StringBuilder();
            for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
                // 使用 ; 分隔多个错误
                if (detailMessage.length() > 0) {
                    detailMessage.append(";");
                }
                // 拼接内容到其中
                detailMessage.append(constraintViolation.getMessage());
            }
            // 返回异常
            return detailMessage.toString();
        }

    }

    private static ProviderInfo buildProviderInfo(Invocation invocation) {
        RpcContext context = RpcContext.getContext();
        ProviderInfo providerInfo = new ProviderInfo();
        providerInfo.setLocalAddress(context.getLocalAddressString());
        providerInfo.setRemoteAddress(context.getRemoteAddressString());
        providerInfo.setApplicationName(context.getUrl().getParameter("application"));
        providerInfo.setMethodName(invocation.getMethodName());
        providerInfo.setAttachments(invocation.getAttachments());
        return providerInfo;
    }

    static class ExceptionListener implements Listener {

        @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
                    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<?>[] exceptionClassses = method.getExceptionTypes();
                        for (Class<?> exceptionClass : exceptionClassses) {
                            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.
                    log.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);

                    // 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)));
                    return;
                } catch (Throwable e) {
                    log.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;
                }
            }
        }

        @Override
        public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
            log.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);
        }

    }

}
  • 改动:

  • exceptionFilter

  • 将 替换为ApplicationException

  • 添加对 校验异常 ConstraintViolationException 的判断处理

  • @Activate注解用于 DubboExceptionFilter 过滤器仅在服务提供者生效

  • 这里利用了Dubbo的 SPI 机制, 如果不太明白的话可以品一品<Dubbo的SPI中的IOC和API> 这篇文章

ResultEnum

package com.barm.common.domain.enums;
/**
 * @description 返回结果枚举
 *              1000000000
 *              10---------> 1~ 2 位: 消息提示类型 e.g. 10 正常, 20 系统异常, 30 业务异常
 *                0000-----> 3~ 6 位: 服务类型 e.g. 0001 用户服务
 *                    0000-> 7~10 位: 错误类型 e.g. 5000 参数校验错误
 * @author Allen
 * @version 1.0.0
 * @create 2020/2/24 0:21
 * @e-mail allenalan@139.com
 * @copyright 版权所有 (C) 2020 allennote
 */
public enum ResultEnum {
    // 200 操作成功 500 操作失败
    SUCCESS(1000000000, "操作成功"),
    FAIL(2000000000, "操作失败"),
    RPC_ERROR(2000001000, "远程调用失败"),
    INVALID_REQUEST_PARAM_ERROR(2000005000, "参数校验错误"),
    ;

    private Integer code;

    private String msg;

    ResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

ApplicationException 自定义异常

package com.barm.common.exceptions;

import com.barm.common.domain.enums.ResultEnum;
import com.google.common.base.Joiner;
import lombok.Getter;

/**
 * @author Allen
 * @version 1.0.0
 * @description ApplicationException
 * @create 2020/2/23 23:44
 * @e-mail allenalan@139.com
 * @copyright 版权所有 (C) 2020 allennote
 */
@Getter
public class ApplicationException extends RuntimeException{

    private static final long serialVersionUID = 1L;

    /** 结果枚举*/
    private final ResultEnum resultEnum;
    /** 自定义异常信息*/
    private final String errMsg;
    /** 异常码 */
    private final Integer errCode;

    public ApplicationException() {
        super();
        this.resultEnum = ResultEnum.FAIL;
        this.errCode = resultEnum.getCode();
        this.errMsg = resultEnum.getMsg();
    }

    public ApplicationException(ResultEnum resultEnum) {
        super(resultEnum.getMsg());
        this.errCode = resultEnum.getCode();
        this.errMsg = resultEnum.getMsg();
        this.resultEnum = resultEnum;
    }

    public ApplicationException(String... errMsgs) {
        super(Joiner.on(",").skipNulls().join(errMsgs));
        this.resultEnum = ResultEnum.FAIL;
        this.errMsg = super.getMessage();
        this.errCode = this.resultEnum.getCode();
    }

    public ApplicationException(ResultEnum resultEnum, String... errMsgs) {
        super(Joiner.on(",").skipNulls().join(errMsgs));
        this.resultEnum = resultEnum;
        this.errMsg = super.getMessage();
        this.errCode = this.resultEnum.getCode();
    }

/*    @Override
    public synchronized Throwable fillInStackTrace() {
        return this;
    }*/
}

ExceptionHandlers 异常统一处理类

package com.barm.order.server;

import com.barm.common.domain.enums.ResultEnum;
import com.barm.common.domain.vo.ResultVO;
import com.barm.common.exceptions.ApplicationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;

/**
 * @author Allen
 * @version 1.0.0
 * @description 异常处理类
 * @create 2020/2/23 23:43
 * @e-mail allenalan@139.com
 * @copyright 版权所有 (C) 2020 barm
 */
@Slf4j
@RestControllerAdvice
public class ExceptionHandlers {
    
    /* @ExceptionHandler(value = ApplicationException.class)
    public ResultVO applicationException(ApplicationException ex){
        log.error("ApplicationException: ", ex);
        String errMsg = ex.getErrMsg();
        log.info("exception for application with errMsg: " + errMsg);
        return new ResultVO(ex.getResultEnum(), errMsg);
    }*/
    
    @ExceptionHandler(value = ProviderException.class)
    public ResultVO applicationException(ProviderException ex){
        log.error("ProviderException: ", ex);
        log.info("exception for Provider info: " + ex.getProviderInfo().toString());
        String errMsg = ex.getErrMsg();
        log.info("exception for Provider with errMsg: " + errMsg);
        return new ResultVO(ex.getResultEnum(), errMsg);
    }

    /*@ExceptionHandler(value = ConstraintViolationException.class)
    public ResultVO constraintViolationExceptionHandler(ConstraintViolationException ex) {
        // 拼接错误
        StringBuilder detailMessage = new StringBuilder();
        for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
            // 使用 , 分隔多个错误
            if (detailMessage.length() > 0) {
                detailMessage.append(",");
            }
            // 拼接内容到其中
            detailMessage.append(constraintViolation.getMessage());
        }
        return new ResultVO(ResultEnum.INVALID_REQUEST_PARAM_ERROR,ResultEnum.INVALID_REQUEST_PARAM_ERROR.getMsg() + ":" + detailMessage.toString());
    }*/
}

application.yaml

  • Provider端配置
dubbo:
  provider: # Dubbo 服务端配置
    cluster: failfast # 集群方式,可选: failover/failfast/failsafe/failback/forking
    retries: 0 # 远程服务调用重试次数, 不包括第一次调用, 不需要重试请设为0
    timeout: 600000 # 远程服务调用超时时间(毫秒)
    token: true # 令牌验证, 为空表示不开启, 如果为true, 表示随机生成动态令牌
    dynamic: true # 服务是否动态注册, 如果设为false, 注册后将显示后disable状态, 需人工启用, 并且服务提供者停止时, 也不会自动取消册, 需人工禁用. 
    delay: -1 # 延迟注册服务时间(毫秒)- , 设为-1时, 表示延迟到Spring容器初始化完成时暴露服务
    version: 1.0.0 # 服务版本
    validation: true # 是否启用JSR303标准注解验证, 如果启用, 将对方法参数上的注解进行校验
    filter: -exception # 服务提供方远程调用过程拦截器名称, 多个名称用逗号分隔
  • Consumer端配置, 取消Consumer端的直接校验
dubbo:
  consumer: # Dubbo 消费端配置
    check: false
#    validation: true # 是否启用JSR303标准注解验证, 如果启用, 将对方法参数上的注解进行校验
    version: 1.0.0 # 默认版本

测试结果

随便抛个异常测试一下, 这里我们还用 <Dubbo的参数校验> 这篇文章的代码
test.png
欢迎关注, 转发, 收藏, 评论, 点赞~

posted @ 2020-03-22 11:22  AllenAlan  阅读(6795)  评论(0编辑  收藏  举报