写在前面

dubbo给我们提供了很多的扩展点,异常过滤就是其中的一个,比如参数验证的ContraintViolationException异常需要我们在service方法上手动的throw,才能抛出去,不然会自动的封装为RuntimeException,我们来扩展异常过滤,抛出自定义的异常信息,从而以更加友好的方式来暴漏出现的问题。
扩展的原理是利用SPI,SPI符合开闭原则,即,对修改关闭,对扩展开放,dubbo在jdk spi 的基础上进行了升级改造,定义了dubbo SPI。


1:rpc-service-api

1.1:自定义异常类

当发生异常时我们抛出自定义的异常来暴漏错误信息给用户。

public final class ServiceException extends RuntimeException {

    /**
     * 错误码
     */
    private Integer code;

    public ServiceException() { // 创建默认构造方法,用于反序列化的场景。
    }

    public ServiceException(ServiceExceptionEnum serviceExceptionEnum) {
        // 使用父类的 message 字段
        super(serviceExceptionEnum.getMessage());
        // 设置错误码
        this.code = serviceExceptionEnum.getCode();
    }

    public ServiceException(ServiceExceptionEnum serviceExceptionEnum, String message) {
        // 使用父类的 message 字段
        super(message);
        // 设置错误码
        this.code = serviceExceptionEnum.getCode();
    }

    public Integer getCode() {
        return code;
    }

}

1.2:自定义异常错误码

定义用于返回给用户的错误编码和错误信息。

public enum ServiceExceptionEnum {

    // ========== 系统级别 ==========
    SUCCESS(0, "成功"),
    SYS_ERROR(2001001000, "服务端发生异常"),
    MISSING_REQUEST_PARAM_ERROR(2001001001, "参数缺失"),
    INVALID_REQUEST_PARAM_ERROR(2001001002, "参数信息不合法"),

    // ========== 用户模块 ==========
    USER_NOT_FOUND(1001002000, "用户不存在"),
    USER_EXISTS(1001002001, "用户已经存在"),

    // ========== 订单模块 ==========

    // ========== 商品模块 ==========

    // ========== 其他模块 ==========
    ;
    /**
     * 错误码
     */
    private int code;
    /**
     * 错误提示
     */
    private String message;

    ServiceExceptionEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        return "ServiceExceptionEnum{" +
                "code=" + code +
                ", message='" + message + '\'' +
                '}';
    }
}

2:rpc-service-provider

2.1:自定义异常过滤器类

继承org.apache.dubbo.rpc.ListenableFilter:

@Activate(group = CommonConstants.PROVIDER)
public class DubboExceptionFilter extends ListenableFilter {

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

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

    static class ExceptionListenerX extends ExceptionListener {

        @Override
        public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
            // 发生异常,并且非泛化调用
            if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
                Throwable exception = appResponse.getException();
                // <1> 如果是 ServiceException 异常,直接返回
                if (exception instanceof ServiceException) {
                    return;
                }
                // <2> 如果是参数校验的 ConstraintViolationException 异常,则封装返回
                if (exception instanceof ConstraintViolationException) {
                    appResponse.setException(this.handleConstraintViolationException((ConstraintViolationException) exception));
                    return;
                }
            }
            // <3> 其它情况,继续使用父类处理
            super.onResponse(appResponse, invoker, invocation);
        }

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

    }

    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();

                    // 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.
                    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);

                    // 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) {
                    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);
        }

        // For test purpose
        public void setLogger(Logger logger) {
            this.logger = logger;
        }
    }

}

@Activate(group = CommonConstants.PROVIDER)是设置在服务提供者端显示。

2.2:dubbo SPI

在resources目录下创建META-INF/dubbo文件夹,并创建org.apache.dubbo.rpc.Filter文件,并添加如下内容:

dubboExceptionFilter=cn.iocoder.springboot.lab30.rpc.filter.DubboExceptionFilter

2.3:UserRpcServiceImpl

修改代码模拟添加一个已经存在的用户:

@Override
public Integer add(UserAddDTO addDTO) {
    // 【额外添加】这里,模拟用户已经存在的情况
    if ("dongshidaddy".equals(addDTO.getName())) {
        throw new ServiceException(ServiceExceptionEnum.USER_EXISTS);
    }
    return (int) (System.currentTimeMillis() / 1000); // 嘿嘿,随便返回一个 id
}

2.4:修改配置文件

修改application.yml,增加filter="-exception"去除本身提供的ExceptionFiler过滤器。

<dubbo:service ref="userRpcServiceImpl"
               interface="dongshi.daddy.api.UserRpcService"
               version="${dubbo.provider.UserRpcService.version}"
               validation="true"
               filter="-exception"/>

3:rpc-service-consumer

增加会触发名称重复错误的spring bean:

@Component
public class UserRpcServiceTest03 implements CommandLineRunner {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Resource
    private UserRpcService userRpcService;

    @Override
    public void run(String... args) {
        // 添加用户
        try {
            // 创建 UserAddDTO
            UserAddDTO addDTO = new UserAddDTO();
            addDTO.setName("dongshidaddy"); // 设置为 dongshidaddy ,触发 ServiceException 异常
            addDTO.setGender(1);
            // 发起调用
            userRpcService.add(addDTO);
            logger.info("[run][发起一次 Dubbo RPC 请求,添加用户为({})]", addDTO);
        } catch (Exception e) {
            logger.error("[run][添加用户发生异常({}),信息为:[{}]", e.getClass().getSimpleName(), e.getMessage());
        }
    }

}

此时启动在消费者端会看到如下异常,则说明成功了:

2021-11-15 13:23:17.162 ERROR 18604 --- [           main] d.daddy.consumer.UserRpcServiceTest03    : [run][添加用户发生异常(ServiceException),信息为:[用户已经存在]