您现在的位置是:首页 >学无止境 >从零开始 Spring Boot 30:数据校验网站首页学无止境

从零开始 Spring Boot 30:数据校验

魔芋红茶 2024-06-17 10:47:00
简介从零开始 Spring Boot 30:数据校验

从零开始 Spring Boot 30:数据校验

spring boot

图源:简书 (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)

需要注意的是,这里使用的Validatorjakarta.validation.Validator,而非Spring的同名类。

通过Validator.validate方法可以利用相应的校验用注解进行验证,如果没有通过验证,会返回一个Set<ConstraintViolation<?>>类型的对象,其中包含了所有验证出错信息。

当然这里也可以注入Spring的Validator进行验证,具体的验证写法有所不同,相应的示例可以看后边的自定义ValidatorDataBinder一节。

自定义校验注解

可以自定义类似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是仅检查UserDTOname属性。

创建一个类,并实现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

我们也可以像之前介绍的ConverterFormatter那样,简单实现SpringValidator接口,并在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)获取。

参考资料

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。