Dubbo RPC调用参数校验---错误message自动返回
Dubbo 的RPC调用中Consumer 和 Provider端都可以对调用的方法做传参验证,参数的验证可以通过JSR303规范 (Java Specification Requests) 提到的 Bean Validation 方式来验证,Dubbo官方也是这么推荐的。最佳实践中分包部分提到传参的数据模型定义在API的jar包中,如果你是这样做的,那么参数的验证完全可以在Consumer端完成,这样一来就可以减少网络开销并提早得到失败结果。
下面的介绍基于 Dubbo2.6.2 + Springboot2.1.3 的环境,内容会在Dubbo官方文档的基础上做一些扩展。
Hibernate Validation 是Bean Validation 的一个实现,也是Dubbo官方推荐的实现方式,使用Hibernate Validation需要引入如下依赖:
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.16.Final</version> </dependency> <!-- 若启动报错 java.lang.ExceptionInInitializerError 则还需引入如下 el-api 依赖 --> <dependency> <groupId>javax.el</groupId> <artifactId>javax.el-api</artifactId> <version>3.0.0</version> </dependency>
在传参DTO中加上 "@NotNull" 注解
@Data
public class AddressDto implements Serializable {
@NotNull(message = "地址不能为空")
private String address;
private String city;
private Boolean batch;
}
传参数据模型中添加需要验证的注解后, consumer端在RPC调用前会经过一个叫做 ValidationFilter 的过滤器,该过滤期获取到validator后会调用Dubbo提供的 JValidator 如下validate方法
@Override public void validate(String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Exception { List<Class<?>> groups = new ArrayList<Class<?>>(); String methodClassName = clazz.getName() + "$" + toUpperMethoName(methodName); Class<?> methodClass = null; try { methodClass = Class.forName(methodClassName, false, Thread.currentThread().getContextClassLoader()); groups.add(methodClass); } catch (ClassNotFoundException e) { } Set<ConstraintViolation<?>> violations = new HashSet<ConstraintViolation<?>>(); Method method = clazz.getMethod(methodName, parameterTypes); Class<?>[] methodClasses = null; if (method.isAnnotationPresent(MethodValidated.class)){ methodClasses = method.getAnnotation(MethodValidated.class).value(); groups.addAll(Arrays.asList(methodClasses)); } // add into default group groups.add(0, Default.class); groups.add(1, clazz); // convert list to array Class<?>[] classgroups = groups.toArray(new Class[0]); 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); } }
然后在for循环的中调用了另一 validate的私有方法,该方法对传参DTO的类型做了判断后 使用了Hibernate Validation 提供的 ValidatorImpl.validate 方法来完成的参数验证,验证完之后返回 ConstraintVoilation 的集合,在以上方法最后对集合violations做判断是否非空,非空则代表验证失败, 并在后续抛出 ConstraintViolationException 异常,这个异常在经过你controller层之后,是可以被我们定义的 ControllerAdvice 捕获到。全局异常处理方式参考,这样只需要增加一个 ExceptionHandler 处理ConstraintViolationException 就能将 Validation 中用到的注解的message返回调用端,
@ResponseBody @ExceptionHandler(value = ValidationException.class) public Result handleAddressException(ValidationException e){ log.error(e.getMessage()); String validateMsg = null; // 若判断是 ConstraintViolationException异常 则取出message信息 if(e instanceof ConstraintViolationException){ ConstraintViolationException cve = (ConstraintViolationException)e; Set<ConstraintViolation<?>> constraintViolations = cve.getConstraintViolations(); for(ConstraintViolation cv : constraintViolations){ validateMsg = cv.getMessage(); break; } return ResultUtil.error(ResultEnum.VALIDATION_FAILURE.getCode(), validateMsg); } return ResultUtil.error(ResultEnum.VALIDATION_FAILURE.getCode(),e.getMessage()); }
最终在调用端获取到JSON结果如下
{ "requestId": null, "success": false, "business": null, "code": "20001", "message": "地址不能为空", "date": null, "version": null, "obj": null }
参考:
[1] Dubbo官方文档
思考沉淀心智,写作使人准确