SpringBoot参数验证(三十七)

我喜欢你,可是你却并不知道.

上一章简单介绍了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>

二. Valid 和 Validation 参数验证

二.一 Valid 参数校验

二.一.一 @Valid 验证

@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 对象打印时:

SpringBoot参数验证(三十七)_第1张图片

使用 FieldError 对象打印时:

SpringBoot参数验证(三十七)_第2张图片

一般会使用 FieldError

二.二 @Validated 验证

二.二.一 验证 Url

    @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 {

}

SpringBoot参数验证(三十七)_第3张图片

这是抛出异常的, 在参数上进行处理, 很不友好。 可以通过 实体类进行封装。

二.二.二 实体封装

    @PostMapping("valid4")
    public String valid4(@RequestBody @Validated User user) {
        return "验证正确" +user.getName();
    }

不需要在 Controller 上添加 @Validated 注解。

User 对象验证跟之前的保持一致.

这个时候 打印是这样

SpringBoot参数验证(三十七)_第4张图片

二.三 嵌套校验

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();
        }
    }

SpringBoot参数验证(三十七)_第5张图片

二.四 @Valid 和 @Validated 的区别

@Valid是属于javax.validation, @Validated 属于 Spring 的 org.springframework.validation.annotation

可以将@Validated看做是@Valid的升级版,属于是HibernateValid的封装应用。

SpringBoot参数验证(三十七)_第6张图片

二.四.一 @Valid

如果写了bindingResult接收错误信息,但是业务代码没有写处理逻辑的话,即没有判断bindingResult.hasErrors(),则会跳过这个@Valid,什么都不判断。

如果没有写bindingResult的话,只写@Valid的话,还是会进行判断的

  @PostMapping("valid5")
    public String valid5(@RequestBody @Valid User user) {
        return "验证正确" +user.getName();
    }

SpringBoot参数验证(三十七)_第7张图片

如果是 嵌套校验的话, 必须使用 @Valid

二.四.二 @Validated

使用@Validated不需要BindingResult,直接在参数前使用@Validated即可,效果是一样的,遇到校验出错会抛出异常

二.四.三 注解总结

  1. @Valid 和 @Validated 两者都可以对数据进行校验,待校验字段上打的规则注解(@NotNull, @NotEmpty等)都可以对 @Valid 和 @Validated 生效;
  2. @Valid 进行校验的时候,需要用 BindingResult 来做一个校验结果接收。当校验不通过的时候,如果手动不 return ,则并不会阻止程序的执行;
  3. @Validated 进行校验的时候,当校验不通过的时候,程序会抛出400异常,阻止方法中的代码执行,这时需要再写一个全局校验异常捕获处理类,然后返回校验提示
  4. 总体来说,@Validated 使用起来要比 @Valid 方便一些,它可以帮我们节省一定的代码,并且使得方法看上去更加的简洁

三. 验证优化

使用 @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);
    }
}

当失败时, 捕获到异常

SpringBoot参数验证(三十七)_第8张图片

三.二 快速失败

上面可以发现, 当某个属性验证失败后,会继续验证,并不会遇到第一个错误后就返回。

@Component
public class ValidationConfig {
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 快速失败模式
                .failFast(true)
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}

SpringBoot参数验证(三十七)_第9张图片

四. 分组验证

我们定义一个实体 进行接收时, 通过 添加操作 是不需要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;
}

四.三 Controller 层验证

 @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 "删除成功";
    }

SpringBoot参数验证(三十七)_第10张图片

SpringBoot参数验证(三十七)_第11张图片

SpringBoot参数验证(三十七)_第12张图片

也可以 让 自定义的组 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;

五.一 自定义 @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();
    }
}

五.二 编写编码校验器 PasswordValidator

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();
    }
}

这样, 当密码不符合时,就会进行提示。

SpringBoot参数验证(三十七)_第13张图片

五.三 编写常见的性别 验证器

五.三.一 @Sex 注解

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 {}; }

五.三.二 性别验证器 SexValidator

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

你可能感兴趣的:(SpringBoot,spring,boot,参数验证)