# SpringBoot参数校验 **Repository Path**: jianml/validation ## Basic Information - **Project Name**: SpringBoot参数校验 - **Description**: SpringBoot参数校验 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 8 - **Forks**: 11 - **Created**: 2020-01-04 - **Last Updated**: 2023-09-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringBoot参数校验 ## 前言 在日常开发写rest接口时,接口参数校验这一部分是必须的,但是如果全部用代码去做,显得十分麻烦,spring也提供了这部分功能 ## 注解介绍 ### validator内置注解 | **注解** | **详细信息** | | :---------------------------- | :------------------------------------------------------- | | `@Null` | 被注释的元素必须为 `null` | | `@NotNull` | 被注释的元素必须不为 `null` | | `@AssertTrue` | 被注释的元素必须为 `true` | | `@AssertFalse` | 被注释的元素必须为 `false` | | `@Min(value)` | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | | `@Max(value)` | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | | `@DecimalMin(value)` | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | | `@DecimalMax(value)` | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | | `@Size(max, min)` | 被注释的元素的大小必须在指定的范围内 | | `@Digits (integer, fraction)` | 被注释的元素必须是一个数字,其值必须在可接受的范围内 | | `@Past` | 被注释的元素必须是一个过去的日期 | | `@Future` | 被注释的元素必须是一个将来的日期 | | `@Pattern(value)` | 被注释的元素必须符合指定的正则表达式 | ### Hibernate Validator 附加的 constraint | **注解** | **详细信息** | | :---------- | :------------------------------------- | | `@Email` | 被注释的元素必须是电子邮箱地址 | | `@Length` | 被注释的字符串的大小必须在指定的范围内 | | `@NotEmpty` | 被注释的字符串的必须非空 | | `@Range` | 被注释的元素必须在合适的范围内 | | `@NotBlank` | 验证字符串非null,且长度必须大于0 | **注意**: - `@NotNull `适用于任何类型被注解的元素必须不能与NULL - `@NotEmpty `适用于String Map或者数组不能为Null且长度必须大于0 - `@NotBlank` 只能用于String上面 不能为null,调用trim()后,长度必须大于0 ## 异常处理 验证不通过会产生异常,因为我们项目提供rest接口,所以通过全局捕获异常,然后转换为Json给前台 > 全局异常处理可以参考 https://gitee.com/jianml/exception ## 方法参数校验 ```java @GetMapping("/validate1") public String validate1( @Size(min = 1, max = 10, message = "姓名长度必须为1到10") @RequestParam("name") String name, @Min(value = 10, message = "年龄最小为10") @Max(value = 100, message = "年龄最大为100") @RequestParam("age") Integer age, @Future @RequestParam("birth") @DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss") Date birth){ return "validate1"; } ``` 注意类名需要加注解`@Validated` 校验失败会抛出`ConstraintViolationException`异常 然后我们在全局异常捕获类捕获这个异常,返回给前台对应的错误Json ```java /** * 统一处理请求参数校验(普通传参) */ @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public WebResult handleConstraintViolationException(ConstraintViolationException e){ log.info("普通参数异常:{}", e.getMessage()); StringBuilder msgBuilder = new StringBuilder(); Set> violations = e.getConstraintViolations(); for (ConstraintViolation violation : violations) { Path path = violation.getPropertyPath(); String[] pathArr = path.toString().split("\\."); msgBuilder.append(pathArr[1]).append(violation.getMessage()).append(","); } msgBuilder = new StringBuilder(msgBuilder.substring(0, msgBuilder.length() - 1)); return new WebResult().msg(msgBuilder.toString()); } ``` ## 表单对象/VO对象校验 当参数是VO时,可以在VO类的属性上添加校验注解。 ```java @Data public class User { @Size(min = 1, max = 10, message = "姓名长度必须为1到10") private String name; @Min(value = 10, message = "年龄最小为10") @Max(value = 100, message = "年龄最大为100") private Integer age; @Future @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date birth; } ``` 其中,`Future`注解要求必须是相对当前时间来讲“未来的”某个时间。 在`controller`对应`User`实体前增加`@Valid`注解 ```java @PostMapping("/validate2") public User validate2(@Valid @RequestBody User user){ return user; } ``` 如果校验不合法会出现`MethodArgumentNotValidException`异常,可以在`ControllerAdvice` 做全局异常处理 ```java /** * 统一处理请求参数校验(被@RequestBody注解的实体对象传参) */ @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public WebResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e){ log.info("实体对象参数异常:{}", e.getMessage()); StringBuilder msgBuilder = new StringBuilder(); List fieldErrors = e.getBindingResult().getFieldErrors(); for (FieldError error : fieldErrors) { msgBuilder.append(error.getField()).append(error.getDefaultMessage()).append(","); } msgBuilder = new StringBuilder(msgBuilder.substring(0, msgBuilder.length() - 1)); return new WebResult().msg(msgBuilder.toString()); } ``` 如果实体对象没有加`@RequestBody`注解,则会出`BindException` 异常 ```java /** * 统一处理请求参数校验(普通实体对象传参) */ @ExceptionHandler(BindException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public WebResult handleBindException(BindException e){ log.info("实体对象参数异常:{}", e.getMessage()); StringBuilder msgBuilder = new StringBuilder(); List fieldErrors = e.getBindingResult().getFieldErrors(); for (FieldError error : fieldErrors) { msgBuilder.append(error.getField()).append(error.getDefaultMessage()).append(","); } msgBuilder = new StringBuilder(msgBuilder.substring(0, msgBuilder.length() - 1)); return new WebResult().msg(msgBuilder.toString()); } ``` ## 自定义校验规则 ### 创建约束注解类 ```java @Target({ElementType.PARAMETER, ElementType.FIELD}) @Documented @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = SexValidator.class) public @interface IsSex { String values(); String message() default "sex必须是预定义的那几个值,不能随便写"; Class[] groups() default {}; Class[] payload() default {}; } ``` 注意:`message`用于显示错误信息这个字段是必须的,`groups`和`payload`也是必须的 `@Constraint(validatedBy = { SexValidator.class})`用来指定处理这个注解逻辑的类 ### 创建验证器类 ```java public class SexValidator implements ConstraintValidator { /** * IsSex注解规定的那些有效值 */ private String values; @Override public void initialize(IsSex constraintAnnotation) { this.values = constraintAnnotation.values(); } /** * 用户输入的值,必须是IsSex注解规定的那些值其中之一。 * 否则,校验不通过。 * @param o 用户输入的值,如从前端传入的某个值 */ @Override public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) { String[] sexs = values.split(","); for(String sex : sexs){ if(sex.equals(o)) return true; } return false; } } ``` ### 测试 ```java @GetMapping("/validate3") public String validate3( @Size(min = 1, max = 10, message = "姓名长度必须为1到10") @RequestParam("name") String name, @Min(value = 10, message = "年龄最小为10") @Max(value = 100, message = "年龄最大为100") @RequestParam("age") Integer age, @Future @RequestParam("birth") @DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss") Date birth, @IsSex(values = "0,1") @RequestParam("sex") String sex){ return "validate3"; } ``` ## 分组校验 在实际开发中经常会遇到这种情况:想要用一个实体类去接收多个`controller`的参数,但是不同controller所需要的参数又有些许不同,而你又不想为这点不同去建个新的类接收参数。比如有一个`/addRole`接口不需要`id`参数,而`/updateRole`接口又需要该参数,这种时候就可以使用参数分组来实现 ### 创建用于分组的接口 ```java //什么都不需要写,springboot根据类去分组校验 //添加角色校验 public interface Add {} //修改角色校验 public interface Update {} ``` ### 创建一个测试分组的实体类 ```java @Data public class Role { // 修改角色时,必须要有id @NotNull(message = "修改角色必须要有id", groups = Update.class) private Long id; // 添加角色时必须要有name @NotNull(message = "添加角色时必须要有name", groups = Add.class) // 添加修改角色都需要name的长度在1-10 @Length(min = 1, max = 10, message = "名称不合法", groups = {Add.class, Update.class}) private String name; } ``` ### 创建测试controller ```java @RestController public class RoleController { @PostMapping("/addRole") public WebResult addRole(@Validated(Add.class) Role role){ return new WebResult().msg("添加成功"); } @PutMapping("/updateRole") public WebResult updateRole(@Validated(Update.class) Role role){ return new WebResult().msg("修改成功"); } } ``` ## 级联参数校验 如果要验证属性关联的对象,那么需要在属性上添加`@Valid`注解,如果一个对象被校验,那么它的所有的标注了`@Valid`的关联对象都会被校验,这些对象也可以是数组、集合、Map等,这时会验证他们持有的所有元素。 ### 新建`Book`实体类 ```java @Data public class Book { @NotBlank(message = "name不允许为空") @Length(min = 2, max = 10, message = "name的长度必须在{min}-{max}之间") private String name; @Valid private User author; } ``` ### 测试 ```java @GetMapping("/validate4") public String validate4(@Valid @RequestBody Book book){ return "validate4"; } ``` ## 同时校验2个或更多个字段/参数 常见的场景之一是,查询某信息时要输入开始时间和结束时间。显然,结束时间要≥开始时间。可以在查询VO类上使用自定义注解 ### 新建`Coupon`类 ```java @Data public class Coupon { @NotBlank(message = "name不允许为空") @Length(min = 2, max = 10, message = "name的长度必须在{min}-{max}之间") private String name; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date releaseStartTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date releaseEndTime; } ``` ### 创建约束注解类 ```java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = CheckTimeIntervalValidator.class) public @interface CheckTimeInterval { String startTime() default "from"; String endTime() default "to"; String message() default "开始时间不能大于结束时间"; Class[] groups() default {}; Class[] payload() default {}; } ``` ### 创建验证器类 ```java public class CheckTimeIntervalValidator implements ConstraintValidator { private String startTime; private String endTime; @Override public void initialize(CheckTimeInterval constraintAnnotation) { this.startTime = constraintAnnotation.startTime(); this.endTime = constraintAnnotation.endTime(); } @Override public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) { if(null == o) return true; BeanWrapper beanWrapper = new BeanWrapperImpl(o); Object start = beanWrapper.getPropertyValue(startTime); Object end = beanWrapper.getPropertyValue(endTime); if(null == start || null == end) return true; int result = ((Date) end).compareTo((Date) start); return result >= 0; } } ``` ### 在Coupon类上加`@CheckTimeInterval`注解 ```java @CheckTimeInterval(startTime = "releaseStartTime", endTime = "releaseEndTime", message = "发放开始时间不能大于发放结束时间") @Data public class Coupon { @NotBlank(message = "name不允许为空") @Length(min = 2, max = 10, message = "name的长度必须在{min}-{max}之间") private String name; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date releaseStartTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date releaseEndTime; } ``` ### 测试 ```java @GetMapping("/validate5") public String validate5(@Valid @RequestBody Coupon coupon){ return "validate5"; } ``` > 源码地址:https://gitee.com/jianml/validation