参数校验简单体验

目前pig系统的common-core核心包支持JSR-303参数校验,通过spring-boot-starter-validation实现,底层基于Hibernate Validator。

先来看个简单例子。

Java POJO定义中引入注解。@NotBlank就代表了所注解的元素值为字符串且该字符串不为空。

  1. @Data
  2. @EqualsAndHashCode(callSuper = true)
  3. public class SysDept extends Model<SysDept> {
  4. private static final long serialVersionUID = 1L;
  5. private Integer deptId;
  6. /**
  7. * 部门名称
  8. */
  9. @NotBlank(message = "部门名称不能为空")
  10. private String name;
  11. /**
  12. * 父级部门id
  13. */
  14. private Integer parentId;
  15. // 省略部分代码
  16. }

使用的话在Controller 通过@Valid注解激活校验即可。代码如下:

  1. /**
  2. * 添加
  3. * @param sysDept 实体
  4. * @return success/false
  5. */
  6. @SysLog("添加部门")
  7. @PostMapping
  8. @PreAuthorize("@pms.hasPermission('sys_dept_add')")
  9. public R save(@Valid @RequestBody SysDept sysDept) {
  10. return R.ok(sysDeptService.saveDept(sysDept));
  11. }

为什么要做参数验证?

永远不要相信我们在后端接收到的用户数据。

  1. 防止恶意用户通过精心构造的参数破坏我们的系统
  2. 保证我们的业务有序进行

即使前端已经校验过,因为我们不能保证我们收到的请求都是由我们的前端程序发出,所以,后端必须进行参数校验!

Bean Validation 中内置的 constraint

JSR303 规范默认提供了几种约束注解的定义。具体如下表格:

Constraint 详细信息
AssertFalse 被注解的元素必须是否定值。
支持:
- boolean
- Boolean
- null
AssertTrue 被注解的元素必须是肯定值。支持范围同AssertFalse
DecimalMax 被注解的元素必须是一个数字,其值必须小于或等于指定的最大值。支持:
- BigDecimal
- BigInteger
- CharSequence
- byte, short, int, long及其包装类
- null

由于精度范围限制,不支持double,float及其包装类 | | DecimalMin | 被注解的元素必须是一个数字,其值必须大于或等于指定的最小值。支持范围同
DecimalMax | | Digits
| 被注解的元素必须是可接受范围内的数字。支持范围同
DecimalMax | | Email
� | 被注解的元素必须是字符串,必须是格式正确的电子邮件地址。
支持:
- CharSequence
- null
| | Future
� | 被注解的元素必须是未来的某个时刻、日期或时间。
支持:
- java.util.Date
- java.util.Calendar
- java.time.Instant
- java.time.LocalDate
- java.time.LocalDateTime
- java.time.LocalTime
- java.time.MonthDay
- java.time.OffsetDateTime
- java.time.OffsetTime
- java.time.Year
- java.time.YearMonth
- java.time.ZonedDateTime
- java.time.chrono.HijrahDate
- java.time.chrono.JapaneseDate
- java.time.chrono.MinguoDate
- java.time.chrono.ThaiBuddhistDate
- null
| | FutureOrPresent
| 被注解的元素必须是现在或者未来的某个时刻、日期或时间。
支持范围同Future | | Max
� | 被注解的元素必须是一个数字,其值必须小于或等于指定的最大值。
支持:
- BigDecimal
- BigInteger
- byte, short, int, long及其包装类
- null
由于精度范围限制,不支持double,float及其包装类 | | Min
� | 被注解的元素必须是一个数字,其值必须大于或等于指定的最小值。
支持范围同Max | | Negative
| 被注解的元素必须是严格的负数(即 0 被视为无效值)。
支持范围同Max | | NegativeOrZero
� | 被注解的元素必须是负数或 0。
支持范围同Max | | NotBlank
� | 被注解的元素不能为空,并且必须至少包含一个非空白字符。
支持:
- CharSequence
| | NotEmpty
� | 被注解的元素不得为 null 或为空。
支持:
- CharSequence(评估字符序列的长度)
- Collection(评估集合容量)
- Map(评估Map容量)
- Array(评估数组长度)
| | NotNull
� | 被注解的元素不得为 null。
支持任意类型。 | | Null
� | 被注解的元素必须是 null。
支持任意类型。 | | Past
� | 被注解的元素必须是过去的某个时刻、日期或时间。
支持范围同Future | | PastOrPresent
� | 被注解的元素必须是过去或现在的某个时刻、日期或时间。
支持范围同Future | | Pattern
� | 被注解的元素必须匹配指定的正则表达式。正则表达式遵循 Java 正则表达式约定,请参见 java.util.regex.Pattern。
支持:
- CharSequence
- null
| | Positive
� | 被注解的元素必须是严格的正数(即 0 被视为无效值)。
支持范围同Max | | PositiveOrZero
� | 被注解的元素必须是正数或 0。
支持范围同Max | | Size
� | 被注解的元素必须在指定的边界(包括)之间。
支持:
- CharSequence(评估字符序列的长度)
- Collection(评估集合容量)
- Map(评估Map容量)
- Array(评估数组长度)
- null
|

参数校验分组

在实际开发中经常会遇到这种情况:添加部门时,id是由后端生成的,不需要校验id是否为空,但是修改部门时就需要校验id是否为空。如果在接收参数的SysDept实体类的id属性上添加NotNull,显然无法实现。这时候就可以定义分组,在需要校验id的时候校验,不需要的时候不校验。

定义表示组别的接口类

  1. public interface Insert {
  2. }
  1. public interface Update {
  2. }

在实体类的注解中标记id使用上面定义的组

  1. @Data
  2. @EqualsAndHashCode(callSuper = true)
  3. public class SysDept extends Model<SysDept> {
  4. private static final long serialVersionUID = 1L;
  5. @NotNull(groups = Update.class, message = "id不能为空")
  6. private Integer deptId;
  7. /**
  8. * 部门名称
  9. */
  10. @NotBlank(message = "部门名称不能为空")
  11. private String name;
  12. /**
  13. * 父级部门id
  14. */
  15. @NotNull(groups = {Insert.class,Update.class}, message = "id不能为空")
  16. private Integer parentId;
  17. // 省略部分代码
  18. }

在controller中使用@Validated指定使用哪个组

新增时指定Insert,修改时指定Update,这样就会在新增时不对id进行校验,修改时校验id属性是否为空;新增修改都会校验部门父id是否为空。
注意:如果存在其他校验,那么还得必须添加Default.class,否则不会执行其他的校验(如我们案例中的@NotBlank)

  1. /**
  2. * 添加
  3. * @param sysDept 实体
  4. * @return success/false
  5. */
  6. @SysLog("添加部门")
  7. @PostMapping
  8. @PreAuthorize("@pms.hasPermission('sys_dept_add')")
  9. public R save(@Validated({Insert.class,Default.class}) @RequestBody SysDept sysDept) {
  10. return R.ok(sysDeptService.saveDept(sysDept));
  11. }
  12. /**
  13. * 编辑
  14. * @param sysDept 实体
  15. * @return success/false
  16. */
  17. @SysLog("编辑部门")
  18. @PutMapping
  19. @PreAuthorize("@pms.hasPermission('sys_dept_edit')")
  20. public R update(@Validated({Update.class,Default.class}) @RequestBody SysDept sysDept) {
  21. sysDept.setUpdateTime(LocalDateTime.now());
  22. return R.ok(sysDeptService.updateDeptById(sysDept));
  23. }

1.后端如何使用注解优雅的进行参数校验
####1.1 常用注解

  1. @Null(message = "XXXX不能为空") 被注释的元素必须为 null, message尽量要写不然前端不知道是哪个字段
  2. @NotNull(message = "XXXX不能为空") 被注释的元素必须不为 null, message尽量要写不然前端不知道是哪个字段
  3. @Length 被注释的字符串的大小必须在指定的范围内,注意只能用在String 否则会报错, message尽量要写不然前端不知道是哪个字段
  4. @NotEmpty 被注释的字符串的必须非空,注意只能用在String 否则会报错, message尽量要写不然前端不知道是哪个字段
  5. @AssertTrue(message = "XXXX") 被注释的元素必须为 true, message尽量要写不然前端不知道是哪个字段
  6. @AssertFalse 被注释的元素必须为 false
  7. @Min(value=L,message="XXXX") 被注释的元素必须是一个数字,其值必须大于等于指定的最小值, message尽量要写不然前端不知道是哪个字段
  8. @Max(value=L,message="XXXX") 被注释的元素必须是一个数字,其值必须小于等于指定的最小值, message尽量要写不然前端不知道是哪个字段
  9. @DecimalMin(value=L,message="XXXX") 被注释的元素必须是一个数字,其值必须大于等于指定的最小值, message尽量要写不然前端不知道是哪个字段
  10. @DecimalMax(value=L,message="XXXX") 被注释的元素必须是一个数字,其值必须小于等于指定的最大值, message尽量要写不然前端不知道是哪个字段
  11. @Size(max, min) 被注释的元素的大小必须在指定的范围内, message尽量要写不然前端不知道是哪个字段
  12. @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内, message尽量要写不然前端不知道是哪个字段
  13. @Past 被注释的元素必须是一个过去的日期, message尽量要写不然前端不知道是哪个字段
  14. @Future 被注释的元素必须是一个将来的日期, message尽量要写不然前端不知道是哪个字段
  15. @Pattern(value) 被注释的元素必须符合指定的正则表达式, message尽量要写不然前端不知道是哪个字段
  16. @Email 被注释的元素必须是电子邮箱地址, message尽量要写不然前端不知道是哪个字段
  17. @Range 被注释的元素必须在合适的范围内, message尽量要写不然前端不知道是哪个字段
  18. @NotBlank 验证字符串非null,且长度必须大于0,注意只能用在String 否则会报错

然后需要在controller方法体添加@Validated @Valid 不加校验会不起作用
pig 参数校验及技巧 - 图1

1.3 如何扩展(如何自定义校验注解)

(1)定义注解,必须包含message、groups、Payload三个属性

  1. /**
  2. * 边界值校验
  3. * @author Lilu
  4. * @date 2021-7-16 16:57
  5. */
  6. import javax.validation.Constraint;
  7. import javax.validation.Payload;
  8. import java.lang.annotation.Documented;
  9. import java.lang.annotation.Retention;
  10. import java.lang.annotation.Target;
  11. import static java.lang.annotation.ElementType.*;
  12. import static java.lang.annotation.RetentionPolicy.RUNTIME;
  13. @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
  14. @Retention(RUNTIME)
  15. @Documented
  16. @Constraint(validatedBy = { IntegerValidImpl.class})
  17. public @interface IntegerValid {
  18. int max();//最大值
  19. int min();//最小值
  20. String message() default "{你不对劲}";
  21. Class<?>[] groups() default { };
  22. Class<? extends Payload>[] payload() default { };
  23. }

(1)实现接口

  1. import org.springframework.stereotype.Component;
  2. import javax.validation.ConstraintValidator;
  3. import javax.validation.ConstraintValidatorContext;
  4. /**
  5. * @author Lilu
  6. * @date 2021-7-16 16:57
  7. */
  8. /**
  9. * 自定义类,用于对校验注解规则的实现
  10. * 实现 ConstraintValidator 接口,泛型,第一个是对什么注解进行实现,第二个是检验的数据的数据类型 ;
  11. */
  12. @Component
  13. public class IntegerValidImpl implements ConstraintValidator<IntegerValid, Integer> {
  14. private int min;
  15. private int max;
  16. /**
  17. * @Description 初始化
  18. * @Date 2021-7-16 17:09
  19. * @Param [constraintAnnotation]
  20. * @return void
  21. **/
  22. @Override
  23. public void initialize(IntegerValid constraintAnnotation) {
  24. min=constraintAnnotation.min();
  25. max=constraintAnnotation.max();
  26. }
  27. @Override
  28. public boolean isValid(Integer value, ConstraintValidatorContext context) {
  29. System.out.println(value);
  30. if(value>min&&value<max){
  31. return true;
  32. }
  33. return false;
  34. }
  35. }

(3)使用

  1. @ApiOperation(value = "测试接口", notes = "测试接口")
  2. @PostMapping("test")
  3. public ResponseEntity test(@Validated @RequestBody TestBean testBean){
  4. return ResponseEntity.ok(testBean);
  5. }
  6. public static class TestBean{
  7. @IntegerValid(min = 1, max = 20)
  8. private Integer aa;
  9. @IntegerValid(min = 1, max = 20)
  10. private Integer bb;
  11. public Integer getAa() {
  12. return aa;
  13. }
  14. public void setAa(Integer aa) {
  15. this.aa = aa;
  16. }
  17. public Integer getBb() {
  18. return bb;
  19. }
  20. public void setBb(Integer bb) {
  21. this.bb = bb;
  22. }
  23. }

1.3 分组如何使用

groups可以指定注解使用的场景,一个实体类可能会在多个场合有使用,如插入,删除等。通过groups可以指定该注解在插入/删除的环境下生效。
payload往往对bean进行使用。

例子

(1)定义group最后可以再定义两个接口作为group,代表两种不同的操作。

  1. /**
  2. * @author Lilu
  3. * @date 2021-7-16 17:45
  4. */
  5. public interface Insert {
  6. }
  1. /**
  2. * @author Lilu
  3. * @date 2021-7-16 17:45
  4. */
  5. public class Update {
  6. }

(2)使用

  1. import com.jc.purchase.annotation.IntegerValid;
  2. import com.jc.purchase.annotation.Update;
  3. import io.swagger.annotations.Api;
  4. import io.swagger.annotations.ApiOperation;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7. import org.springframework.http.ResponseEntity;
  8. import org.springframework.validation.annotation.Validated;
  9. import org.springframework.web.bind.annotation.*;
  10. import java.awt.*;
  11. /**
  12. * @author Lilu
  13. * @date 2021-7-16 17:45
  14. */
  15. @Api(tags = "通用接口")
  16. @RestController
  17. @RequestMapping("/api/se/general")
  18. public class GeneralController {
  19. private final Logger log = LoggerFactory.getLogger(GeneralController.class);
  20. @ApiOperation(value = "测试新增")
  21. @PostMapping("testAdd")
  22. public ResponseEntity add(@Validated(Insets.class) @RequestBody TestBean testBean){
  23. return ResponseEntity.ok(testBean);
  24. }
  25. @ApiOperation(value = "测试修改")
  26. @PutMapping("testPut")
  27. public ResponseEntity put(@Validated(Update.class) @RequestBody TestBean testBean){
  28. return ResponseEntity.ok(testBean);
  29. }
  30. public static class TestBean{
  31. @IntegerValid(min = 1, max = 20,groups = Update.class)//修改时验证生效
  32. private Integer aa;
  33. @IntegerValid(min = 1, max = 20,groups = Insets.class)//新建时验证生效
  34. private Integer bb;
  35. public Integer getAa() {
  36. return aa;
  37. }
  38. public void setAa(Integer aa) {
  39. this.aa = aa;
  40. }
  41. public Integer getBb() {
  42. return bb;
  43. }
  44. public void setBb(Integer bb) {
  45. this.bb = bb;
  46. }
  47. }
  48. }

2.全局异常响应码封装

2.1 后端代码

pig 参数校验及技巧 - 图2

2.2 验证未通过请求响应结果

  1. {
  2. status: 400,
  3. code:1,
  4. msg:""
  5. }

❤ 问题咨询

手势点击蓝字求关注简约风动态引导关注__2022-09-07+23_18_38.gif