dubbo 基于Filter的自定义参数校验
在web应用中,我们经常使用注解的方式来校验参数,使得业务开发不用过分关注参数校验的逻辑
但是 在现有的微服务架构中,常常只是作为一个服务提供rpc 服务的方式,那是不是还是退步回繁琐的参数校验 不能统一处理了
转载请注明出处 https://www.cnblogs.com/majianming/p/16938621.html
背景
项目使用了dubbo 作为rpc治理的框架,所以仅对于这种方式的调用进行说明
本文基于Dubbo 2.6, 在2.7之后,dubbo成为Apache顶级项目,包名有所变化
需求
- 能提供全局的参数校验
- 能兼容现有的没有办法实现参数校验的接口(有一些旧接口 使用了实体直接返回 没有办法返回错误信息)
解决思路
dubbo 提供了Dubbo SPI机制来拓展拦截器 从拦截器可以获得调用方法以及参数信息
虽然dubbo 也同时提供了验证拓展 ,但是对于验证的结果,是直接抛出rpc 错误的形式,如果要自定义返回的话 还是使用拦截器处理相应的异常,还不如直接用spi来实现一个自定义的拦截器方便
实现
首先可以了解一下Dubbo 的SPI ,与Java 的SPI 相比拓展了一些功能,但实际上还是作为插件机制提供,这里我们就不详细讨论
声明SPI
-
需要在上建立一个拦截器实现,我们这里新建一个类ParamValidationFilter 实现
com.alibaba.dubbo.rpc.Filter
接口 -
然后需要定义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;
...
}
这样如果参数有问题 就会返回我们定义的消息
拓展
本文仅支持单字段校验,可以考虑支持多字段的组合校验