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的处理方式主要是:
- 如果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的参数校验> 这篇文章的代码
欢迎关注, 转发, 收藏, 评论, 点赞~