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官方文档
  
原文地址:https://www.cnblogs.com/mingorun/p/11013095.html