Dubbo参数校验
|字数总计:2.5k|阅读时长:11分钟|阅读量:
一、前言
由于Dubbo默认的JSR303参数校验在未通过校验时,所返回的结果(ConstraintViolationException)并没有实现序列化,导致消费者调用服务时报错。且项目对于服务调用结果使用统一的Result对象,因此Dubbo默认的参数校验无法满足项目要求,故对Dubbo的参数校验进行改造。
二、解决方案
2.1 Maven依赖
在公共模块的pom文件下引入以下依赖
1 2 3 4 5 6 7 8 9 10 11 12
| <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.2.0.Final</version> </dependency>
|
2.2定义接口
2.2.1 定义接口参数
在公共模块中定义接口参数,并使用参数校验注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Data public class AddAuthForm implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull(message = "请输入用户ID") @Pattern(regexp = "^GHCD\\d{5}$", message = "用户ID不合法!") private String userId;
@NotEmpty(message = "权限不能为空!") @ValidAuthIdList(min = 1, max = 6, message = "权限不合法!") private List<Integer> authIds;
@NotNull(message = "请输入操作人员ID") @Pattern(regexp = "^GHCD\\d{5}$", message = "操作人员ID不合法!") private String operatorId;
}
|
2.2.2 定义接口
1
| void addAuth(AddAuthForm addAuthForm);
|
2.2.3 定义全局异常类
在公共模块中定义全局异常类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
public class GlobalException extends RuntimeException { private static final long serialVersionUID = 1L; private String msg; private int code = 500; public GlobalException(String msg) { super(msg); this.msg = msg; } public GlobalException(String msg, Throwable e) { super(msg, e); this.msg = msg; } public GlobalException(String msg, int code) { super(msg); this.msg = msg; this.code = code; } public GlobalException(String msg, int code, Throwable e) { super(msg, e); this.msg = msg; this.code = code; }
public String getMsg() { return msg; }
public void setMsg(String msg) { this.msg = msg; }
public int getCode() { return code; }
public void setCode(int code) { this.code = code; } }
|
2.2.4 定义异常处理器(重点)
在Consumer中定义异常出路Handler,这是重点!!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
@RestControllerAdvice public class GlobalExceptionHandler { private Logger logger = LoggerFactory.getLogger(getClass());
@ExceptionHandler(GlobalException.class) public R handleGlobalException(GlobalException e){ R r = new R(); r.put("code", e.getCode()); r.put("msg", e.getMessage()); logger.error(e.getMessage(), e); return r; } @ExceptionHandler(HttpMessageNotReadableException.class) public R handleHttpMessageNotReadableException(HttpMessageNotReadableException e){ logger.error(e.getMessage(), e); return R.error("输入数据不合法"); }
@ExceptionHandler(ConstraintViolationException.class) public R handleConstraintViolationException(ConstraintViolationException e){ logger.error(e.getMessage(), e); Set<ConstraintViolation<?>> violations = e.getConstraintViolations(); if (CollectionUtils.isNotEmpty(violations)) { ConstraintViolation<?> violation = violations.iterator().next(); return R.error(violation.getMessage()); } return R.error("输入数据不合法!"); }
@ExceptionHandler(Exception.class) public R handleException(Exception e){ logger.error(e.getMessage(), e); return R.error(); } }
|
2.3 Dubbo服务提供者端配置
Dubbo服务提供者端必须作这个validation=”true”的配置,具体示例配置如下
1 2 3
| dubbo: provider: validation: true
|
2.4 Dubbo服务消费者端配置
Dubbo服务消费者端一般也建议作这个validation=”true”的配置,具体示例配置如下
1 2 3
| dubbo: consumer: validation: true
|
2.5 消费者Controller
1 2 3 4 5 6 7 8
|
@RequestMapping("/save") public R save(@RequestBody AddAuthForm addAuthForm){ userAuthService.addAuth(addAuthForm); return R.ok(); }
|
http调用save接口时,传入AddAuthForm参数,即可校验。
2.6 测试
例如,为传入userId
这参数,即传入参数为:
1 2 3 4
| { "authIds": [1,3], "operatorId": "GHCD20241" }
|
返回结果为:
1 2 3 4
| { "msg": "请输入用户ID", "code": 500 }
|
符合预期结果。
三、服务端异常处理
从前面内容我们可以很看到,当Dubbo消费端开启参数校验时,参数如果不合法就会抛出相关异常信息,并且返回给前端统一的返回结果。
但是如果是Dubbo服务提供端报错,则会出现问题。1. Consumer端收到的是RuntimeException。2. 错误信息过去复杂,不符合要求。
3.1 针对问题1
是因为Dubbo本身异常处理的一些问题,具体处理方案可以参考这篇文章Dubbo异常处理
3.2 针对问题2
从异常堆栈内容我们可以看出这个异常信息返回是由ValidationFilter抛出的,从名字我们可以猜到这个是采用Dubbo的Filter扩展机制的一个内置实现,当我们对Dubbo服务接口启用参数校验时(即前文Dubbo服务配置中的validation=”true”),该Filter就会真正起作用,会把复杂的错误信息包装起来。我们看一下源码分析:
3.2.1 ValidationFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { if (validation != null && !invocation.getMethodName().startsWith("$") && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), VALIDATION_KEY))) { try { Validator validator = validation.getValidator(invoker.getUrl()); if (validator != null) { validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments()); } } catch (RpcException e) { throw e; } catch (ValidationException e) { return AsyncRpcResult.newDefaultAsyncResult(new ValidationException(e.getMessage()), invocation); } catch (Throwable t) { return AsyncRpcResult.newDefaultAsyncResult(t, invocation); } } return invoker.invoke(invocation); }
|
从前文的异常堆栈信息我们可以知道异常信息是由上述代码「注2」处所产生,这边是因为捕获了ValidationException,通过走读代码或者调试可以得知,该异常是由「注1」处valiator.validate方法所产生。
而Validator接口在Dubbo框架中实现只有JValidator,当然调试代码也可以很轻松定位到。
3.2.2 JValidator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @Override public void validate(String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Exception { List<Class<?>> groups = new ArrayList<>(); Class<?> methodClass = methodClass(methodName); if (methodClass != null) { groups.add(methodClass); } Set<ConstraintViolation<?>> violations = new HashSet<>(); Method method = clazz.getMethod(methodName, parameterTypes); Class<?>[] methodClasses; if (method.isAnnotationPresent(MethodValidated.class)){ methodClasses = method.getAnnotation(MethodValidated.class).value(); groups.addAll(Arrays.asList(methodClasses)); } groups.add(0, Default.class); groups.add(1, clazz); Class<?>[] classgroups = groups.toArray(new Class[groups.size()]); Object parameterBean = getMethodParameterBean(clazz, method, arguments); if (parameterBean != null) { violations.addAll(validator.validate(parameterBean, classgroups )); } for (Object arg : arguments) { validate(violations, arg, classgroups); } if (!violations.isEmpty()) { logger.error("Failed to validate service: " + clazz.getName() + ", method: " + methodName + ", cause: " + violations); throw new ConstraintViolationException("Failed to validate service: " + clazz.getName() + ", method: " + methodName + ", cause: " + violations, violations); } }
|
上述代码中可以看出当「注1」和注「2」两处代码进行参数校验时所得到的「违反约束」的信息都被加入到violations集合中,而在「注3」处检查到「违反约束」不为空时,就会抛出包含「违反约束」信息的ConstraintViolationException,该异常继承自ValidationException,这样也就会被ValidationFilter中方法所捕获,进而向调用方返回相关异常信息。
3.2.3 自定义参数校验异常返回
在provider下新建CustomValidationFilter实现Filter接口,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Activate(group = {CONSUMER, PROVIDER}, value = "customValidationFilter", order = 10000) public class CustomValidationFilter implements Filter {
private Validation validation;
public void setValidation(Validation validation) { this.validation = validation; }
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { if (validation != null && !invocation.getMethodName().startsWith("$") && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), VALIDATION_KEY))) { 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<?>> violations = e.getConstraintViolations(); if (CollectionUtils.isNotEmpty(violations)) { ConstraintViolation<?> violation = violations.iterator().next(); return AsyncRpcResult.newDefaultAsyncResult(new GlobalException(violation.getMessage()), invocation); } return AsyncRpcResult.newDefaultAsyncResult(new ValidationException("参数不合法!"), invocation); } catch (Throwable t) { return AsyncRpcResult.newDefaultAsyncResult(t, invocation); } } return invoker.invoke(invocation); } }
|
定义filter与内置的ValidationFilter唯一不同的地方就在于「注1」处所新增的针对特定异常ConstraintViolationException的处理,从异常对象中获取包含的「违反约束」信息,并取其中第一个参数校验错误信息,返回自定义异常类。配合问题1的解决方案就可以实现Consumer端接收到指定异常。
3.2.4 自定义Filter的配置
在resources下新建二级目录:META-INF/dubbo
再新建一个文件名org.apache.dubbo.rpc.Filter
文件中配置内容为:customValidationFilter=com.xxx.demo.dubbo.filter.CustomValidationFilter
配置provider的application.yml文件
1 2 3 4
| dubbo: provider: validation: true filter: -exception,-validation,dubboExceptionFilter,customValidationFilter
|
四、如何扩展校验注解
在实际开发中有时候会遇到默认内置的注解无法满足校验需求,这时就需要自定义一些校验注解去满足需求,方便开发。例如,我需要给一个List\类型的属性设置最大值和最小值。
4.1 定义校验注解
1 2 3 4 5 6 7 8 9 10 11
| @Constraint(validatedBy = ListIntegerValidator.class) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) public @interface ValidAuthIdList{
String message() default "权限未找到!"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int min() default 1; int max() default 6; }
|
4.2 创建校验器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class ListIntegerValidator implements ConstraintValidator<ValidAuthIdList, List<Integer>> { private int min; private int max;
@Override public void initialize(ValidAuthIdList constraintAnnotation) { this.min = constraintAnnotation.min(); this.max = constraintAnnotation.max(); }
@Override public boolean isValid(List<Integer> value, ConstraintValidatorContext context) {
for (int num : value) { if (num < min || num > max) { return false; } } return true; } }
|
4.3 用法
参考2.2.4小节中创建异常处理器,处理ConstraintViolationException
异常。
在要校验的字段上加@ValidAuthIdList(min = 1, max = 6, message = "权限不合法!")
,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Data public class AddAuthForm implements Serializable {
private static final long serialVersionUID = 1L;
@NotEmpty(message = "权限不能为空!") @ValidAuthIdList(min = 1, max = 6, message = "权限不合法!") private List<Integer> authIds;
}
|
此时,如果我们传入的参数有大于6或小于1的值,则会抛出ConstraintViolationException异常。
五、总结
本文主要介绍了使用Dubbo框架时如何使用优雅点方式完成参数的校验,首先演示了如何利用Dubbo框架默认支持的校验实现参数校验,并且返回统一的数据格式;然后针对服务端异常处理的两个问题给出来相应的解决方案;最后介绍了下如何进行自定义校验注解的实现,方便进行后续自行扩展实现,希望能在实际工作中有一定的帮助。