SpringBoot 参数校验组件
在编码过程中数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。
最普通的做法就是在进入方法的时候进行一些参数校验,但是,不太建议这样来写,这样的代码明显违背了单一职责原则。大量的非业务代码混杂在业务代码中,非常难以维护,还会导致业务层代码冗杂。
下面会通过实例程序演示如何在 SpringBoot 程序中优雅地的进行参数验证。
添加相关依赖
// 一般应用需要引入下面这些依赖
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.9.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.6</version>
</dependency>
// Spring Boot 应用引入spring-boot-starter-web即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
// Spring Boot 2.3 1 之后,spring-boot-starter-validation 已经不包括在了 spring-boot-starter-web 中,需要手动引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Controller的校验
/**
* Controller使用示例
*/
@RestController
@RequestMapping("/api/persons")
@Validated
public class PersonController {
@PostMapping
public ResponseEntity<PersonRequest> save(@RequestBody @Valid PersonRequest personRequest) {
return ResponseEntity.ok().body(personRequest);
}
@GetMapping("/{id}")
public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5, message = "超过 id 的范围了") Integer id) {
return ResponseEntity.ok().body(id);
}
@PutMapping
public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "超过 name 的范围了") String name) {
return ResponseEntity.ok().body(name);
}
}
/**
* bean使用示例
**/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {
@NotNull(message = "classId 不能为空")
private String classId;
@Size(max = 33)
@NotNull(message = "name 不能为空")
private String name;
@Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可选范围")
@NotNull(message = "sex 不能为空")
private String sex;
@NotNull(message = "phoneNumber 不能为空")
private String phoneNumber;
}
Service的校验
@Service
@Validated
public class PersonService {
public void validatePersonRequest(@Valid PersonRequest personRequest) {
// do something
}
}
// 校验不通过会抛出ConstraintViolationException
e.getConstraintViolations().forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
手动校验参数的方式
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<PersonRequest>> validate = validator.validate(personRequest);
常用校验注解总结
JSR303 定义了 Bean Validation(校验)的标准 validation-api,并没有提供实现。Hibernate Validation是对这个规范/规范的实现 hibernate-validator,并且增加了 @Email、@Length、@Range 等注解。Spring Validation 底层依赖的就是Hibernate Validation。
JSR 提供的校验注解:
- @Null 被注释的元素必须为 null
- @NotNull 被注释的元素必须不为 null
- @AssertTrue 被注释的元素必须为 true
- @AssertFalse 被注释的元素必须为 false
- @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
- @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
- @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
- @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
- @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
- @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
- @Past 被注释的元素必须是一个过去的日期
- @Future 被注释的元素必须是一个将来的日期
- @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
Hibernate Validator 提供的校验注解:
- @NotBlank(message =) 验证字符串非 null,且长度必须大于 0
- @Email 被注释的元素必须是电子邮箱地址
- @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
- @NotEmpty 被注释的字符串的必须非空
- @Range(min=,max=,message=) 被注释的元素必须在合适的范围内
自定义 Validator(实用)
如果自带的校验注解无法满足你的需求的话,还可以自定义实现注解。
案例一、校验特定字段的值是否在可选范围
比如我们现在多了这样一个需求:PersonRequest 类多了一个 Region 字段,Region 字段只能是China、China-Taiwan、China-HongKong这三个中的一个。
/**
* 步骤一:创建Region注解
*/
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = RegionValidator.class)
@Documented
public @interface Region {
String message() default "Region 值不在可选范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
/**
* 步骤二:实现 ConstraintValidator接口,并重写isValid 方法
*/
public class RegionValidator implements ConstraintValidator<Region, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
HashSet<Object> regions = new HashSet<>();
regions.add("China");
regions.add("China-Taiwan");
regions.add("China-HongKong");
return regions.contains(value);
}
}
/**
* 步骤三:在bean中使用注解
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {
@Region
private String region;
}
案例二、校验电话号码
校验我们的电话号码是否合法,这个可以通过正则表达式来做,相关的正则表达式都可以在网上搜到,你甚至可以搜索到针对特定运营商电话号码段的正则表达式
/**
* 步骤一:创建PhoneNumber注解
*/
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface PhoneNumber {
String message() default "Invalid phone number";
Class[] groups() default {};
Class[] payload() default {};
}
/**
* 步骤二:实现 ConstraintValidator接口,并重写isValid 方法
*/
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
@Override
public boolean isValid(String phoneField, ConstraintValidatorContext context) {
if (phoneField == null) {
// can be null
return true;
}
String regExp = "^1[3|4|5|7|8][0-9]{9}$";
return phoneField.matches(regExp);
}
}
/**
* 步骤三:在bean中使用注解
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {
@PhoneNumber(message = "phoneNumber 格式不正确")
private String phoneNumber;
}
验证组
当我们对对象操作的不同方法有不同的验证规则的时候才会用到验证组
// 举例:对Person进行增删是对参数有不同对校验
// 步骤一:先创建两个接口,代表不同的验证组
public interface AddPersonGroup {
}
public interface DeletePersonGroup {
}
// 步骤二:使用验证组
@Data
public class Person {
// 当验证组为 DeletePersonGroup 的时候 group 字段不能为空
@NotNull(groups = DeletePersonGroup.class)
// 当验证组为 AddPersonGroup 的时候 group 字段需要为空
@Null(groups = AddPersonGroup.class)
private String group;
}
@Service
@Validated
public class PersonService {
@Validated(AddPersonGroup.class)
public void validatePersonGroupForAdd(@Valid Person person) {
// do something
}
@Validated(DeletePersonGroup.class)
public void validatePersonGroupForDelete(@Valid Person person) {
// do something
}
}
// 步骤三:测试
@Test(expected = ConstraintViolationException.class)
public void should_check_person_with_groups() {
Person person = new Person();
person.setGroup("group1");
personService.validatePersonGroupForAdd(person);
}
@Test(expected = ConstraintViolationException.class)
public void should_check_person_with_groups2() {
Person person = new Person();
personService.validatePersonGroupForDelete(person);
}