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的處理方式主要是:
- 如果provider實現了GenericService接口,直接拋出
- 如果是checked異常,直接拋出
- 在方法簽名上有聲明,直接拋出
- 異常類和接口類在同一jar包里,直接拋出
- 是JDK自帶的異常,直接拋出
- 是Dubbo本身的異常,直接拋出
- 否則,包裝成RuntimeException拋給客戶端
如何正確捕獲業務異常
有多種方法可以解決這個問題,每種都有優缺點,這里不做詳細分析,僅列出供參考:
- 將該異常的包名以"java.或者"javax. " 開頭
- 使用受檢異常(繼承Exception)
- 不用異常,使用錯誤碼
- 把異常放到provider-api的jar包中
- 判斷異常message是否以XxxException.class.getName()開頭(其中XxxException是自定義的業務異常)
- provider實現GenericService接口
- provider的api明確寫明throws XxxException,發布provider(其中XxxException是自定義的業務異常)
- 實現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);
}
}
}
-
改動:
-
-
將 替換為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的參數校驗> 這篇文章的代碼
歡迎關注, 轉發, 收藏, 評論, 點贊~