Dubbo 异常处理的正确姿势

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

*

* 功能:

*

    *

  1. 不期望的异常打ERROR日志(Provider端)

    * 不期望的日志即是,没有的接口上声明的Unchecked异常。

    *

  2. 异常不在API包中,则Wrap一层RuntimeException。

    * RPC对于第一层异常会直接序列化传输(Cause异常会String化),避免异常在Client出不能反序列化问题。

    *

*

*/

@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 机制, 如果不太明白的话可以品一品 这篇文章

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 # 默认版本

测试结果

随便抛个异常测试一下, 这里我们还用 这篇文章的代码

欢迎关注, 转发, 收藏, 评论, 点赞~

相关推荐

天天酷跑怎么领工资 天天酷跑登陆送钻石怎么玩
365bet取款要多久

天天酷跑怎么领工资 天天酷跑登陆送钻石怎么玩

📅 08-10 👁️ 8847
北京赛迪信息工程监理有限公司
365bet取款要多久

北京赛迪信息工程监理有限公司

📅 01-08 👁️ 2002
策略与战争融合:《手游帝国霸权》玩法全解析,帝国霸权怎么玩
好消息!魔兽世界商标已解冻,电竞世界杯等待国服回归
雅思备考神器大揭秘!10 款 App 深度评测
365体育app网址

雅思备考神器大揭秘!10 款 App 深度评测

📅 12-11 👁️ 3603
地笼为什么容易进蛇
365bet取款要多久

地笼为什么容易进蛇

📅 10-14 👁️ 611
地推是什么?地推真的能赚钱吗?去哪里对接地推项目?
【Tissot天梭手表型号T035.627.16.051.00经典价格查询】官网报价
重庆人最爱的10家火锅店!麻辣鲜香,越吃越嗨!