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
歡迎關注, 轉發, 收藏, 評論, 點贊~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM