java中的参数校验1:基本使用

java,参数校验,validation

Posted by Chris on January 5, 2020

1 背景

在业务代码开发中,为了代码稳健性的问题,不得不校验参数是否合法(防御性质的编程习惯)。但是,参数校验会很繁琐:

  • 参数校验的逻辑大多数很相似且很枯燥 参数校验的逻辑大部分相似,很多场景大都是校验是否为空,例如字符串是否为空、数字是否大于0、列表是否为空、对象是否为null等。

为了提升开发效率,自然会想到将参数校验的逻辑抽象出来。

目前,JSR 380定义了一套参数校验的API,标准的API定义在包validation-api里面定义了:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.0.Final</version>
</dependency>

validation-api只是API定义,还需要实现类。hibernate validator是一套实现了validation-api定义的实现类。它的pom坐标如下:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.2.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-annotation-processor</artifactId>
    <version>6.0.2.Final</version>
</dependency>

在实际使用中,我们必须同时引入validation-api定义包和实现类hibernate-validator、hibernate-validator-annotation-processor。

引入了API定义包和实现包之后,还需要定义是否进行参数校验。目前,有两种方式:AOP的方式和手动API编程的方式。

  • AOP的方式
    通过AOP的方式,在AOP的切面中,获取方法中的参数,并进行参数校验。
  • 手动API编程的方式
    在每个方法中,通过ValidatorFactory获取到Validator,然后调用Validator.validate(object, groups)方法进行参数校验。

下面看看这两种方式。

2 spring-boot-starter-web提供的aop方式

2.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>

校验JavaBean属性

  1. 定义JavaBean和rest接口
    Customer的定义如下。其中@Data为lombok的注解,提供getter/setter方法(读者需要自行引入,这里不多说)。@Min@Max@NotEmpty均为validation-api中定义的校验注解。(读者自己引入validation-api之后,看看这些注解的详细解释。)这里虽然使用了@Min@Max@NotEmpty等注解,但是并不起作用,还需要通过配置开启AOP。
     @Data
     public class Customer {
    
         private int customerId;
         // age最小值为1
         @Min(1)
         private int age;
         // gender值最小为1,最大为2
         @Min(1)
         @Max(2)
         private int gender;
         // country不为空
         @NotEmpty
         private String country;
         // productNames不为空,且里面的元素不能为空
         @NotEmpty
         private List<@NotBlank(message = "productNames can not be null") String> productNames;
     }
    

    REST接口定义如下。@Valid注解是JSR定义的一个标准注解,根据接口的文档,注解用来标记方法参数、方法返回值或者对象的属性需要进行校验,并且校验时会递归的进行校验。@Validated是Spring定义的一个注解,放在类级别上面,会开启一个AOP。

     @RestController
     @Validated
     public class CustomerController {
    
         @RequestMapping("/customer/saveCustomer")
         public void saveCustomer(@Valid Customer customer) {
             System.out.println(customer);
         }
     }
    
  2. 测试
    这样做了之后,就可以进行测试了。
     POST localhost:8080/customer/saveCustomer
    

    这里没有带上POST内容,因此,内容是空的。 当执行请求时,会执行Spring实现的一个AOP。当校验不符合要求时,默认会抛出异常。

     {
         "timestamp": "2020-01-05T12:34:05.895+0000",
         "status": 400,
         "error": "Bad Request",
         "errors": [
             {
                 "codes": [
                     "NotEmpty.customer.productNames",
                     "NotEmpty.productNames",
                     "NotEmpty.java.util.List",
                     "NotEmpty"
                 ],
                 "arguments": [
                     {
                         "codes": [
                             "customer.productNames",
                             "productNames"
                         ],
                         "arguments": null,
                         "defaultMessage": "productNames",
                         "code": "productNames"
                     }
                 ],
                 "defaultMessage": "must not be empty",
                 "objectName": "customer",
                 "field": "productNames",
                 "rejectedValue": null,
                 "bindingFailure": false,
                 "code": "NotEmpty"
             },
             {
                 "codes": [
                     "Min.customer.age",
                     "Min.age",
                     "Min.int",
                     "Min"
                 ],
                 "arguments": [
                     {
                         "codes": [
                             "customer.age",
                             "age"
                         ],
                         "arguments": null,
                         "defaultMessage": "age",
                         "code": "age"
                     },
                     1
                 ],
                 "defaultMessage": "must be greater than or equal to 1",
                 "objectName": "customer",
                 "field": "age",
                 "rejectedValue": 0,
                 "bindingFailure": false,
                 "code": "Min"
             },
             // 省略其他属性的校验结果信息
         ],
         "message": "Validation failed for object='customer'. Error count: 4",
         "path": "/customer/saveCustomer"
     }
    

校验PathVariable或者RequestParam的方法参数

  1. REST接口定义 @Validated注解需要放在类级别上,用于支持方法的基本类型的参数校验。

     @RestController
     @Validated
     public class CustomerController {
    
         @RequestMapping("/customer/getCustomer")
         public Customer getCustomer( @RequestParam("customerId") @Min(1) int customerId) {
             System.out.println(customerId);
             return new Customer();
         }
     }
    
  2. 测试

     GET localhost:8080/customer/getCustomer?customerId=0
    

    结果为如下。返回500错误。

     {
         "timestamp": "2020-01-05T12:47:23.662+0000",
         "status": 500,
         "error": "Internal Server Error",
         "message": "getCustomer.customerId: must be greater than or equal to 1",
         "path": "/customer/getCustomer"
     }
    

2.2 @Validated@Valid比较

项目 @Valid @Validated
定义 java community process (JSR-303)定义的标准,javax.validation.Valid JSR-303的@Valid注解的变体,由Spring支持。org.springframework.validation.annotation.Validated
作用 @Valid支持标准的参数校验,但不支持validation group。 由于@Valid不支持validation group,故spring推出了@Validated来支持validation group,有一个属性value用来支持validation group。当类上面有@Validated注解时,会开启一个AOP代理,进行方法级别的参数校验和返回值校验。
示例   echo(@ModelAttribute(“myCandidate”) @Validated(UpdateGroup.class) Candidate myCandidate)
使用位置 可以用于类、方法、方法参数、属性和构造器上。 只能用于类、方法和方法参数上。可以用于类级别上,用于Spring支持方法的参数校验,包括JavaBean和基本类型

3 显式API编程的方式校验JavaBean

显式API编程的方式是指在代码中进行参数校验,一般的步骤如下。

  1. 引入jar包
     <dependency>
         <groupId>javax.validation</groupId>
         <artifactId>validation-api</artifactId>
         <version>2.0.0.Final</version>
     </dependency>
     <dependency>
         <groupId>org.hibernate.validator</groupId>
         <artifactId>hibernate-validator</artifactId>
         <version>6.0.2.Final</version>
     </dependency>
     <dependency>
         <groupId>org.hibernate.validator</groupId>
         <artifactId>hibernate-validator-annotation-processor</artifactId>
         <version>6.0.2.Final</version>
     </dependency>
    
  2. JavaBean的定义
    接口的定义如下。先得到一个ValidatorFactory工厂对象(线程安全),然后得到一个Validator对象(线程安全)。通过显示调用<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);方法来进行参数校验。
     public class CustomerService {
         private static ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
         private static Validator validator = factory.getValidator();
    
         public void saveCustomer(@Valid Customer customer) {
             Set<ConstraintViolation<@Valid Customer>> validate = validator.validate(customer);
             validate.forEach(System.out::println);
         }
    
         public static void main(String[] args) {
             CustomerService customerService = new CustomerService();
             customerService.saveCustomer(new Customer());
         }
     }
    

    validator.validate()方法的返回结果为一个Set集合。若参数校验都通过,则为一个空集合;否则,将错误信息放入Set中。

根据Validator类的定义,只能对JavBean实例及其属性进行校验,不能对普通类型进行校验。

4 最佳实践

一种最佳写法是,在类注解上使用Spring定义的@Validated注解,在方法参数上使用JSR-303定义的注解,例如@Valid@NotNull。这样配合使用,既支持JavaBean,又支持基本类型。不仅适用于SpringMVC,也可以适用于service层。例如下面的例子所示。

Controller层的使用如下:

@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;
}

@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();
    }
}

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();
    }
}

5 总结

上面讲了AOP的方式和API编程的方式来校验JavaBean或者方法基本类型的参数。由于@Validated方式由Spring AOP支持,使用场景广泛,推荐使用。也可以看出,在默认情况下,若参数校验不合格,方法会抛异常。如果我们想要自定义参数校验不通过时的业务处理逻辑,则需要额外的工作。这部分下次再讲。

6 参考

HOW-TO: Method-level validation in Spring with @Validated annotation