数据校验是Web开发中的重要部分,也是必须要考虑和面对的事情。
前置知识
先了解一下JSR、Hibernate Validator、Spring Validation:
-
JSR(Java Specification Request)规范是Java EE 6中的一项子规范,也叫作Bean Validation。它指定了一整套基于bean的验证API,通过标注给对象属性添加约束条件 -
Hibernate Validator是对JSR规范的实现,并增加了一些其他校验注解,如@Email、@Length、@Range等。
-
Spring Validation是Spring为了给开发者提供便捷,对Hibernate Validator进行了二次封装。
@Validated ,对@Valid 进行了二次封装
JSR定义了数据验证规范,而Hibernate Validator则是基于JSR规范,实现了各种数据验证的注解以及一些附加的约束注解。Spring Validation则是对Hibernate Validator的封装整合。
Spring Boot是从Spring发展而来的,所以自然支持Hibernate Validator和Spring Validation两种方式,默认使用的是Hibernate Validator组件。
Spring Boot添加maven依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
常用注解
所有的注解都包含code和message这两个属性(见后边的代码result.getFieldErrors())。Message定义数据校验不通过时的错误提示信息。code定义错误的类型。
扩展
@NotNull、@NotEmpty、@NotBlank区别:
-
1、@NotNull
- 不能为 null,但可以为 empty,一般用在 int、long、double、BigDecimal等数值类型的非空校验上,而且被其标注的字段可以使用 @size、@Max、@Min 对字段数值进行大小的控制
- @Range 一般用在 数值 类型上可对字段数值进行大小范围的控制。
-
2、@NotEmpty
- 不能为 null,且长度必须大于 0,一般用在 集合 类上或者 数组 上
-
3、@NotBlank
- 只能作用在接收的 String 类型上,不能为 null,而且调用 trim() 后,长度必须大于 0即:必须有实际字符
- @Length 一般用在 String 类型上可对字段数值进行最大长度限制的控制。
案例1-bean中进行数据校验
post请求参数较多时,可以在对应的数据模型(Java Bean)中进行数据校验,通过注解来指定字段校验的规则。
1、定义数据模型
定义一个接收数据的数据模型(javabean),使用注解的形式描述字段校验的规则。下面以PostTag_form对象为例:
@Getter
@Setter
@Accessors(chain = true)
@ApiModel(value = "标签form对象", description = "前端提交的表单数据")
public class PostTag_form implements Serializable {
@ApiModelProperty(value = "标签名",required = true)
@NotBlank(message = "标签名称不能为空!")
@Length(max = 20,message = "标签名称长度不合法,应大于0、小于等于20!")
private String tagName;
@ApiModelProperty(value = "创建者ID",required = true)
@Length(min = 5,max = 25,message = "创建者ID长度不合法,应大于等于5、小于等于25!")
private String userId;
}
注解@NotBlank,表示该字段必填(该字符串不能为空)
注解中的属性message是数据校验不通过时要给出的提示信息
扩展:
@NotNull(message = "年龄不能为空!")
@Min(18)
private int age;
@Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误")
private String phone;
@Email(message = "邮箱格式错误")
private String email;
@Size(min = 3,max = 5,message = "list的Size在[3,5]")
private List<String> list;
2、应用
在controller中应用:
post请求中,@RequestBody PostTag_form form 接收前端所提交的数据。在@RequestBody注解后面添加了@Validated注解 ,然后在后面添加了BindingResult返回验证结果 ,BindingResult是验证不通过时的结果集合。
@ApiOperation(value = "新增标签",notes = "创建成功,则返回新创建标签的ID")
@PostMapping
public DataVo addTag(@RequestBody @Validated PostTag_form form, BindingResult result){
if(result.hasErrors()) {
return JSONResult.errorMap(result);
}
return tagService.addTag(form);
}
注意,BindingResult必须跟在被校验参数之后,若被校验参数之后没有BindingResult对象,则会抛出BindException。
在 JSONResult 中添加方法 errorMap(BindingResult result) 如下:
public static DataVo errorMap(BindingResult result) {
HashMap<String, String> errMap = new HashMap<>(result.getFieldErrors().size());
for (FieldError error : result.getFieldErrors()) {
errMap.put(error.getField(),error.getDefaultMessage());
}
return new DataVo(501, "参数异常!详情见data!", errMap);
}
扩展
前边的示例中,参数校验结果保存在BindingResult中,若存在校验不通过的情况还需要我们手动调用JSONResult.errorMap() 返回异常数据给前端。若每个接口都这样处理感觉还是比较麻烦的,那有没有更优雅的处理方式呢?
我们可以利用“没有BindingResult对象会抛出BindException异常”,然后在全局异常处理类中捕获处理。
controller中修改方法addTag():
@ApiOperation(value = "新增标签",notes = "创建成功,则返回新创建标签的ID")
@PostMapping
public DataVo addTag(@RequestBody @Validated PostTag_form form){
return tagService.addTag(form);
}
全局异常处理类中添加方法handlerViolationException():
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object handlerViolationException(MethodArgumentNotValidException e) {
HashMap<String, String> errMap = new HashMap<>(e.getFieldErrors().size());
for (FieldError error : e.getFieldErrors()) {
errMap.put(error.getField(),error.getDefaultMessage());
}
e.printStackTrace();
String message = ExceptionUtils.getMessage(e);
log.error(message);
return JSONResult.build(501,"参数异常,详情见data!",errMap);
}
使用 全局异常处理类 来捕获数据校验不通过时抛出的异常,更加方便。
3、测试
提交空数据
请求参数如下:
{
"tagName": "",
"userId": ""
}
响应内容:
{
"code": 501,
"msg": "参数异常!详情见data!",
"data": {
"tagName": "标签名称不能为空!",
"userId": "创建者ID长度不合法,应大于等于5、小于等于25!"
}
}
提交错误数据(超长度)
请求参数如下:
{
"tagName": "123451234512345123451",
"userId": "1234"
}
响应内容:
{
"code": 501,
"msg": "参数异常!详情见data!",
"data": {
"tagName": "标签名称长度不合法,应大于0、小于等于20!",
"userId": "创建者ID长度不合法,应大于等于5、小于等于25!"
}
}
案例2-URL参数校验
一般GET请求都是在URL中传入参数。对于这种情况,可以直接通过注解来指定参数的校验规则。
@Validated
。。。
public class SysTagController {
@ApiOperation(value = "删除标签图片")
@DeleteMapping("/img/{tagId}")
public DataVo<String> delTagImg(@PathVariable
@ApiParam("标签ID")
@Length(max = 25, message = "标签ID长度不合法,应小于等于25!") String tagId){
return JSONResult.ok();
}
@ApiOperation(value = "获取标签信息")
@GetMapping
public DataVo<String> getTag(@ApiParam(value = "标签ID",required = true)
@NotBlank
@Length(max = 25, message = "标签ID长度不合法,应小于等于25!") String tagId,
@ApiParam(value = "标签名称",required = true)
@NotBlank
@Length(max = 20, message = "标签名称长度不合法,应小于等于20!") String tagName){
return JSONResult.ok();
}
}
注意:
1、使用注解对URL中传入的参数进行校验。
2、在方法所在的控制器controller上添加@Validated注解来使得验证生效(方法的参数前不用添加注解@Validated)。
全局异常处理类中添加方法handlerViolationException():
@ExceptionHandler(ConstraintViolationException.class)
public Object handlerViolationException(ConstraintViolationException e) {
HashMap<String, String> errMap = new HashMap<>();
Arrays.stream(e.getMessage().split(",")).forEach( msg->errMap.put(msg.substring(msg.indexOf(".")+1,msg.indexOf(":")),msg.substring(msg.indexOf(":")+2)) );
e.printStackTrace();
String message = ExceptionUtils.getMessage(e);
log.error(message);
return JSONResult.build(501,"参数异常,详情见data!",errMap);
}
测试
提交空数据
请求参数如下:
?tagId=%20&tagName=%20
响应内容:
{
"code": 501,
"msg": "参数异常!详情见data!",
"data": {
"tagId": "不能为空",
"tagName": "不能为空"
}
}
提交错误数据(超长度)
请求参数如下:
?tagId=01234567890123456789012345
?tagId=01234567890123456789012345&tagName=%20
响应内容:
{
"code": 501,
"msg": "参数异常!详情见data!",
"data": {
"tagId": "标签ID长度不合法,应小于等于25!"
}
}
{
"code": 501,
"msg": "参数异常!详情见data!",
"data": {
"tagId": "标签ID长度不合法,应小于等于25!",
"tagName": "不能为空"
}
}
案例3-bean对象级联校验
对于JavaBean对象中的普通属性字段,我们可以直接使用注解进行数据校验(见前边案例1),那如果是关联对象呢?其实也很简单,在属性上添加@Valid注解 就可以作为属性对象的内部属性进行验证。
例如:前边案例1中在类PostTag_form中添加变量TagIconDetail iconDetail
public class PostTag_form implements Serializable {
@ApiModelProperty(value = "标签名",required = true)
@NotBlank(message = "标签名称不能为空!")
@Length(max = 20,message = "标签名称长度不合法,应大于0、小于等于20!")
private String tagName;
@ApiModelProperty(value = "创建者ID",required = true)
@NotBlank(message = "创建者ID不能为空!")
@Length(max = 25,message = "创建者ID长度不合法,应大于0、小于等于25!")
private String userId;
@Valid
private TagIconDetail iconDetail;
}
TagIconDetail如下:
@Getter
@Setter
@Accessors(chain = true)
@ApiModel(value = "TagIconDetail对象", description = "标签各尺寸图标信息!")
public class TagIconDetail implements Serializable {
@ApiModelProperty(value = "高分屏应用图标",required = true)
@NotBlank(message = "高分屏应用图标 不能为空!")
private String icon72;
@ApiModelProperty(value = "720P高分屏应用图标",required = true)
@NotBlank(message = "720P高分屏应用图标 不能为空!")
private String icon96;
@ApiModelProperty(value = "1080P高分屏应用图标",required = true)
@NotBlank(message = "1080P高分屏应用图标 不能为空!")
private String icon144;
}
测试
请求参数:
{
"iconDetail": {
"icon144": "",
"icon72": "",
"icon96": ""
},
"tagName": "ffgegwergaerfegqertgqerfqerg",
"userId": ""
}
响应内容:
可以看到,新增加的变量iconDetail,由于添加了@Valid注解 故也校验其内部配置了校验规则的属性(iconDetail.icon72、iconDetail.icon96等)
{
"code": 501,
"msg": "参数异常,详情见data!",
"data": {
"iconDetail.icon72": "高分屏应用图标 不能为空!",
"iconDetail.icon96": "720P高分屏应用图标 不能为空!",
"iconDetail.icon144": "1080P高分屏应用图标 不能为空!",
"tagName": "标签名称长度不合法,应大于0、小于等于20!",
"userId": "创建者ID不能为空!"
}
}
案例4-分组校验
在不同情况下,可能对JavaBean对象的数据校验规则有所不同,有时需要根据数据状态对JavaBean中的某些属性字段进行单独验证。这时就可以使用分组校验功能,即根据状态启用一组约束。
Hibernate Validator的注解提供了groups参数,用于指定分组,如果没有指定groups参数,则默认属于javax.validation.groups.Default分组。
创建分组
创建分组Group_A、Group_B
public interface Group_A {
}
public interface Group_B {
}
定义了Group_A和Group_B两个接口作为两个校验规则的分组。
应用
bean中定义
还是前边案例中的PostTag_form,在相关的字段中定义校验分组规则。
...
public class PostTag_form implements Serializable {
@ApiModelProperty(value = "标签名",required = true)
@NotBlank(message = "标签名称不能为空!")
@Length(max = 20,message = "标签名称长度不合法,应大于0、小于等于20!",groups = {Group_A.class})
@Pattern(regexp = "[^0-9]+", message = "不能包含数字!",groups = {Group_B.class})
private String tagName;
@ApiModelProperty(value = "创建者ID",required = true)
@NotBlank(message = "创建者ID不能为空!")
@Length(max = 25,message = "创建者ID长度不合法,应大于0、小于等于25!")
private String userId;
}
- tagName字段定义了Group_A和Group_B两个分组校验规则。
- Group_A的校验规则限制长度
- Group_B的校验规则限制不能包含数字
- userId字段中,不定义分组校验规则,默认属于Default分组。
controller中使用
使用校验分组。
在@Validated注解中增加了{Group_A.class}参数,表示对于定义了分组校验的字段使用Group_A校验规则。
@ApiOperation(value = "新增标签",notes = "创建成功,则返回新创建标签的ID")
@PostMapping
public DataVo addTag(@RequestBody @Validated({Group_A.class}) PostTag_form form){
return tagService.addTag(form);
}
若要同时使用分组A与B,这样配置:@Validated({Group_A.class,Group_B.class}) ,使用逗号分隔开。
测试
1.应用分组Group_A
...
public DataVo addTag(@RequestBody @Validated({Group_A.class}) PostTag_form form){
...
}
请求参数:
{
"tagName": "abcde1abcde2abcde3abcde",
"userId": ""
}
响应内容:
{
"code": 501,
"msg": "参数异常,详情见data!",
"data": {
"tagName": "标签名称长度不合法,应大于0、小于等于20!"
}
}
应用分组Group_A,所以只有bean中tagName字段才进行数据校验(参数中userId字段没有数据,也不提示不能为空)。
分组A,tagName字段限制字符长度。
2.应用分组Group_B
...
public DataVo addTag(@RequestBody @Validated({Group_B.class}) PostTag_form form){
...
}
请求参数:
{
"tagName": "abcde1abcde2abcde3abcde",
"userId": ""
}
响应内容:
{
"code": 501,
"msg": "参数异常,详情见data!",
"data": {
"tagName": "不能包含数字!"
}
}
应用分组Group_B,所以只有bean中tagName字段才进行数据校验(参数中userId字段没有数据,也不提示不能为空)。
分组B,tagName字段限制不能包含数字。
3.同时应用多个分组
上边两个测试中,我们注意到userId字段配置的校验规则没有生效。是因为userId字段不定义分组校验规则,默认属于Default分组。若想在应用分组A(或B)的同时,默认分组的校验规则也生效,就需要同时应用多个分组。
...
public DataVo addTag(@RequestBody @Validated({Group_A.class, Default.class}) PostTag_form form){
...
}
请求参数:
{
"tagName": "abcde1abcde2abcde3abcde",
"userId": ""
}
响应内容:
{
"code": 501,
"msg": "参数异常,详情见data!",
"data": {
"tagName": "标签名称长度不合法,应大于0、小于等于20!",
"userId": "创建者ID不能为空!"
}
}
分组A,tagName字段限制字符长度。
默认分组,userId字段限制必填。
注意:同时应用多个分组,各个分组的校验默认是没有顺序,但有些时候是需要按我们指定的顺序来进行校验,那么如何让各个分组按顺序进行校验呢? 可以通过“组序列”来实现。
组序列
一个组可以定义为其他组的序列,使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证。
定义组序列Group_seq:
@GroupSequence({Group_A.class, Group_B.class, Default.class})
public interface Group_seq {
}
测试
请求参数:
{
"tagName": "abcde1abcde2abcde3abcde",
"userId": ""
}
响应内容:
{
"code": 501,
"msg": "参数异常,详情见data!",
"data": {
"tagName": "标签名称长度不合法,应大于0、小于等于20!"
}
}
分组A,tagName字段限制字符长度。分组A校验不通过,不再校验分组B、默认分组
请求参数:
{
"tagName": "abcde1abcde2abcde3ab",
"userId": ""
}
响应内容:
{
"code": 501,
"msg": "参数异常,详情见data!",
"data": {
"tagName": "不能包含数字!"
}
}
分组A,tagName字段限制字符长度。
分组B,tagName字段限制不能包含数字。
分组A校验通过,分组B校验不通过,不再校验默认分组
请求参数:
{
"tagName": "abcdeabcdeabcdeab",
"userId": ""
}
响应内容:
{
"code": 501,
"msg": "参数异常,详情见data!",
"data": {
"userId": "创建者ID不能为空!"
}
}
分组A,tagName字段限制字符长度。
分组B,tagName字段限制不能包含数字。
默认分组,userId字段限制必填。
分组A校验通过,分组B校验通过,默认分组校验不通过。按顺序依次验证各个分组。
案例5-自定义校验
通过自定义校验规则,可以实现一些复杂、特殊的数据验证功能。
定义校验注解
定义新的校验注解@CustomAgeValidator,示例代码如下:
@Min(value = 18,message = "年龄最小不能小于18")
@Max(value = 120,message = "年龄最大不能超过120")
@Constraint(validatedBy = {})
@Documented
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAgeValidator {
String message() default "年龄大小必须大于18并且小于120";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
创建了CustomAgeValidator自定义注解,用于自定义年龄的数据校验规则。
使用
在前边案例的bean中添加age变量,添加自定义校验注解@CustomAgeValidator:
@CustomAgeValidator
private int age;
controller如下:
@ApiOperation(value = "新增标签",notes = "创建成功,则返回新创建标签的ID")
@PostMapping
public DataVo addTag(@RequestBody @Validated PostTag_form form){
return tagService.addTag(form);
}
测试
请求参数:
{
"age": 8,
"tagName": "xxx",
"userId": "xxx"
}
响应内容:
{
"code": 501,
"msg": "参数异常,详情见data!",
"data": {
"age": "年龄最小不能小于18"
}
}
自定义校验注解@CustomAgeValidator中,限制了年龄最小不能小于18,请求参数中设置age=8,校验不通过!
说明
本博客中的案例,使用的maven依赖如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
笔记摘自:CSDN-故屿γ、CSDN-王大地X、《Spring Boot从入门到实战》-章为忠
|