java中的参数校验2:自定义返回结果

java,参数校验,validation

Posted by Chris on January 7, 2020

参数校验的几个层级:

  • 前端校验
    前端校验一般是为了更快速的响应用户输入,通知用户及时纠正错误输入。
  • Controller层校验
    Controller层直接接受前端传递过来的参数。在这一层建议做参数的“基本校验”。这里的基本校验是指:字段非空判断、数字的最大值和最小值等,这些校验一般是JSR-303注解可以支持的。这里并不进行业务逻辑的校验,理由是Controller层并不做业务逻辑处理,因此不做业务逻辑校验。
  • Service层校验
    Service层实现业务逻辑,在这一层需要进行业务逻辑的参数校验。此外,当Service层的接口通过网关暴露时,则需要进行基本校验和业务逻辑参数校验。
  • DAO层校验
    DAO层只能有本地的Service层方法来调用。一般在Service层做参数校验,这一层就不用做校验了。

前面讲过了在Controller层和Service层对于JavaBean和基本参数类型的校验方式。当参数校验不通过时,则默认会抛异常,这是一种处理办法。但是,另一种处理方式是,业务RD会捕获异常,然后返回一个CommonResponse对象。

1 自定义Controller层校验结果

Spring对于Controller层的参数校验支持很完善,可以支持自定义返回结果。

使用步骤如下。

  1. 引入jar包
    引入spring-boot-starter-web,会间接引入validation-api和hibernate-validator等jar包。

     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
         <version>2.2.2.RELEASE</version>
     </dependency>
    
  2. 定义JavaBean和CommonResponse
    JavaBean的定义如下。使用了@Data这个lombok注解来减少开发量。
     @Data
     public class Customer {
    
         private int customerId;
         @Min(1)
         private int age;
         @Min(1)
         @Max(2)
         private int gender;
         @NotEmpty
         private String country;
         @NotEmpty
         private List<@NotBlank(message = "productNames can not be null") String> productNames;
     }
    

    一般后端返回给前端一个CommonResponse,定义如下。code为状态码,msg为提示信息,result为真正的VO数据。

     @Data
     @AllArgsConstructor
     @NoArgsConstructor
     public class CommonResponse <T> {
         private int code;
         private String msg;
         private T result;
     }
    
  3. 定义Controller类
     @RestController
     @Validated
     public class CustomerController {
    
         @RequestMapping("/customer/saveCustomer")
         public void saveCustomer(@Valid Customer customer) {
             System.out.println(customer);
         }
    
         @RequestMapping("/customer/getCustomer")
         public Customer getCustomer( @RequestParam("customerId") @Min(1) Integer customerId) {
             System.out.println(customerId);
             return new Customer();
         }
     }
    

    前面的这3步,与上一篇博客的内容是相同的。

  4. 自定义全局异常处理类
    @ControllerAdvice注解放在类上面,定义了一个Controller的切面类。@ExceptionHandler注解放在方法上,定义了一个异常处理器。它可以带一个属性,用来表明该异常处理器只能对哪些类型的异常进行处理。若为空,则对所有的异常类型进行处理。@ResponseBody表明了返回结果是一个响应实体。
     @ControllerAdvice
     public class MyExceptionHandler {
    
         @ExceptionHandler(ConstraintViolationException.class)
         @ResponseBody
         public CommonResponse handleException(Exception ex) {
             System.out.println(ex);
             return new CommonResponse(400, "参数错误", null);
         }
    
         @ExceptionHandler(BindException.class)
         @ResponseBody
         public CommonResponse handleParamException(Exception ex) {
             System.out.println(ex);
             return new CommonResponse(400, "参数错误", null);
         }
     }
    

    这里,我们统一将ConstraintViolationException和BindException都视为参数错误,返回“参数错误”。前端在很多情况下是不关心具体的错误原因的,这里我们只是记录下来,返回给前端的提示文案就是“参数错误”。

  5. 测试
    • 测试BindException异常的处理器
        POST localhost:8080/customer/saveCustomer
      

      结果如下

        {
            "code": 400,
            "msg": "param error",
            "result": null
        }
      
    • 测试ConstraintViolationException异常的处理器
        GET localhost:8080/customer/getCustomer?customerId=0
      

      结果如下

        {
            "code": 400,
            "msg": "param error",
            "result": null
        }
      

2 自定义Service层校验结果

首先看看@Validated的原理是什么。Service层的使用方式如下所示。

@Service
@Validated
public class CustomerService {

    public void saveCustomer(@Valid Customer customer) {
    }

    public Customer loadCustomer(@Min(1) Integer id, @Valid Customer customer) {
        return new Customer();
    }
}

当Spring的MethodValidationPostProcessor(一个BeanPostProcessor)发现类上面有@Validated注解时,就会通过下面的方法,创建一个切面(adviser)。

@Override
public void afterPropertiesSet() {
    Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
    this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}

在程序运行时,就会运行到MethodValidationInterceptor这个拦截器类的invoke()方法。

可以看出,若参数校验不通过,默认是抛异常的,抛出的异常为ConstraintViolationException。那怎么自定义参数校验的返回结果呢?有两种思路:

  1. 看看Spring是否支持类似spring web中的ExceptionHandler这样的异常处理器
    本人在网上搜索,除了spring web之外,没有提到有这种全局处理器的。
  2. 模仿Spring的MethodValidationInterceptor实现思路,自己实现一个AOP拦截器。当参数校验不通过时,返回一个Response对象。
    本人模仿着Spring处理的逻辑,写了一个AOP,将抛出异常改为返回一个对象。代码见附录A

3 总结

如果调用方做了异常兼容处理,则可以直接使用默认的处理方式(抛异常)。毕竟,参数校验只是一个防御性质的编程习惯,正常逻辑下的参数都是符合要求的。
如果想要给调用方更好的体验方式,则可以“捕获异常”,将异常包装为一个对象返回给调用方。

附录A

这里只是一个示例,仅供参考。

  1. 注解定义

     package com.chris.validation;
     // 省略其他说明
    
     import java.lang.annotation.Documented;
     import java.lang.annotation.ElementType;
     import java.lang.annotation.Retention;
     import java.lang.annotation.RetentionPolicy;
     import java.lang.annotation.Target;
    
     @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
     @Retention(RetentionPolicy.RUNTIME)
     @Documented
     public @interface MyValidated {
    
         Class<?>[] value() default {};
    
     }
    
  2. 方法校验拦截器定义
     @Component
     public class MyMethodValidationInterceptor implements MethodInterceptor {
    
         private final Validator validator;
    
         /**
         * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath.
         */
         public MyMethodValidationInterceptor() {
             this(Validation.buildDefaultValidatorFactory());
         }
    
         /**
         * Create a new MethodValidationInterceptor using the given JSR-303 ValidatorFactory.
         * @param validatorFactory the JSR-303 ValidatorFactory to use
         */
         public MyMethodValidationInterceptor(ValidatorFactory validatorFactory) {
             this(validatorFactory.getValidator());
         }
    
         /**
         * Create a new MethodValidationInterceptor using the given JSR-303 Validator.
         * @param validator the JSR-303 Validator to use
         */
         public MyMethodValidationInterceptor(Validator validator) {
             this.validator = validator;
         }
    
    
         @Override
         @SuppressWarnings("unchecked")
         public Object invoke(MethodInvocation invocation) throws Throwable {
             // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
             if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
                 return invocation.proceed();
             }
    
             Class<?>[] groups = determineValidationGroups(invocation);
    
             // Standard Bean Validation 1.1 API
             ExecutableValidator execVal = this.validator.forExecutables();
             Method methodToValidate = invocation.getMethod();
             Set<ConstraintViolation<Object>> result;
    
             try {
                 result = execVal.validateParameters(
                         invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
             }
             catch (IllegalArgumentException ex) {
                 // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
                 // Let's try to find the bridged method on the implementation class...
                 methodToValidate = BridgeMethodResolver.findBridgedMethod(
                         ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
                 result = execVal.validateParameters(
                         invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
             }
             if (!result.isEmpty()) {
                 // 这里是处理的重点。原来的方式是throw new ConstraintViolationException(result);这里根据returnType,创建一个对象,然后返回空对象。
                 Class<?> returnType = invocation.getMethod().getReturnType();
                 Object retObj = Class.forName(returnType.getName()).newInstance();
                 CommonResponse response = (CommonResponse) retObj; // 这里直接进行了强转,实际上需要先判断类型。
                 response.setCode(400);
                 response.setMsg("参数错误");
                 response.setResult(null);
                 return response;
     //            throw new ConstraintViolationException(result);
             }
    
             Object returnValue = invocation.proceed();
    
             // 这里我们就不校验返回结果了。
             // result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
             // if (!result.isEmpty()) {
              //   throw new ConstraintViolationException(result);
             // }
    
             return returnValue;
         }
    
         private boolean isFactoryBeanMetadataMethod(Method method) {
             Class<?> clazz = method.getDeclaringClass();
    
             // Call from interface-based proxy handle, allowing for an efficient check?
             if (clazz.isInterface()) {
                 return ((clazz == FactoryBean.class || clazz == SmartFactoryBean.class) &&
                         !method.getName().equals("getObject"));
             }
    
             // Call from CGLIB proxy handle, potentially implementing a FactoryBean method?
             Class<?> factoryBeanType = null;
             if (SmartFactoryBean.class.isAssignableFrom(clazz)) {
                 factoryBeanType = SmartFactoryBean.class;
             }
             else if (FactoryBean.class.isAssignableFrom(clazz)) {
                 factoryBeanType = FactoryBean.class;
             }
             return (factoryBeanType != null && !method.getName().equals("getObject") &&
                     ClassUtils.hasMethod(factoryBeanType, method.getName(), method.getParameterTypes()));
         }
    
         /**
         * Determine the validation groups to validate against for the given method invocation.
         * <p>Default are the validation groups as specified in the {@link Validated} annotation
         * on the containing target class of the method.
         * @param invocation the current MethodInvocation
         * @return the applicable validation groups as a Class array
         */
         protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
             Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
             if (validatedAnn == null) {
                 validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class);
             }
             return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
         }
    
     }
    
  3. BeanPostProcessor定义
     @SuppressWarnings("serial")
     @Component
     public class MyMethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
             implements InitializingBean {
    
         private Class<? extends Annotation> validatedAnnotationType = MyValidated.class;
    
         @Nullable
         private Validator validator;
    
    
         /**
         * Set the 'validated' annotation type.
         * The default validated annotation type is the {@link Validated} annotation.
         * <p>This setter property exists so that developers can provide their own
         * (non-Spring-specific) annotation type to indicate that a class is supposed
         * to be validated in the sense of applying method validation.
         * @param validatedAnnotationType the desired annotation type
         */
         public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
             Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
             this.validatedAnnotationType = validatedAnnotationType;
         }
    
         /**
         * Set the JSR-303 Validator to delegate to for validating methods.
         * <p>Default is the default ValidatorFactory's default Validator.
         */
         public void setValidator(Validator validator) {
             // Unwrap to the native Validator with forExecutables support
             if (validator instanceof LocalValidatorFactoryBean) {
                 this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
             }
             else if (validator instanceof SpringValidatorAdapter) {
                 this.validator = validator.unwrap(Validator.class);
             }
             else {
                 this.validator = validator;
             }
         }
    
         /**
         * Set the JSR-303 ValidatorFactory to delegate to for validating methods,
         * using its default Validator.
         * <p>Default is the default ValidatorFactory's default Validator.
         * @see javax.validation.ValidatorFactory#getValidator()
         */
         public void setValidatorFactory(ValidatorFactory validatorFactory) {
             this.validator = validatorFactory.getValidator();
         }
    
    
         @Override
         public void afterPropertiesSet() {
             Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
             this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
         }
    
         /**
         * Create AOP advice for method validation purposes, to be applied
         * with a pointcut for the specified 'validated' annotation.
         * @param validator the JSR-303 Validator to delegate to
         * @return the interceptor to use (typically, but not necessarily,
         * a {@link MethodValidationInterceptor} or subclass thereof)
         * @since 4.2
         */
    
         // 这里使用上面创建的方法校验拦截器类
         protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
             return (validator != null ? new MyMethodValidationInterceptor(validator) : new MyMethodValidationInterceptor());
         }
     }
    
  4. 使用@MyValidated注解
     @Service
     @MyValidated
     public class CustomerService {
    
         public CommonResponse<Customer> loadCustomer(@Min(1) Integer id) {
             return new CommonResponse(200, "成功", new Customer());
         }
     }