参数校验简单体验
目前pig系统的common-core核心包支持JSR-303参数校验,通过spring-boot-starter-validation实现,底层基于Hibernate Validator。
先来看个简单例子。
Java POJO定义中引入注解。@NotBlank就代表了所注解的元素值为字符串且该字符串不为空。
@Data@EqualsAndHashCode(callSuper = true)public class SysDept extends Model<SysDept> {private static final long serialVersionUID = 1L;private Integer deptId;/*** 部门名称*/@NotBlank(message = "部门名称不能为空")private String name;/*** 父级部门id*/private Integer parentId;// 省略部分代码}
使用的话在Controller 通过@Valid注解激活校验即可。代码如下:
/*** 添加* @param sysDept 实体* @return success/false*/@SysLog("添加部门")@PostMapping@PreAuthorize("@pms.hasPermission('sys_dept_add')")public R save(@Valid @RequestBody SysDept sysDept) {return R.ok(sysDeptService.saveDept(sysDept));}
为什么要做参数验证?
永远不要相信我们在后端接收到的用户数据。
- 防止恶意用户通过精心构造的参数破坏我们的系统
- 保证我们的业务有序进行
即使前端已经校验过,因为我们不能保证我们收到的请求都是由我们的前端程序发出,所以,后端必须进行参数校验!
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的时候校验,不需要的时候不校验。
定义表示组别的接口类
public interface Insert {}
public interface Update {}
在实体类的注解中标记id使用上面定义的组
@Data@EqualsAndHashCode(callSuper = true)public class SysDept extends Model<SysDept> {private static final long serialVersionUID = 1L;@NotNull(groups = Update.class, message = "id不能为空")private Integer deptId;/*** 部门名称*/@NotBlank(message = "部门名称不能为空")private String name;/*** 父级部门id*/@NotNull(groups = {Insert.class,Update.class}, message = "id不能为空")private Integer parentId;// 省略部分代码}
在controller中使用@Validated指定使用哪个组
新增时指定Insert,修改时指定Update,这样就会在新增时不对id进行校验,修改时校验id属性是否为空;新增修改都会校验部门父id是否为空。
注意:如果存在其他校验,那么还得必须添加Default.class,否则不会执行其他的校验(如我们案例中的@NotBlank)
/*** 添加* @param sysDept 实体* @return success/false*/@SysLog("添加部门")@PostMapping@PreAuthorize("@pms.hasPermission('sys_dept_add')")public R save(@Validated({Insert.class,Default.class}) @RequestBody SysDept sysDept) {return R.ok(sysDeptService.saveDept(sysDept));}/*** 编辑* @param sysDept 实体* @return success/false*/@SysLog("编辑部门")@PutMapping@PreAuthorize("@pms.hasPermission('sys_dept_edit')")public R update(@Validated({Update.class,Default.class}) @RequestBody SysDept sysDept) {sysDept.setUpdateTime(LocalDateTime.now());return R.ok(sysDeptService.updateDeptById(sysDept));}
1.后端如何使用注解优雅的进行参数校验
####1.1 常用注解
@Null(message = "XXXX不能为空") 被注释的元素必须为 null, message尽量要写不然前端不知道是哪个字段@NotNull(message = "XXXX不能为空") 被注释的元素必须不为 null, message尽量要写不然前端不知道是哪个字段@Length 被注释的字符串的大小必须在指定的范围内,注意只能用在String上 否则会报错, message尽量要写不然前端不知道是哪个字段@NotEmpty 被注释的字符串的必须非空,注意只能用在String上 否则会报错, message尽量要写不然前端不知道是哪个字段@AssertTrue(message = "XXXX") 被注释的元素必须为 true, message尽量要写不然前端不知道是哪个字段@AssertFalse 被注释的元素必须为 false@Min(value=L,message="XXXX") 被注释的元素必须是一个数字,其值必须大于等于指定的最小值, message尽量要写不然前端不知道是哪个字段@Max(value=L,message="XXXX") 被注释的元素必须是一个数字,其值必须小于等于指定的最小值, message尽量要写不然前端不知道是哪个字段@DecimalMin(value=L,message="XXXX") 被注释的元素必须是一个数字,其值必须大于等于指定的最小值, message尽量要写不然前端不知道是哪个字段@DecimalMax(value=L,message="XXXX") 被注释的元素必须是一个数字,其值必须小于等于指定的最大值, message尽量要写不然前端不知道是哪个字段@Size(max, min) 被注释的元素的大小必须在指定的范围内, message尽量要写不然前端不知道是哪个字段@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内, message尽量要写不然前端不知道是哪个字段@Past 被注释的元素必须是一个过去的日期, message尽量要写不然前端不知道是哪个字段@Future 被注释的元素必须是一个将来的日期, message尽量要写不然前端不知道是哪个字段@Pattern(value) 被注释的元素必须符合指定的正则表达式, message尽量要写不然前端不知道是哪个字段@Email 被注释的元素必须是电子邮箱地址, message尽量要写不然前端不知道是哪个字段@Range 被注释的元素必须在合适的范围内, message尽量要写不然前端不知道是哪个字段@NotBlank 验证字符串非null,且长度必须大于0,注意只能用在String上 否则会报错
然后需要在controller方法体添加@Validated 或 @Valid 不加校验会不起作用 
1.3 如何扩展(如何自定义校验注解)
(1)定义注解,必须包含message、groups、Payload三个属性
/*** 边界值校验* @author Lilu* @date 2021-7-16 16:57*/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;@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })@Retention(RUNTIME)@Documented@Constraint(validatedBy = { IntegerValidImpl.class})public @interface IntegerValid {int max();//最大值int min();//最小值String message() default "{你不对劲}";Class<?>[] groups() default { };Class<? extends Payload>[] payload() default { };}
(1)实现接口
import org.springframework.stereotype.Component;import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;/*** @author Lilu* @date 2021-7-16 16:57*//*** 自定义类,用于对校验注解规则的实现* 实现 ConstraintValidator 接口,泛型,第一个是对什么注解进行实现,第二个是检验的数据的数据类型 ;*/@Componentpublic class IntegerValidImpl implements ConstraintValidator<IntegerValid, Integer> {private int min;private int max;/*** @Description 初始化* @Date 2021-7-16 17:09* @Param [constraintAnnotation]* @return void**/@Overridepublic void initialize(IntegerValid constraintAnnotation) {min=constraintAnnotation.min();max=constraintAnnotation.max();}@Overridepublic boolean isValid(Integer value, ConstraintValidatorContext context) {System.out.println(value);if(value>min&&value<max){return true;}return false;}}
(3)使用
@ApiOperation(value = "测试接口", notes = "测试接口")@PostMapping("test")public ResponseEntity test(@Validated @RequestBody TestBean testBean){return ResponseEntity.ok(testBean);}public static class TestBean{@IntegerValid(min = 1, max = 20)private Integer aa;@IntegerValid(min = 1, max = 20)private Integer bb;public Integer getAa() {return aa;}public void setAa(Integer aa) {this.aa = aa;}public Integer getBb() {return bb;}public void setBb(Integer bb) {this.bb = bb;}}
1.3 分组如何使用
groups可以指定注解使用的场景,一个实体类可能会在多个场合有使用,如插入,删除等。通过groups可以指定该注解在插入/删除的环境下生效。
payload往往对bean进行使用。
例子
(1)定义group最后可以再定义两个接口作为group,代表两种不同的操作。
/*** @author Lilu* @date 2021-7-16 17:45*/public interface Insert {}
/*** @author Lilu* @date 2021-7-16 17:45*/public class Update {}
(2)使用
import com.jc.purchase.annotation.IntegerValid;import com.jc.purchase.annotation.Update;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.http.ResponseEntity;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.*;import java.awt.*;/*** @author Lilu* @date 2021-7-16 17:45*/@Api(tags = "通用接口")@RestController@RequestMapping("/api/se/general")public class GeneralController {private final Logger log = LoggerFactory.getLogger(GeneralController.class);@ApiOperation(value = "测试新增")@PostMapping("testAdd")public ResponseEntity add(@Validated(Insets.class) @RequestBody TestBean testBean){return ResponseEntity.ok(testBean);}@ApiOperation(value = "测试修改")@PutMapping("testPut")public ResponseEntity put(@Validated(Update.class) @RequestBody TestBean testBean){return ResponseEntity.ok(testBean);}public static class TestBean{@IntegerValid(min = 1, max = 20,groups = Update.class)//修改时验证生效private Integer aa;@IntegerValid(min = 1, max = 20,groups = Insets.class)//新建时验证生效private Integer bb;public Integer getAa() {return aa;}public void setAa(Integer aa) {this.aa = aa;}public Integer getBb() {return bb;}public void setBb(Integer bb) {this.bb = bb;}}}
2.全局异常响应码封装
2.1 后端代码

2.2 验证未通过请求响应结果
{status: 400,code:1,msg:""}

