从零开始 Spring Boot 30:数据校验
图源:简书 (jianshu.com)
在从零开始 Spring Boot 13:参数校验 - 红茶的个人站点 (icexmoon.cn)一文中,我讨论了一些可以用于参数校验的注解。实际上这些注解都是来自于Jakarta Bean Validation
的Java数据验证体系的一部分。关于Bean Validation在Spring中的应用,还可以进行更进一步的探索,这将是本文接下来的内容。
关于
Jakarta Bean Validation
的更多介绍,可以参考Jakarta Bean Validation - Home。
将验证移入Service层
我们之前讨论的都是怎么在Controller层对入参进行验证,比如下面的例子:
@Data
public class UserDTO {
@NotBlank
private String name;
@NotBlank
private String password;
@NotBlank
private String phone;
@Min(1)
private Integer age;
}
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/add")
public String addUser(@Validated @RequestBody UserDTO user) {
userService.addUser(user);
return Result.success().toString();
}
}
通常来说这样做是最正确的,因为可以将错误输入排除在所有的服务端处理逻辑之外。但是某些时候我们也可能希望将验证行为移动到Service层,这样做的理由可能是并非所有输入数据都来自Web接口的请求,可能有一些其他方式的输入动作。此时为了能充分复用数据验证逻辑,我们可以将数据验证逻辑移动到Service层:
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/add")
public String addUser(@RequestBody UserDTO user) {
userService.addUser(user);
return Result.success().toString();
}
}
public interface UserService {
void addUser(UserDTO userDTO);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private Validator validator;
public void addUser(UserDTO userDTO) {
Set<ConstraintViolation<UserDTO>> violations = validator.validate(userDTO);
if (!violations.isEmpty()) {
//没有通过验证
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<UserDTO> constraintViolation : violations) {
sb.append(constraintViolation.getMessage());
}
throw new ConstraintViolationException("没有通过验证:" + sb.toString(), violations);
}
//通过验证
//这里可以添加向持久层添加用户的操作
}
}
注意,这里在Controller中,addUser
方法的user
参数前没有注解@Validated
,所以不会在Controller层触发验证逻辑。
在UserServiceImpl
中,注入了一个Validator
。这是Spring默认的用于验证的对象。
也可以自己配置一个
LocalValidatorFactoryBean
类型的bean,具体可以参考核心技术 (springdoc.cn)。
需要注意的是,这里使用的Validator
是jakarta.validation.Validator
,而非Spring的同名类。
通过Validator.validate
方法可以利用相应的校验用注解进行验证,如果没有通过验证,会返回一个Set<ConstraintViolation<?>>
类型的对象,其中包含了所有验证出错信息。
当然这里也可以注入Spring的
Validator
进行验证,具体的验证写法有所不同,相应的示例可以看后边的自定义Validator的DataBinder一节。
自定义校验注解
可以自定义类似Bean Validation中那样的数据校验注解。
这里同样以上面的User
类的验证为例。
先定义一个验证User
类的注解:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UserConstraintValidator.class)
public @interface UserConstraint {
//这里可以根据需要添加一些属性用于丰富验证手段
Pattern value() default Pattern.CHECK_ALL;
String message() default "用户信息有错,无法通过验证";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
enum Pattern {
CHECK_ALL, ONLY_NAME
}
}
要注意的是,自定义注解中必须包含以下三个属性:
message
,包含验证出错后的提示信息。groups
payload
如果缺少这三个属性,就会报错。
在这个示例中,我增加了一个value
属性,用于指定一个检查模式,CHECK_ALL
是检查UserDTO
的所有属性,ONLY_NAME
是仅检查UserDTO
的name
属性。
创建一个类,并实现ConstraintValidator<A extends Annotation, T>
接口:
public class UserConstraintValidator implements ConstraintValidator<UserConstraint, UserDTO> {
private UserConstraint.Pattern pattern;
@Autowired
private UserService userService;
@Override
public void initialize(UserConstraint constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
pattern = constraintAnnotation.value();
}
@Override
public boolean isValid(UserDTO userDTO, ConstraintValidatorContext constraintValidatorContext) {
if (pattern == UserConstraint.Pattern.CHECK_ALL) {
if (userDTO.getAge() == null || userDTO.getAge() <= 0) {
return false;
}
if (userDTO.getName() == null || userDTO.getName().isEmpty()) {
return false;
}
if (userDTO.getPassword() == null || userDTO.getPassword().isEmpty()) {
return false;
}
if (userDTO.getPhone() == null || userDTO.getPhone().isEmpty()) {
return false;
}
}
if (pattern == UserConstraint.Pattern.ONLY_NAME) {
if (userDTO.getName() == null || userDTO.getName().isEmpty()) {
return false;
}
} else {
;
}
return true;
}
}
因为Spring会使用SpringConstraintValidatorFactory
创建ConstraintValidator
实例,也就是说自定义的ConstraintValidator
也是以bean的方式被注入,因此可以在自定义的ConstraintValidator
类中使用@Autowired
进行依赖注入。
当然这里注入的
userService
实际上并没有任何用途,只是为了说明可以进行注入。
现在就可以像其他bean validation注解那样使用自定义注解了,比如:
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/add")
public String addUser(@UserConstraint @RequestBody UserDTO user) {
userService.addUser(user);
return Result.success().toString();
}
}
这里的@UserConstraint
与@Min
之类的注解用法类似,只不过实现的是自定义的验证逻辑。
自定义Validator
我们也可以像之前介绍的Converter
和Formatter
那样,简单实现Spring
的Validator
接口,并在Controller中注册以使用:
public class UserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return UserDTO.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
ValidationUtils.rejectIfEmpty(errors, "password", "password.empty");
ValidationUtils.rejectIfEmpty(errors, "phone", "phone.empty");
UserDTO user = (UserDTO) target;
if (user.getAge() < 0 || user.getAge() > 150) {
errors.rejectValue("age", "too.low.or.too.big");
}
}
}
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/add")
public String addUser(@Validated @RequestBody UserDTO user) {
// userService.addUser(user);
return Result.success().toString();
}
@InitBinder
public void initBinder(WebDataBinder webDataBinder){
webDataBinder.addValidators(new UserValidator());
}
}
现在即使注释掉UserDTO
中的相应注解,也可以正常进行验证:
@Data
public class UserDTO {
// @NotBlank
private String name;
// @NotBlank
private String password;
// @NotBlank
private String phone;
// @Min(1)
private Integer age;
}
DataBinder
Validator
结合DataBinder
可以在任何地方进行验证,比如之前说的将验证移入Service层,如果要使用自定义的Spring Validator,可以这样写:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private org.springframework.validation.Validator springValidator;
// ...
@Override
public void addUserWithSpringValidator(UserDTO userDTO) {
DataBinder dataBinder = new DataBinder(userDTO);
dataBinder.addValidators(springValidator, new UserValidator());
dataBinder.validate();
BindingResult bindingResult = dataBinder.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
if (!allErrors.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (ObjectError error : allErrors) {
String objectName = error.getObjectName();
if (error instanceof FieldError){
FieldError fieldError = (FieldError)error;
objectName = fieldError.getField();
}
String errorMsg = error.getDefaultMessage();
if (ObjectUtils.isEmpty(errorMsg)) {
errorMsg = error.getCode();
}
sb.append(objectName).append(" ").append(errorMsg);
sb.append(",");
}
throw new ValidationException(sb.toString());
}
//通过验证
//这里可以添加向持久层添加用户的操作
}
}
这里通过DataBinder.addValidators()
方法将Spring默认的Validator
与自定义的Validator
都添加了进去,这样,在调用DataBinder.validate()
方法时,就可以让UserDTO
上使用的bean validation注解和自定义的UserValidator
相应的验证逻辑都生效。
Controller层:
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
// ...
@PostMapping("/add/springValidator")
public String addUserWithSpringValidator(@RequestBody UserDTO user){
userService.addUserWithSpringValidator(user);
return Result.success().toString();
}
}
The End,谢谢阅读。
本文所有的示例代码可以通过ch30/validator · 魔芋红茶/learn_spring_boot - 码云 - 开源中国 (gitee.com)获取。
参考资料
- MethodValidationPostProcessor (Spring Framework 6.0.8-SNAPSHOT API)
- 自定义校验注解ConstraintValidator - 陈皮的JavaLib - 博客园 (cnblogs.com)
- Spring Validation in the Service Layer | Baeldung — 服务层中的Spring验证|白洞
- 从零开始 Spring Boot 13:参数校验 - 红茶的个人站点 (icexmoon.cn)
- Jakarta Bean Validation - Home
- 核心技术 (springdoc.cn)
- 聊聊Spring中的数据绑定 — DataBinder本尊(源码分析)【享学Spring】_YourBatman的博客-CSDN博客
- SpringMVC常见组件之DataBinder数据绑定器分析_流烟默的博客-CSDN博客