Apache Dubbo Triple 3.3协议tri-rest方式的统一异常处理与参数传递

Triple 3.3的RPC协议提供了开箱即用的REST支持,无需网关转换即可对外暴露REST服务,同时还支持SpringMvc等多种注解,极大的拓展了Dubbo RPC的适用范围、降低了升级适配和移植难度,特别是对于Service代码一大坨、Controller代码就一行的应用场景(当然这种代码风格是否合适暂且不论),能够节省一个Controller文件,只要在Dubbo API中使用官方支持的Rest注解暴露服务即可。

不过,原有基于SpringMvc的全局异常处理逻辑就需要调整,官方文档说对于使用SpringMvc注解暴露的服务,支持使用ExceptionHandler注解处理异常,但异常捕获的类型受限,且无法处理RPC调用的异常,因此经过实践发现了对于tri-rest方式的全局异常处理方式。既能支持Rest调用的自定义统一异常返回(如用GET方式调用POST接口、服务没有注册等),也支持通过RPC调用的自定义统一异常处理返回。

有关Dubbo的异常抛出逻辑这里就不再详细介绍了,网上关于org.apache.dubbo.rpc.filter.ExceptionFilter的介绍很多,在最新的3.3.2中的处理逻辑也没有变化,官方文档对此也说的很清楚。

有关服务间的参数传递(如网关集中认证的权限信息后向传递等典型应用场景),由于tri-rest方式既支持rest访问,又支持tri-rpc调用,因此需要统一对参数的传入方式做适配处理。

1. tri-rest中rest部分的全局异常处理

需要扩展点:

org.apache.dubbo.remoting.http12.ExceptionHandler

实践代码如下:

@Activate(order = -2000)
@Slf4j
public class GlobalExceptionHandler implements ExceptionHandler<Exception, Response<?>> {
    @Override
    public Level resolveLogLevel(Exception exception) {
        return Level.ERROR;
    }

    @Override
    public Response<?> handle(Exception exception, RequestMetadata metadata, MethodDescriptor descriptor) {
        log.error("===> GlobalExceptionHandler: call {} at {} from {}: {}", metadata.method(), metadata.path(), descriptor, exception);
        return switch (exception) {
            case BizException bizException -> Response.fail(bizException); //处理业务异常,需自定义异常类
            case RestException _ -> Response.fail(CommonResponseEnum.PARAM_NOT_VALID);
            case IllegalArgumentException _ ->
                    Response.fail(CommonResponseEnum.PARAM_NOT_VALID.getErrorCode(), exception.getMessage()); //处理未传入任何参数的异常,注意不是参数校验异常
            case HttpStatusException httpStatusException ->
                    Response.fail(String.valueOf(httpStatusException.getStatusCode()), httpStatusException.getMessage()); //处理未找到invoker、用GET请求POST接口等REST方法异常
            default -> Response.fail(CommonResponseEnum.SYSTEM_ERROR.getErrorCode(), exception.getMessage()); //其他统一异常
        };
    }
}

SPI的扩展方式:通过新建resources/META-INF/dubbo/org.apache.dubbo.remoting.http12.ExceptionHandler文件,然后写入一行文字:

customGlobalExceptionHandler=XX.XX.XX.GlobalExceptionHandler

其中customGlobalExceptionHandler是自定义名称,只要不和Dubbo官方包内使用的一样即可。(官方的SPI定义统一放在dubbo的jar包中  jar:/META-INF/dubbo.internal中,打开对应扩展点文件即可看到,下同)

2.tri-rest中triple部分的全局异常处理

需要扩展点:

org.apache.dubbo.rpc.Filter

实践代码如下:

@Activate(group = CommonConstants.PROVIDER)
@Slf4j
public class GlobalExceptionFilter extends ExceptionFilter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

        log.info("===> GlobalExceptionFilter: call {} at {} with {}", invoker.getInterface().getName(), invoker.getUrl(), invocation.getArguments());

        Result appResponse = invoker.invoke(invocation);
        if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
            try {
                Throwable exception = appResponse.getException();
                String classname = exception.getClass().getName();
                log.error("===> GlobalExceptionFilter: catch exception {}", classname, exception);
                if (exception instanceof BizException) { //处理自定义异常,需定义异常类
                    return AsyncRpcResult.newDefaultAsyncResult(Response.fail((BizException) exception), invocation);
                }
            } catch (Throwable e) {
                return AsyncRpcResult.newDefaultAsyncResult(Response.fail(CommonResponseEnum.SYSTEM_ERROR), invocation); //包装其他默认异常
            }
        }
        return appResponse;
    }
}

SPI的扩展方式:通过新建resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter文件,然后写入一行文字:

globalExceptionFilter=XX.XX.XX.GlobalExceptionFilter

文字说明同上。

3.tri-rest中参数校验的全局异常处理

需要扩展点:

org.apache.dubbo.rpc.Filter

tri-rest本身是支持JSR303校验的,和SpringMvc等使用方法一样,只需要使用支持的注解即可,详细文档参见官方,这里只说明如何全局处理校验异常。

实践代码如下:

@Setter
@Activate(
        group = {CONSUMER, PROVIDER},
        value = VALIDATION_KEY,
        order = 10000)
@Slf4j
public class CustomValidationFilter implements Filter {

    private Validation validation;

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        if (needValidate(invoker.getUrl(), invocation.getMethodName())) {
            log.info("====> ValidationFilter {} invoked {}", invoker.getUrl(), invocation.getMethodName());
            try {
                Validator validator = validation.getValidator(invoker.getUrl());
                if (validator != null) {
                    validator.validate(
                            invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
                }
            } catch (RpcException e) {
                throw e;
            } catch (ConstraintViolationException e) {
                Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
                StringBuilder sb = new StringBuilder();

                // 自定义参数校验不通过的异常提示,可自定义提示信息如何返回
                constraintViolations.forEach(violation ->
                        sb.append(violation.getPropertyPath().toString())
                                .append(" ")
                                .append(violation.getMessage())
                                .append(", 当前值: '")
                                .append(violation.getInvalidValue())
                                .append("'; ")

                );
                return AsyncRpcResult.newDefaultAsyncResult(Response.fail("10000", sb.toString()), invocation);
            } catch (Throwable t) {
                return AsyncRpcResult.newDefaultAsyncResult(t, invocation);
            }
        }
        return invoker.invoke(invocation);
    }

    private boolean needValidate(URL url, String methodName) {
        return validation != null
                && !methodName.startsWith("$")
                && ConfigUtils.isNotEmpty(url.getMethodParameter(methodName, VALIDATION_KEY))
                && !"false".equalsIgnoreCase(url.getParameter(VALIDATION_KEY));
    }
}

SPI的扩展方式:通过新建resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter文件,然后写入一行文字:

globalValidationFilter=XX.XX.XX.CustomValidationFilter

文字说明同上。

4.tri-rest中服务间参数的统一处理

需要扩展点:

org.apache.dubbo.rpc.Filter

实践代码如下:

@Activate(group = {CONSUMER, PROVIDER})
@Slf4j
public class TokenContextFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        log.info("====> TokenContextFilter ");
        // 获取 RpcServiceContext
        RpcServiceContext context = RpcServiceContext.getServiceContext();

        // 从 RpcContext 中获取附加信息(适用于 Dubbo 客户端)
        if (context.isConsumerSide()) {
            Long token = LoginUserContextHolder.getToken();
            if (token != null)
                RpcContext.getClientAttachment().setAttachment(GlobalConstants.TOKEN_ID, userId);
            return invoker.invoke(invocation);
        } else {
            String userTokenFromRpcContext = RpcContext.getServerContext().getAttachment(GlobalConstants.TOKEN_ID);

            // 从 HttpServletRequest 中获取 HTTP 头(适用于 HTTP 客户端)
            String userTokenFromHttpHeader = null;
            Object request = RpcContext.getServerContext().getRequest();
            if (request instanceof DefaultHttpRequest httpRequest) {
                userTokenFromHttpHeader = httpRequest.header(GlobalConstants.TOKEN_ID);
            }

            // 优先使用 HTTP 头中的 Token
            String token = userTokenFromHttpHeader != null ? userTokenFromHttpHeader : userTokenFromRpcContext;

            
            // 将 Token 保存到 ThreadLocal,如自定义类LoginUserContextHolder
            if (userId != null) {
                LoginUserContextHolder.setUserId(userId);
            }

            try {
                return invoker.invoke(invocation);
            } finally {
                // 清理 ThreadLocal
                LoginUserContextHolder.remove();
            }
        }
    }
}

SPI的扩展方式:通过新建resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter文件,然后写入一行文字:

globalTokenFilter=XX.XX.XX.TokenContextFilter

文字说明同上。


在最佳实践中,可以将上面的几种方式编写成一个或几个module,需要时直接导入依赖即可方便实现开箱使用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值