springboot参数校验validation

1. 引入

在项目中,大部分的工作就是前后端请求的交互,接口的编写。

接口编写就不得不做很多的参数校验,通常在业务代码之前,就要做很多很多的显示参数校验,造成代码冗余。

springboot-validation提供了优雅的参数校验,入参通常都是实体类,在实体类字段上加上对应的注解,就可以在方法之前进行参数校验,校验不通过,是不会进入方法的

springboot-validation的底层是hibernate-validation

2. 基本使用

本文对应的源码地址:04-spring-boot-validator · master · csdn / spring-boot-csdn · GitLab (sea-clouds.cn)

2.1 引入依赖

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.5.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- springboot validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
        <version>2.10.8</version>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.6.0</version>
    </dependency>
</dependencies>

2.2 基本使用

以登录接口为例,需要登录用户vo,包含登录需要的用户名和密码,要求都不能为空

LoginUserVo

/**
 * @author HLH
 * @description 登录接口需要的用户名密码封装的vo
 * @email 17703595860@163.com
 * @date Created in 2021/8/10 下午10:18
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUserVo implements Serializable {

    private static final long serialVersionUID = -5331320733431220933L;

    @NotBlank(message = "用户名不能为空")   // 非空,message为错误的提示信息
    private String username;
    @NotBlank(message = "密码不能为空")   // 非空
    private String passwrod;

}

UserController

/**
 * @author HLH
 * @description 用户模拟接口
 * @email 17703595860@163.com
 * @date Created in 2021/8/10 下午10:20
 */
@RestController
@RequestMapping("/user")
public class UserController {

    /**
     * 加上@Validated 注解,会自动对LoginUserVo中加了注解的字段进行校验
     */
    @PostMapping("/login")
    public String login(@Validated LoginUserVo loginUserVo) {
        return "登录成功";
    }

}

测试

使用PostMan进行测试

Post /user/login

正常输入

image-20210810223200992

如果留空,就会报错

image-20210810223237462

image-20210810223302562

2.3 注解介绍

内置校验注解包含很多,包括如下

注解校验功能
@Null被注释的元素必须为 null
@NotNull被注释的元素必须不为 null
@NotBlank验证字符串非null,且长度必须大于0
@NotEmpty被注释的字符串的必须非空
@Length(min=,max=)被注释的字符串的大小必须在指定的范围内
@Size(max=, min=)被注释的元素的大小必须在指定的范围内
@Range(min=,max=,message=)被注释的元素必须在合适的范围内
@Email被注释的元素必须是电子邮箱地址
@AssertTrue被注释的元素必须为 true
@AssertFalse被注释的元素必须为 false
@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Digits(integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past被注释的元素必须是一个过去的日期
@Future被注释的元素必须是一个将来的日期
@FutureOrPresent当前或将来时间
@Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式
@Negative负数(不包括0)
@NegativeOrZero负数或0
@PositiveOrZero正数或0

2.4 返回值完善

刚刚的案例大家也看到了,如果没有一个统一的返回值格式,返回的数据是不容易读,并且不容易解析的。所以需要构造统一的返回结果

Result

/**
 * @author HLH
 * @description 统一的返回结果
 * @email 17703595860@163.com
 * @date Created in 2021/8/10 下午10:45
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Result<T> {

    private int status;
    private T data;
    private String errorMsg;

}

ResultBuilder

/**
 * @author HLH
 * @description 返回值构造
 * @email 17703595860@163.com
 * @date Created in 2021/8/10 下午10:47
 */
public interface ResultBuilder {

    /**
     * 成功的构造
     * @param data 数据
     * @return Result
     */
    default Result<?> success(Object data) {
        return Result.builder()
                .status(HttpStatus.OK.value()).data(data)
                .build();
    }

    /**
     * 404的构造
     * @param errorMsg 错误信息
     * @return Result
     */
    default Result<?> notFound(String errorMsg) {
        return Result.builder()
                .status(HttpStatus.NOT_FOUND.value())
                .errorMsg(errorMsg)
                .build();
    }

    /**
     * 500的构造
     * @param errorMsg 错误信息
     * @return Result
     */
    default Result<?> internalServerError(String errorMsg) {
        return Result.builder()
                .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
                .errorMsg(errorMsg)
                .build();
    }

}

UserController改造

/**
 * @author HLH
 * @description 用户模拟接口
 * @email 17703595860@163.com
 * @date Created in 2021/8/10 下午10:20
 */
@RestController
@RequestMapping("/user")
public class UserController implements ResultBuilder {

    /**
     * 加上@Validated 注解,会自动对LoginUserVo中加了注解的字段进行校验
     */
    @PostMapping("/login")
    public Result<?> login(@RequestBody @Validated LoginUserVo loginUserVo) {
        return success("登录成功");
    }

}

2.5 统一异常处理

刚刚看到,校验错误的异常只会在控制台打印。返回值只有400的提示,没有明显的自定义提示

/**
 * @author HLH
 * @description 统一异常处理类
 * @email 17703595860@163.com
 * @date Created in 2021/8/10 下午10:56
 */
@RestControllerAdvice  // 返回json
public class GlobExceptionHandler implements ResultBuilder {

    @ExceptionHandler(value = BindException.class)
    public Result<?> bindExceptionHandler(BindException ex) {
        // 获取所有错误信息,拼接
        List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
        String errorMsg = fieldErrors.stream()
                .map(fieldError -> fieldError.getField() + ":" + fieldError.getDefaultMessage())
                .collect(Collectors.joining(","));
        // 返回统一处理类
        return internalServerError(errorMsg);
    }

}

再次访问接口,可以看到明显的错误提示

image-20210810230605992

2.6 @Valid和@Validate注解

基本介绍

@Valid与@Validated都是用来校验接收参数的。

@Valid是使用Hibernate validation的时候使用

@Validated是只用Spring Validator校验机制使用

区别

  • 分组校验

    • 使用@Valid 注解不支持分组验证

    • @Validated则支持分组验证。

  • 嵌套验证

    • @Valid:可以用在类、方法、构造函数、方法参数和成员属性(字段)上,支持嵌套检测

      @Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
      @Retention(RUNTIME)
      @Documented
      public @interface Valid {
      }
      
    • @Validated:可以用在类、方法和方法参数上。但是不能用在成员属性(字段)上,不支持嵌套检测

      @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface Validated {
      
         Class<?>[] value() default {};
      
      }
      

3. 分组校验

如果同一个参数,在不同的场景下应用不同的校验规则,那么就需要用到分组校验了,再不同的场景下,使用不同的分组。

比如:用户新注册的时候,还没有起名字,可以允许nickName为空,由系统自动生产。但是如果更新用户信息,那么需要保证nickName不为空

实现步骤

  • 定义一个分组类
  • 再校验注解上添加goroups属性指定分组
  • Controlelr方法的@Validated注解添加分组类

UpdateUser

/**
 * @author HLH
 * @description 更新用户使用的分组
 * @email 17703595860@163.com
 * @date 2021/8/11 上午9:23
 */
public interface UpdateUser extends Default {
}

UserVo

/**
 * @author HLH
 * @description 用户vo,用于新增和修改用户
 * @email 17703595860@163.com
 * @date 2021/8/11 上午9:22
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserVo implements Serializable {

    @NotBlank(message = "更新记录id不能为空", groups = UpdateUser.class) // 更新是保证ID不为空,根据ID更新
    private String id;
    @NotBlank(message = "用户名不能为空")
    private String username;
    @NotBlank(message = "密码不能为空")
    private String password;
    @NotBlank(message = "昵称不能为空", groups = UpdateUser.class) // 更新时保证nickName不为空
    private String nickName;

}

UserController改造

/**
 * @author HLH
 * @description 用户模拟接口
 * @email 17703595860@163.com
 * @date Created in 2021/8/10 下午10:20
 */
@RestController
@RequestMapping("/user")
public class UserController implements ResultBuilder {

    /**
     * 加上@Validated 注解,会自动对LoginUserVo中加了注解的字段进行校验
     */
    @PostMapping("/login")
    public Result<?> login(@RequestBody @Validated LoginUserVo loginUserVo) {
        return success("登录成功");
    }

    /**
     * 新增用户,使用默认的分组校验
     */
    @PostMapping
    public Result<?> insertUser(@RequestBody @Validated UserVo userVo) {
        return success("新增成功");
    }

    /**
     * 更新用户时使用 更新用户 的校验分组
     *  包含分组内的校验规则和默认的校验规则
     */
    @PutMapping
    public Result<?> updateUser(@RequestBody @Validated(UpdateUser.class) UserVo userVo) {
        return success("新增成功");
    }

}

4. 递归校验

如果一个对象中包含另外一个对象,而且内层对象也需要校验,此时就会用到递归校验。再需要校验的内部对象上加注解@Valid即可实现,对集合对象同样使用

比如,添加订单,订单包含订单主表和订单详情表,要求订单主表的name和订单详情表中的商品name不能为空,而且必须同时满足

实现流程

  • 准备OrderVoOrderDetailVo
  • OrderVo上加上校验注解,完成一层属性校验
  • OrderVo中的OrderDetailVo上加上@valid注解,并且在OrderDetailVo的属性上加上校验注解
  • 编写Controller测试,即可完成递归校验

OrderVo

/**
 * @author HLH
 * @description 订单vo
 * @email 17703595860@163.com
 * @date 2021/8/11 上午9:37
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderVo implements Serializable {
    private static final long serialVersionUID = 8015529308743266490L;

    private String id;
    @NotBlank(message = "订单名称不能为空")
    private String orderName;
    private Double price;

    @Valid
    private List<OrderDetailVo> orderDetailVoList;

}

OrderDetailVo

/**
 * @author HLH
 * @description 订单详情vo
 * @email 17703595860@163.com
 * @date 2021/8/11 上午9:43
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderDetailVo implements Serializable {

    private static final long serialVersionUID = 8529818233485384618L;

    private String id;
    private String orderId;


    @NotBlank(message = "订单中商品id不能为空")
    private String goodsId;
    @NotBlank(message = "订单中商品名称不能为空")
    private String goodsName;
    private String goodsPrice;

}

OrderController

/**
 * @author HLH
 * @description 订单模拟Controller
 * @email 17703595860@163.com
 * @date 2021/8/11 上午9:46
 */
@RestController
@RequestMapping("/order")
public class OrderController implements ResultBuilder {

    @PostMapping
    public Result<?> insertOrder(@RequestBody @Validated OrderVo orderVo) {
        return success("新增订单成功");
    }

}

测试

Post /order 内部对象数据不完整,正常跑出错误

image-20210811095509249

5. 自定义注解校验

SpringBoot的validation为我们提供了很多的校验规则注解,几乎可以满足我们日常开发中的绝大多数场景。但是,再特殊场景下,我们还是需要自定义一些校验规则注解,实现自定一校验。

一个好的框架一定是方便扩展的。SpringValidation允许用户自定义校验。

实现步骤

  • 自定义校验注解
  • 编写校验处理类

场景:

登录用户的密码必须是4-16位,并且必须同时包含数字,大写字母,小写,特殊字符

5.1 自定义校验注解

PasswordPatten

/**
 * @author HLH
 * @description 密码校验注解 模仿@NotBlank写就可以
 * @email 17703595860@163.com
 * @date 2021/8/11 上午10:01
 */
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(PasswordPatten.List.class)
@Constraint(validatedBy = {PasswordPattenValidator.class})
public @interface PasswordPatten {

    String message() default "密码格式错误,必须位4-16位,并且包含数字,大写字母,小写字母,特殊字符";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        PasswordPatten[] value();
    }

}

5.2 自定义检验处理类

PasswordPattenValidator

/**
 * @author HLH
 * @description 实现ConstraintValidator类,重写isValid校验方法
 * @email 17703595860@163.com
 * @date 2021/8/11 上午10:08
 */
public class PasswordPattenValidator implements ConstraintValidator<PasswordPatten, String> {

    /**
     * 重写校验方法
     * @param value 值
     * @param context 上下文
     * @return 是否校验通过,true通过,false不通过
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (StringUtils.isBlank(value)) {
            return false;
        }
        return validatePassword(value);
    }

    /**
     * 校验方法,校验规则
     * @param password 值
     * @return 是否符合规则,true通过,false不通过
     */
    private boolean validatePassword(String password) {
        return StringUtils.containsAny(password, "-_+=,.?~!@#$%^&*()")
                && StringUtils.containsAny(password, "abcdefghijklmnopqrstuvwxyz")
                && StringUtils.containsAny(password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
                && StringUtils.containsAny(password, "0123456789");
    }
}

5.3 测试

LoginUserVo

/**
 * @author HLH
 * @description 登录接口需要的用户名密码封装的vo
 * @email 17703595860@163.com
 * @date Created in 2021/8/10 下午10:18
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUserVo implements Serializable {

    private static final long serialVersionUID = -5331320733431220933L;

    @NotBlank(message = "用户名不能为空")   // 非空,message为错误的提示信息
    private String username;
    @NotBlank(message = "密码不能为空")   // 非空
    @PasswordPatten // 密码自定义校验
    private String password;

}

UserController修改登录接口

/**
 * 加上@Validated 注解,会自动对LoginUserVo中加了注解的字段进行校验
 */
@PostMapping("/login")
public Result<?> login(@RequestBody @Validated LoginUserVo loginUserVo) {
    return success("登录成功");
}

PostMan测试

Post /user/login 错误密码示例

image-20210811123146887

6. 手动触发校验

标准的调用是在方法入参的时候,会自动校验实体类中的所有属性。

但是很多时候,我们想在任何时候,任何场景,都可以手动的校验某个实体类,或者某个实体类中的某些字段,这个时候,就需要编写一个工具类,来实现这个功能

ValidatorUtil

package xyz.hlh.boot4.util;
 
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.groups.Default;
import java.util.Set;

/**
 * @author HLH
 * @description: 校验工具类
 *      在非Controller调用的时候,手动调用SpringBoot的校验。校验类或者属性
 * @email 17703595860@163.com
 * @date : Created in 2022/6/22 22:35
 */
public class ValidatorUtil {

    /** 多个属性之间的连接符 */
    private static final String SEPARATOR = ", ";

    /**
     * 抛出异常。可以根据实际情况抛出指定异常
     * @param msg 拼装的返回错误信息
     */
    private static void throwException(String msg) {
        throw new RuntimeException(msg);
    }

    /**
     * 按照指定的格式拼装返回格式
     * @param constraintViolations 校验的返回值
     * @return 拼装完的结果字符串
     */
    private static String buildErrorMsg(Set<ConstraintViolation<Object>> constraintViolations) {
        StringBuilder msgBuild = new StringBuilder();
        for (ConstraintViolation<Object> constraintViolation : constraintViolations) {
            String field = constraintViolation.getPropertyPath().toString();
            String message = constraintViolation.getMessage();
            msgBuild.append(field).append(": ").append(message).append(SEPARATOR);
        }
        return msgBuild.substring(0, msgBuild.length() - SEPARATOR.length());
    }


    /**
     * 校验某个对象
     * @param o 要校验的对象
     */
    public static void validate(Object o) {
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(o, Default.class);
        if (!constraintViolations.isEmpty()) {
            String msg = buildErrorMsg(constraintViolations);
            throwException(msg);
        }
    }

    /**
     * 校验某个对象的某些字段
     * @param o 要校验的对象
     * @param validateFields 要检验的字段
     */
    public static void validate(Object o, String[] validateFields) {
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        StringBuilder msgBuild = new StringBuilder();
        for (String fieldName : validateFields) {
            Set<ConstraintViolation<Object>> constraintViolations = validator.validateProperty(o, fieldName, Default.class);
            if (!constraintViolations.isEmpty()) {
                String msg = buildErrorMsg(constraintViolations);
                msgBuild.append(msg).append(SEPARATOR);
            }
        }
        if(msgBuild.length() > 0){
            throwException(msgBuild.substring(0, msgBuild.length() - SEPARATOR.length()));
        }
    }

    /**
     * 分组校验对象
     * @param o 要校验的对象
     * @param groups 分组
     */
    public static void validate(Object o, Class<?> ... groups) {
 
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(o, groups);
        if (!constraintViolations.isEmpty()) {
            String msg = buildErrorMsg(constraintViolations);
            throw new RuntimeException(msg);
        }
 
    }

    /**
     * 校验某个对象的某些字段(指定分组)
     * @param o 要校验的对象
     * @param validateFields 要检验的字段
     * @param groups 分组
     */
    public static void validate(Object o, String[] validateFields, Class<?> ... groups) {
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        StringBuilder msgBuild = new StringBuilder();
        for (String fieldName : validateFields) {
            Set<ConstraintViolation<Object>> constraintViolations = validator.validateProperty(o, fieldName, groups);
            if (!constraintViolations.isEmpty()) {
                String msg = buildErrorMsg(constraintViolations);
                msgBuild.append(msg).append(SEPARATOR);
            }
        }
        if(msgBuild.length() > 0){
            throwException(msgBuild.substring(0, msgBuild.length() - SEPARATOR.length()));
        }
    }
}

7. 最后再来一张思维导图

Validation导图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值