数据校验
spring boot版本:3.5.3
场景
现在有一个用户注册接口,控制器方法参数就是用户对象:
@Slf4j
@RestController
@RequestMapping("user")
public class UserController {
@PostMapping("register")
public Result<String> register(@RequestBody User user){
log.info("插入数据库~");
return Result.success();
}
}
用户对象:
@Data
public class User {
private String username;
private String password;
private String mobile;
private String email;
}
此时没有任何校验,数据会直接写入数据库。接下来引入数据校验功能,校验user对象中的各个字段。
1.引入依赖:
spring boot将数据校验场景抽取到单独的starter中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
这个依赖中引入了以下两个包:
- hibernate-validator-8.0.2.Final.jar,hibernate对JSR303规范的扩展实现
- jakarta.validation-api-3.0.2.jar,JSR303规范
2.在参数实体类的字段上,标注合适的注解
至于可以标注哪些注解,可以参考上面的两个包,示例:
常用的如:@Max、@Min、@NotNull、@NotBlank等。

常用的如:@Length。

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
@Data
public class User {
@NotEmpty(message = "用户名不可为空!")
private String username;
private String password;
@Length(min = 11, max = 11)
private String mobile;
@Email
private String email;
}
3.开启参数校验
在控制器方法参数前,添加注解:@Vaild 或者 @Validated。
@Slf4j
@RestController
@RequestMapping("user")
public class UserController {
@PostMapping("register")
public Result<String> register(@Valid @RequestBody User user){
log.info("插入数据库~");
return Result.success();
}
}
4.测试
Postman发送请求
不填任何参数:

填入正确的参数:

5.当参数未通过校验时,提示详细信息
方式一:BindingResult
在每一个需要校验的控制器方法参数后面,加一个BindingResult参数,这样的话,如果这个参数未通过校验,校验结果会封装到后面的BindingResult中。
如果控制器方法有多个需要校验的参数,那么每个参数之后都要加一个BindingResult。
@Slf4j
@RestController
@RequestMapping("user")
public class UserController {
@PostMapping("register")
public Result<String> register(@Valid @RequestBody User user, BindingResult bindingResult){
log.info("插入数据库~");
return Result.success();
}
}
注意:当使用BindingResult后,必须手动处理异常!因为当参数未通过校验时,spring boot将不会抛出异常,而是将校验结果封装到BindingResult中。如果不手动处理封装结果,参数校验将形同虚设。
如果有多个控制器方法,每个控制器方法有多个需要校验的参数,就会很麻烦,还容易遗漏,因此,这种方式并不推荐。

手动处理BindingResult
@Slf4j
@RestController
@RequestMapping("user")
public class UserController {
@PostMapping("register")
public Result<String> register(@Valid @RequestBody User user, BindingResult bindingResult){
// 判断是否有参数未通过校验
if(bindingResult.hasErrors()){
StringBuilder errorMessage = new StringBuilder();
// 获取所有未通过校验的参数
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
String field = fieldError.getField();
String defaultMessage = fieldError.getDefaultMessage();
errorMessage.append(defaultMessage);
}
return Result.fail(errorMessage.toString());
}
log.info("插入数据库~");
return Result.success();
}
}
方式二:全局异常处理
当参数未通过校验时,会抛出:org.springframework.web.bind.MethodArgumentNotValidException。
MethodArgumentNotValidException继承自BindException,而BindException又实现了BindingResult。
那么就可以加一个全局异常处理器,来处理这个异常。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(exception = MethodArgumentNotValidException.class)
public Result<String> handleValid(MethodArgumentNotValidException methodArgumentNotValidException){
BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
// 获取所有未通过校验的参数
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
Map<String,String> map = new HashMap<>();
for (FieldError fieldError : fieldErrors) {
String field = fieldError.getField();
String defaultMessage = fieldError.getDefaultMessage();
map.put(field,defaultMessage);
}
return Result.fail(map.toString());
}
}
6.进阶-自定义注解
现在要对密码做校验,密码必须包含大小写字母、数字和特殊字符,长度8-20位。类似的比较复杂的校验,就需要我们自定义校验注解。
校验密码的注解
其中@Constraint(validatedBy = {PasswordValidator.class}),指定具体校验逻辑的实现类。
不知道怎么写校验注解,可以参考原有的注解,比如@NotNull。
@Documented
@Constraint(validatedBy = {PasswordValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPassword {
String message() default "密码必须包含大小写字母、数字和特殊字符,长度8-20位";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
校验逻辑的实现类
校验类要实现jakarta.validation.ConstraintValidator接口。
第一个泛型是自定义的校验注解,第二个泛型是要校验的参数的类型。
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
// 密码规则:至少1个大写字母、1个小写字母、1个数字、1个特殊字符,长度8-20
private static final String PASSWORD_PATTERN =
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,20}$";
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
return Pattern.compile(PASSWORD_PATTERN).matcher(password).matches();
}
}
使用自定义注解
这里password属性上标注了两个注解,@NotBlank对密码做非空字符串校验,@ValidPassword校验密码的格式,两个校验分开。
但是测试发现,两个校验注解的生效顺序是随机的,当密码为空字符串时,有时提示密码不可为空,有时提示格式错误:
提示密码不可为空:

提示格式错误:

@Data
public class User {
@NotEmpty(message = "用户名不可为空!")
private String username;
@NotBlank(message = "密码不可为空!")
@ValidPassword
private String password;
@Length(min = 11, max = 11)
private String mobile;
@Email
private String email;
}
改进校验器
密码是空值时,则跳过,让@NotBlank校验。
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
// 密码规则:至少1个大写字母、1个小写字母、1个数字、1个特殊字符,长度8-20
private static final String PASSWORD_PATTERN =
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,20}$";
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
if(!StringUtils.hasText(password)){
// 密码是空值时,则跳过,让@NotBlank校验
return true;
}
return Pattern.compile(PASSWORD_PATTERN).matcher(password).matches();
}
}
这样当密码为空字符串时,就可以稳定提示密码为空了。
Spring Boot后端数据校验全解析
477

被折叠的 条评论
为什么被折叠?



