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属性
- 定义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); } } - 测试
这样做了之后,就可以进行测试了。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的方法参数
-
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(); } } -
测试
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编程的方式是指在代码中进行参数校验,一般的步骤如下。
- 引入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> - 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