我喜欢你,可是你却并不知道.
上一章简单介绍了SpringBoot 单元测试(三十六) ,如果没有看过,请观看上一章
在后端项目中,我们常常需要对 传入的参数进行校验。
如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。
如果spring-boot版本大于2.3.x,则需要手动引入依赖:
<dependency>
<groupId>org.hibernategroupId>
<artifactId>hibernate-validatorartifactId>
<version>6.0.15.Finalversion>
dependency>
@Data
public class User implements Serializable {
/**
* @param id id编号
* @param name 姓名
* @param sex 性别
* @param age 年龄
* @param description 描述
*/
private Integer id;
@NotBlank(message = "姓名不能为空")
@Length(min = 2,max = 10, message = "姓名长度有误,在 2~10 之间")
private String name;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄最小是18")
private Integer age;
@Email(message = "邮箱格式错误")
private String email;
@Pattern(regexp = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$", message = "手机号格式错误")
private String phone;
}
@PostMapping("valid1")
public String valid1(@RequestBody @Valid User user, BindingResult bindingResult) {
// 如果有错误
// if (bindingResult.hasErrors()){
// List allErrors = bindingResult.getAllErrors();
// for (ObjectError objectError : allErrors) {
// resultBuilder.append(objectError.getCode() +":" +objectError.getDefaultMessage()+",");
// }
// return resultBuilder.toString();
// }else {
// return "验证正确" +user.getName();
// }
StringBuilder resultBuilder = new StringBuilder("");
if (bindingResult.hasErrors()){
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
resultBuilder.append(fieldError.getField() +":" +fieldError.getDefaultMessage()+",");
}
return resultBuilder.toString();
}else {
return "验证正确" +user.getName();
}
}
使用 ObjectError 对象打印时:
使用 FieldError 对象打印时:
一般会使用 FieldError
@PostMapping("valid2")
public String valid1(@NotBlank(message = "姓名不能为空")
@Length(min = 2,max = 10, message = "姓名长度有误,在 2~10 之间") String name,
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄最小是18") String age) {
return name +"," +age;
}
需要在 类上 添加 @Validated 注解才会生效
@Validated
@RestController
public class UserController {
}
这是抛出异常的, 在参数上进行处理, 很不友好。 可以通过 实体类进行封装。
@PostMapping("valid4")
public String valid4(@RequestBody @Validated User user) {
return "验证正确" +user.getName();
}
不需要在 Controller 上添加 @Validated 注解。
User 对象验证跟之前的保持一致.
这个时候 打印是这样
UserA 对象 嵌套 UserB 对象
@Data
public class UserA implements Serializable {
@NotBlank(message = "姓名不能为空")
@Length(min = 2,max = 10, message = "姓名长度有误,在 2~10 之间")
private String name;
@Valid
@NotNull
private UserB userB;
}
@Data
public class UserB implements Serializable {
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄最小是18")
private Integer age;
@Email(message = "邮箱格式错误")
private String email;
}
@PostMapping("valid3")
public String valid3(@RequestBody @Valid UserA userA, BindingResult bindingResult) {
StringBuilder resultBuilder = new StringBuilder("");
if (bindingResult.hasErrors()){
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
resultBuilder.append(fieldError.getField() +":" +fieldError.getDefaultMessage()+",");
}
return resultBuilder.toString();
}else {
return "验证正确" +userA.getName();
}
}
@Valid是属于javax.validation, @Validated 属于 Spring 的 org.springframework.validation.annotation
可以将@Validated看做是@Valid的升级版,属于是HibernateValid的封装应用。
如果写了bindingResult接收错误信息,但是业务代码没有写处理逻辑的话,即没有判断bindingResult.hasErrors(),则会跳过这个@Valid,什么都不判断。
如果没有写bindingResult的话,只写@Valid的话,还是会进行判断的
@PostMapping("valid5")
public String valid5(@RequestBody @Valid User user) {
return "验证正确" +user.getName();
}
如果是 嵌套校验的话, 必须使用 @Valid
使用@Validated不需要BindingResult,直接在参数前使用@Validated即可,效果是一样的,遇到校验出错会抛出异常
使用 @Validated 注解时,当失败时,会发现,直接抛出的异常太难处理。 可以进行全部捕获.
@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler {
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public OutputResult handleMethodArgumentNotValidException(ConstraintViolationException ex) {
return OutputResult.buildAlert(ResultCode.INVALID_PARAM, ex.getMessage());
}
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public OutputResult handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringJoiner joiner = new StringJoiner(",");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
joiner.add(fieldError.getField()).add(":").add(fieldError.getDefaultMessage());
}
String msg = joiner.toString();
return OutputResult.buildAlert(ResultCode.INVALID_PARAM, msg);
}
}
当失败时, 捕获到异常
上面可以发现, 当某个属性验证失败后,会继续验证,并不会遇到第一个错误后就返回。
@Component
public class ValidationConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败模式
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
我们定义一个实体 进行接收时, 通过 添加操作 是不需要id 的, 修改操作是需要id的, 删除操作只需要一个 id.
想通过一个实体进行验证接收的话, 可以使用 分组的概念。
默认的组是 Default.class
public interface GroupAdd {
}
public interface GroupUpdate {
}
public interface GroupDelete {
}
GroupUser 进行接收
@Data
public class GroupUser implements Serializable {
// id 更新和删除时要填入
@NotNull(message = "id不能为空", groups = {GroupUpdate.class, GroupDelete.class})
private Integer id;
@NotBlank(message = "姓名不能为空", groups = {GroupAdd.class})
@Length(min = 2,max = 10, message = "姓名长度有误,在 2~10 之间")
private String name;
@Min(value = 18, message = "年龄最小是18", groups = {GroupUpdate.class, GroupDelete.class})
private Integer age;
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d).{6,9}$", message = "密码至少包含字母数字,6~9位",groups = {GroupUpdate.class})
private String password;
}
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated(value = {GroupAdd.class, Default.class}) GroupUser user, BindingResult bindingResult) {
StringBuilder resultBuilder = new StringBuilder("");
if (bindingResult.hasErrors()){
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
resultBuilder.append(fieldError.getField() +":" +fieldError.getDefaultMessage()+",");
}
return resultBuilder.toString();
}
log.info(">>> 可以进行添加操作");
return "添加成功";
}
@PostMapping("/updateUser")
public String updateUser(@RequestBody @Validated(value = {GroupUpdate.class, Default.class}) GroupUser user, BindingResult bindingResult) {
StringBuilder resultBuilder = new StringBuilder("");
if (bindingResult.hasErrors()){
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
resultBuilder.append(fieldError.getField() +":" +fieldError.getDefaultMessage()+",");
}
return resultBuilder.toString();
}
log.info(">>> 可以进行修改操作");
return "修改成功";
}
@PostMapping("/deleteUser")
public String deleteUser(@RequestBody @Validated(value = {GroupDelete.class, Default.class}) GroupUser user, BindingResult bindingResult) {
StringBuilder resultBuilder = new StringBuilder("");
if (bindingResult.hasErrors()){
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
resultBuilder.append(fieldError.getField() +":" +fieldError.getDefaultMessage()+",");
}
return resultBuilder.toString();
}
log.info(">>> 可以进行删除操作");
return "删除成功";
}
也可以 让 自定义的组 extends Default 组,
public interface GroupDelete extends Default {
}
这样直接使用 即可.
public String deleteUser(@RequestBody @Validated(value = {GroupDelete.class}) GroupUser user, BindingResult bindingResult)
如果对密码进行验证的话, 可以使用 @Pattern 注解
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d).{6,9}$", message = "密码至少包含字母数字,6~9位",groups = {GroupUpdate.class})
private String password;
我们能不能 像使用 @Email 一样, 使用一个 @Password 注解呢?
@Password(message = "使用验证注解的",groups = {GroupUpdate.class})
private String password;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 自定义密码校验器注解
*
* @author yuejianli
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(Password.List.class)
@Documented
@Constraint(validatedBy = {PasswordValidator.class})
public @interface Password {
String message() default "密码至少包含字母数字,6~9位";
/**
* 自定义正则表达式,默认的密码正则表达式PatternMatchers.passwordRegx
*/
String regexp() default "^(?=.*[a-zA-Z])(?=.*\\d).{6,9}$";
/**
* @return the groups the constraint belongs to
*/
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@interface List {
Password[] value();
}
}
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 自定义密码校验器
*
* @author yuejianli
* @date 2023-04-04
*/
public class PasswordValidator implements ConstraintValidator<Password, CharSequence> {
private Pattern pattern;
@Override
public void initialize(Password constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
// 获取正则表达式
final String regexp = constraintAnnotation.regexp();
this.pattern = Pattern.compile(regexp);
}
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
if (value == null || value.length() == 0){
return false;
}
// 进行校验
final Matcher matcher = this.pattern.matcher(value);
return matcher.matches();
}
}
这样, 当密码不符合时,就会进行提示。
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
*
*
*
*
* @author yuejianli
* @since 2023-04-04 11:15
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {SexValidator.class})
public @interface Sex {
// 默认错误消息
String message() default "性别格式格式错误";
// 分组
Class<?>[] groups() default {};
// 负载
Class<? extends Payload>[] payload() default {};
}
public class SexValidator implements ConstraintValidator<Sex, String> {
private static final String MAN = "男";
private static final String WOMAN = "女";
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 不为null才进行校验
if (value != null) {
if(!Objects.equals(value,MAN) && !Objects.equals(value,WOMAN)) {
return Boolean.FALSE;
}
}
return Boolean.TRUE;
}
}
这样就可以使用 @Sex 注解了.
@Sex(message = "性别错误")
private String sex;
本文章参考链接: Springboot使用validator进行参数校验
本章节的代码放置在 github 上:
https://github.com/yuejianli/springboot/tree/develop/SpringBoot_Valid