JSR303校验
# 有什么注解关键字呢?
package javax.validation.constraints;
在这个包下,有许多注解。
# 非空字段选择哪个?例如name之类的?
先看一下源码:
NotNull
* The annotated element must not be {@code null}.
* Accepts any type.
2
NotEmpty
* The annotated element must not be {@code null} nor empty.
NotBlank
* The annotated element must not be {@code null} and must contain at least one
* non-whitespace character. Accepts {@code CharSequence}
2
对比一下,只有NotBlank更适合,因为他的意思不能为空,且至少一个非空格的字符。
# 如何使用?
- 给需要校验的字段加上@关键字
package com.elite.mall.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* 品牌
*
* @author Saul.J.Wu
* @email Saul.J.Wu@gmail.com
* @date 2020-09-24 13:20:51
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交")
private String name;
/**
* 品牌logo地址
*/
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@TableLogic(delval = "0",value = "1")
private Integer showStatus;
/**
* 检索首字母
*/
private String firstLetter;
/**
* 排序
*/
private Integer sort;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
- 在contoller的方法参数添加@Valid
就可以测试接口,校验
示例代码
/**
* 保存
*/
@PostMapping
public CommonResult save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);
return CommonResult.ok();
}
2
3
4
5
6
7
8
springboot 2.3.x 版本无法引用 javax.validation.constraints. 下的 @NotNull 注解
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2
3
4
我刚才测试的时候,发现问的注解没有作用,经过排查,原来springboot2.3.x版本开始就需要手动添加了。
# 自定义校验错误返回信息
@关键字(message = "xxxx")
就可以指定返回什么信息了
# 自定义校验结果
给校验的Bean后,紧跟着BindingResult,就可以获取到校验结果。
/**
* 保存
*/
@PostMapping
public CommonResult save(@Valid @RequestBody BrandEntity brand, BindingResult bindingResult){
if(bindingResult.hasErrors()){
Map<String, String> map = new HashMap<>();
//获取错误的校验结果
bindingResult.getFieldErrors().forEach(item -> {
//获取错误提示
String message = item.getDefaultMessage();
//获取错误属性的名字
String field = item.getField();
map.put(field, message);
});
return CommonResult.error(400, "提交的数据不合法").put("data", map);
}
brandService.save(brand);
return CommonResult.ok();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
示例:
- 请求
{"name":""}
- 返回结果
{
"msg": "提交的数据不合法",
"code": 400,
"data": {
"name": "品牌名必须提交"
}
}
2
3
4
5
6
7
实例2
- 实体
package com.elite.mall.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
import java.io.Serializable;
/**
* 品牌
*
* @author Saul.J.Wu
* @email Saul.J.Wu@gmail.com
* @date 2020-09-24 13:20:51
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交")
private String name;
/**
* 品牌logo地址
*/
@NotEmpty
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@TableLogic(delval = "0",value = "1")
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp = "/^[a-zA-z]$/",message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0,message = "排序必须大于等于0")
private Integer sort;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- 请求结果
{
"msg": "提交的数据不合法",
"code": 400,
"data": {
"name": "品牌名必须提交",
"logo": "不能为空",
"sort": "不能为null",
"firstLetter": "不能为空"
}
}
2
3
4
5
6
7
8
9
10
# 分组校验
前言
新增和修改的校验规则不一样,例如,新增不需要品牌id,修改则需要,所以存在分组校验。
用法
- 给注解标注什么情况需要校验
package com.elite.mall.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.elite.mall.common.valid.AddGroup;
import com.elite.mall.common.valid.UpdateGroup;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
import java.io.Serializable;
/**
* 品牌
*
* @author Saul.J.Wu
* @email Saul.J.Wu@gmail.com
* @date 2020-09-24 13:20:51
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message = "修改时,必须指定品牌id", groups = {UpdateGroup.class})
@Null(message = "新增时,不能指定品牌id", groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交", groups = {AddGroup.class, UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotEmpty
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@TableLogic(delval = "0", value = "1")
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp = "/^[a-zA-z]$/", message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0, message = "排序必须大于等于0")
private Integer sort;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
- 在controller中使用@Validated指定使用什么分组
注意:当使用了分组校验后,有些字段并没有指定分组,那么它是无效的,所以要校验的字段,必须指定分组
package com.elite.mall.product.controller;
import com.elite.mall.common.utils.CommonResult;
import com.elite.mall.common.utils.PageUtils;
import com.elite.mall.common.valid.AddGroup;
import com.elite.mall.common.valid.UpdateGroup;
import com.elite.mall.product.entity.BrandEntity;
import com.elite.mall.product.service.BrandService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.Map;
//import org.apache.shiro.authz.annotation.RequiresPermissions;
/**
* 品牌
*
* @author Saul.J.Wu
* @email Saul.J.Wu@gmail.com
* @date 2020-09-24 13:20:51
*/
@RestController
@RequestMapping("product/brands")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 列表
*/
@GetMapping
//@RequiresPermissions("product:brand:list")
public CommonResult list(@RequestParam Map<String, Object> params) {
PageUtils page = brandService.queryPage(params);
return CommonResult.ok().put("page", page);
}
/**
* 信息
*/
@GetMapping("/{brandId}")
//@RequiresPermissions("product:brand:info")
public CommonResult info(@PathVariable("brandId") Long brandId) {
BrandEntity brand = brandService.getById(brandId);
return CommonResult.ok().put("brand", brand);
}
/**
* 保存
*/
@PostMapping
//@RequiresPermissions("product:brand:save")
public CommonResult save(@Validated(AddGroup.class) @RequestBody BrandEntity brand) {
brandService.save(brand);
return CommonResult.ok();
}
/**
* 修改
*/
@PutMapping
//@RequiresPermissions("product:brand:update")
public CommonResult update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand) {
brandService.updateById(brand);
return CommonResult.ok();
}
/**
* 删除
*/
@DeleteMapping
//@RequiresPermissions("product:brand:delete")
public CommonResult delete(@RequestBody Long[] brandIds) {
brandService.removeByIds(Arrays.asList(brandIds));
return CommonResult.ok();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# 测试,新增时指定品牌id
- 请求体
{"name":"xxx","brandId":1,"logo":"abc"}
- 结果
{
"msg": "参数格式校验失败!",
"code": 10001,
"data": {
"brandId": "新增时,不能指定品牌id"
}
}
2
3
4
5
6
7
# 测试,新增时,未指定分组的字段不符合规则
- 请求体
{"name":"xxx","logo":"abc"}
- 结果
{
"msg": "success",
"code": 200
}
2
3
4
居然成功了,所以当使用了分组校验后,有些字段并没有指定分组,那么它是无效的,所以要校验的字段,必须指定分组
那么现在应该为还没有用分组的字段,加上分组。
package com.elite.mall.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.elite.mall.common.valid.AddGroup;
import com.elite.mall.common.valid.UpdateGroup;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
import java.io.Serializable;
/**
* 品牌
*
* @author Saul.J.Wu
* @email Saul.J.Wu@gmail.com
* @date 2020-09-24 13:20:51
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message = "修改时,必须指定品牌id", groups = {UpdateGroup.class})
@Null(message = "新增时,不能指定品牌id", groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交", groups = {AddGroup.class, UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotEmpty(groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址", groups = {AddGroup.class, UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@TableLogic(delval = "0", value = "1")
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(groups = {AddGroup.class})
@Pattern(regexp = "/^[a-zA-z]$/", message = "检索首字母必须是一个字母", groups = {AddGroup.class, UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(groups = {AddGroup.class})
@Min(value = 0, message = "排序必须大于等于0", groups = {AddGroup.class, UpdateGroup.class})
private Integer sort;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
重新测试一次,同样的请求体,返回结果不一样
{
"msg": "参数格式校验失败!",
"code": 10001,
"data": {
"logo": "logo必须是一个合法的url地址",
"sort": "不能为null",
"firstLetter": "不能为空"
}
}
2
3
4
5
6
7
8
9
请求体
{"name":"xxx","logo":""}
- 返回
{
"msg": "参数格式校验失败!",
"code": 10001,
"data": {
"logo": "不能为空",
"sort": "不能为null",
"firstLetter": "不能为空"
}
}
2
3
4
5
6
7
8
9
# 测试修改接口校验
- put请求
{"name":"xxx","logo":""}
- 返回
{
"msg": "参数格式校验失败!",
"code": 10001,
"data": {
"brandId": "修改时,必须指定品牌id"
}
}
2
3
4
5
6
7
- put请求
{"name":"xxx","brandId":1}
- 返回
{
"msg": "success",
"code": 200
}
2
3
4
说明其他字段的分组校验也生效了。
像类似@NotNull(groups = {AddGroup.class})的这种情况,只有在新增时才会校验非空,修改时则不校验,可以保存数据库原数据。
总结:分组校验,可以帮助完成复杂校验场景,没有指定分组的校验注解,在开启分组校验时,不会生效。
# 自定义校验
前景
比如品牌显示状态,0,不显示,1显示,或者其他复杂情况,可能不能只用正则表达式来校验,所以就需要自定义校验来完成业务功能
# 如何开启
- 编写一个自定义的校验注解
- 编写一个自定义的校验器
- 关联自定义的校验注解和校验器
- 自定义校验注解
使用自定义注解需要导入依赖,elitemall-common/pom.xml
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
<scope>compile</scope>
</dependency>
2
3
4
5
6
/**
* 显示状态[0-不显示;1-显示]
*/
@StatusListValue(vals = {0, 1}, groups = {AddGroup.class})
@TableLogic(delval = "0", value = "1")
private Integer showStatus;
2
3
4
5
6
- 关联自定义的校验注解和校验器
package com.elite.mall.common.valid;
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 = {StatusListValueConstraintValidator.class})
public @interface StatusListValue {
String message() default "{com.elite.mall.common.valid.StatusListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] vals() default {};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 校验器
package com.elite.mall.common.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
public class StatusListValueConstraintValidator implements ConstraintValidator<StatusListValue, Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化方法
*
* @param constraintAnnotation
*/
@Override
public void initialize(StatusListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
//todo 非空判断
set.add(val);
}
}
/**
* 判断校验是否成功
*
* @param value 需要校验的值
* @param context 校验的上下文环境信息
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
测试
- 请求体
{"name":"xxx","logo":"abc","showStatus":3}
- 返回
{
"msg": "参数格式校验失败!",
"code": 10001,
"data": {
"logo": "logo必须是一个合法的url地址",
"showStatus": "必须提交指定的值",
"sort": "不能为null",
"firstLetter": "不能为空"
}
}
2
3
4
5
6
7
8
9
10
可见showStatus已经完成了
# 后期拓展
@Constraint(validatedBy = {StatusListValueConstraintValidator.class})
一个校验注解,可以由多个校验器,适配不同类型的注解
# 验证 Service 中的方法
还可以验证任何Spring组件的输入,而不是验证控制器级别的输入,我们可以使用@Validated
和@Valid
注释的组合来实现这一需求。
一定一定不要忘记在类上加上 Validated
注解了,这个参数可以告诉 Spring 去校验方法参数。
@Service
@Validated
publicclass PersonService {
public void validatePerson(@Valid Person person){
// do something
}
}
2
3
4
5
6
7
8
通过测试验证:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
publicclass PersonServiceTest {
@Autowired
private PersonService service;
@Test(expected = ConstraintViolationException.class)
public void should_throw_exception_when_person_is_not_valid() {
Person person = new Person();
person.setSex("Man22");
person.setClassId("82938390");
person.setEmail("SnailClimb");
service.validatePerson(person);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 参考链接
如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里! (opens new window)