Spring 参数校验
在前端对数据进行校验的情况下,在后端还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。
JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候很方便地进行校验了!
实际校验的时候,我们用的是 Hibernate Validator 框架。
Hibernate Validator 是 Hibernate 团队最初的数据校验框架:
-
Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现;
-
Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现;
-
Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现,是目前最新版的。
SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。
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=)
:被注释的元素必须在合适的范围内
需要注意的是:所有的注解,推荐使用 JSR 注解,即
javax.validation.constraints
,而不是org.hibernate.validator.constraints
。
依赖
导入 maven 的依赖:
<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>
验证方式
验证 Controller 的输入
验证请求体:RequestBody
我们需要在待验证的参数上加上 @Valid
注解,如果验证失败,它将抛出 MethodArgumentNotValidException。默认情况下,Spring 会将此异常转换为 HTTP Status 400(错误请求)。
【示例】
@RestController
@RequestMapping("/api")
publicclass PersonController {
@PostMapping("/person")
public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
return ResponseEntity.ok().body(person);
}
}
通常,我们需要一个全局异常处理器,帮助我们捕获未被处理的异常:
@ControllerAdvice(assignableTypes = {PersonController.class})
publicclass GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}
验证请求参数:Path Variables 和 Request Parameters
此时,我们需要在 Controller 上加上 @Validated
注解,这样才能使 Spring 校验方法参数。
@RestController
@RequestMapping("/api")
@Validated
publicclass PersonController {
@GetMapping("/person/{id}")
public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) {
return ResponseEntity.ok().body(id);
}
@PutMapping("/person")
public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6,message = "超过 name 的范围了") String name) {
return ResponseEntity.ok().body(name);
}
}
ExceptionHandler
@ControllerAdvice(assignableTypes = {PersonController.class})
publicclass GlobalExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}
校验 Service
只需要在 Service 类上加上 @Validated
注解即可:
@Service
@Validated
publicclass PersonService {
public void validatePerson(@Valid Person person){
// do something
}
}
Validator 编程方式手动进行参数验证
非 spring 项目中,通过 Validator 工厂类获得 Validator 实例,手动校验:
public void check_person_manually() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Person person = new Person();
person.setSex("Man22");
person.setClassId("82938390");
person.setEmail("SnailClimb");
Set<ConstraintViolation<Person>> violations = validator.validate(person);
//output:
//email 格式不正确
//name 不能为空
//sex 值不在可选范围
for (ConstraintViolation<Person> constraintViolation : violations) {
System.out.println(constraintViolation.getMessage());
}
}
在 spring 项目中,我们也可以通过 @Autowired
直接注入的方式。
@Autowired
Validator validate
自定义 Validator
校验字段范围
【场景】:对于 region 字段只能是 China、China-Taiwan、China-HongKong 这三个中的一个。
接下来,我们按照下面的步骤,就可以实现一个自定义注解校验。
- 步骤一:创建一个枚举类
enum Region {
CHINA("China"), TAIWAN("China-Taiwan"), HONGKONG("China-HongKong");
private final String value;
Region(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
- 步骤二:创建一个注解
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = EnumValidator.class)
@Documented
public @interface EnumSelector {
Class<? extends Enum<?>> clazz();
String message() default "the value should be the enumerate value in the class {clazz}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- 步骤三:实现 ConstraintValidator 接口,并重写 isValid 方法
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
public class EnumValidator implements ConstraintValidator<EnumSelector, Object> {
private List<String> values;
@Override
public void initialize(EnumSelector enumSelector) {
values = Stream.of(enumSelector.clazz().getEnumConstants()).map(Enum::toString).collect(Collectors.toList());
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (Objects.isNull(value)) {
return true;
}
return values.contains(value.toString());
}
}
- 步骤四:使用注解:
@EnumSelector(clazz= Region.class)
@NotNull
private String region;
验证组
某些场景下我们需要使用到验证组,例如,对资源操作的不同方法,有不同的验证规则。
- 先创建三个接口
public interface QueryUser {
}
public interface AddUser {
}
public interface DeleteUser {
}
- 使用验证组
class User {
@NotNull(groups = {AddUser.class, DeleteUser.class})
private String name;
@NotNull(groups = {QueryUser.class, DeleteUser.class})
private String userId;
...
}
- 校验
@Service
@Validated
publicclass UserService {
@Validated(AddUser.class)
public void addUser(@Valid User user) {
// do something
}
@Validated(QueryUser.class)
public void getUser(@Valid User user) {
// do something
}
@Validated(DeleteUser.class)
public void deleteUser(@Valid User user) {
// do something
}
}
使用验证组这种方式的时候一定要小心,这是一种反模式,还会造成代码逻辑性变差。
参考: