dubbo 基于Filter的自定义参数校验

25

在web应用中,我们经常使用注解的方式来校验参数,使得业务开发不用过分关注参数校验的逻辑 但是 在现有的微服务架构中,常常只是作为一个服务提供rpc 服务的方式,那是不是还是退步回繁琐的参数校验 不能统一处理了

转载请注明出处 https://www.cnblogs.com/majianming/p/16938621.html

背景

项目使用了dubbo 作为rpc治理的框架,所以仅对于这种方式的调用进行说明

本文基于Dubbo 2.6, 在2.7之后,dubbo成为Apache顶级项目,包名有所变化

需求

  1. 能提供全局的参数校验
  2. 能兼容现有的没有办法实现参数校验的接口(有一些旧接口 使用了实体直接返回 没有办法返回错误信息)

解决思路

dubbo 提供了Dubbo SPI机制来拓展拦截器 从拦截器可以获得调用方法以及参数信息

虽然dubbo 也同时提供了验证拓展 ,但是对于验证的结果,是直接抛出rpc 错误的形式,如果要自定义返回的话 还是使用拦截器处理相应的异常,还不如直接用spi来实现一个自定义的拦截器方便

实现

首先可以了解一下Dubbo 的SPI ,与Java 的SPI 相比拓展了一些功能,但实际上还是作为插件机制提供,这里我们就不详细讨论

声明SPI

  1. 需要在上建立一个拦截器实现,我们这里新建一个类ParamValidationFilter 实现com.alibaba.dubbo.rpc.Filter 接口

  2. 然后需要定义SPI的服务文件 路径为${classPath}/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter

    对于我们常见的spring boot 项目 等于resources/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter 内容为(这里我们后面再讲内容的意义)

    param_validation_filter=xyz.ewis.app.filter.ParamValidationFilter
    

最终的效果是

src
 |-main
    |-java
        |-xyz
            |-ewis
                |-app
                    |-app
                        |-ParamValidationFilter.java
    |-resources
        |-META-INF
            |-dubbo
                |-com.alibaba.dubbo.rpc.Filter (纯文本文件,内容为:param_validation_filter=xyz.ewis.app.filter.ParamValidationFilter)

实现拦截器

@Slf4j
public class ParamValidationFilter implements Filter {
    private static final Validator validator;

    static {
        try {
            validator = Validation.buildDefaultValidatorFactory().getValidator();
        } catch (Exception e) {
            log.error("can not init validator", e);
            throw e;
        }
    }

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

        try {
            Method declaredMethod = invoker.getInterface()
                    .getDeclaredMethod(invocation.getMethodName(), invocation.getParameterTypes());
            Class<?> returnType = declaredMethod.getReturnType();
            // 因为我们要将错误信息返回 在这里校验了特定返回值情况 如果不是指定的返回值 就不校验了
            if (!ResultDTO.class.equals(returnType)) {
                return invoker.invoke(invocation);
            }
            Annotation[][] parameterAnnotations = declaredMethod.getParameterAnnotations();
            for (int i = 0; i < parameterAnnotations.length; i++) {
                Annotation[] parameterAnnotation = parameterAnnotations[i];
                // 遍历所有的参数 如果有javax.validation的注解才校验
                if (Arrays.stream(parameterAnnotation).noneMatch(pa -> Valid.class.equals(pa.annotationType()))) {
                    continue;
                }
                Object argument = invocation.getArguments()[i];
                // 参数是null 的情况 并且有javax.validation.constraints.NotNull 那么说明参数错误
            
                if (Objects.isNull(argument)) {
                    if (Arrays.stream(parameterAnnotation).anyMatch(pa -> NotNull.class.equals(pa.annotationType()))) {
                        return
                                new RpcResult(
                                        ResultDTO.fail(StateCode.ILLEGAL_ARGS, String.format("第%s 个参数不能为空", i + 1))
                                );
                    }
                    continue;
                }
                // 校验参数
                Set<ConstraintViolation<Object>> validate = validator.validate(argument);
                if (!validate.isEmpty()) {
                    // 如果有多个参数错误 也仅返回第一个参数错误信息 也可以改成多个都返回
                    ConstraintViolation<Object> objectConstraintViolation = CollectionUtils.get(validate, 0);
                    String message = objectConstraintViolation.getMessage();
                    return new RpcResult(ResultDTO.fail(StateCode.ILLEGAL_ARGS, message));
                }
            }
        } catch (
                NoSuchMethodException ignored) {
                    
        }
        // 没有检查到需要校验 或者 没有校验出错误 则调用业务逻辑
        return invoker.invoke(invocation);
    }
}

使用拦截器

参考官方的文件调用拦截扩展可以知道 可以配置为

<!-- 消费方调用过程拦截 -->
<dubbo:reference filter="param_validation_filter" />
<!-- 消费方调用过程缺省拦截器,将拦截所有reference -->
<dubbo:consumer filter="param_validation_filter"/>
<!-- 提供方调用过程拦截 -->
<dubbo:service filter="param_validation_filter" />
<!-- 提供方调用过程缺省拦截器,将拦截所有service -->
<dubbo:provider filter="param_validation_filter"/>

这里的param_validation_filter 实际上就是我们上面提到的META-INF/dubbo/com.alibaba.dubbo.rpc.Filter 中定义param_validation_filter=xyz.ewis.app.filter.ParamValidationFilter,说明调用过程要经过我们定义的xyz.ewis.app.filter.ParamValidationFilter拦截器

考虑到我们是使用api二方包在项目间使用其他项目的服务, 首先推动消费方添加filter 参数比较麻烦,重要的是在提供方修改api校验定义后,需要消费方更新提供api包,这对于众多的微服务来说,是无法接受的 所以我们还是使用了提供方提供拦截这种方式,同时我们目前逐步改成参数校验的模式,在切换过程中,先对指定服务生效,所以我们这里仅使用 提供方调用过程拦截 即

<!-- 提供方调用过程拦截 -->
<dubbo:service filter="param_validation_filter" />

使用

到目前为止我们已经完成了自定义拦截器的开发,接下来是使用

例如我们原来接口以及参数定义为

ResultDTO<UserInfoDTO> findUserByParam(UserParam param);

class UserParam implements Serializable {
    //用户名
    String username;
    //用户id
    Integer userId;
}

这我们需要添加@Valid 注解 并且因为参数不能为空 所以还要加上@NotNull注解 同时对于需要校验的字段 加上合适的注解

ResultDTO<UserInfoDTO> registerUserByParam(@Valid @NotNull UserRegParam param);

class UserRegParam implements Serializable {
    //用户名
    @NotBlank(message = "用户名不能为空")
    String username;
    //性别
    Integer sexType;
    ...
}  

这样如果参数有问题 就会返回我们定义的消息

拓展

本文仅支持单字段校验,可以考虑支持多字段的组合校验


转载请注明出处 https://www.cnblogs.com/majianming/p/16938621.html