JSR303
**注意:**实体类要加上getter 和setter不然会一直报错
一、前言
1、什么是JSR
讲validation之前需要先了解一下JSR。JSR是Java Specification Requests 的缩写,意思是Java规范提案。是指向JCR(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。
2、什么是JSR303
JSR-303是Java EE 6中的一项子规范,叫做Bean Validation,Hibernate Validator是 Bean Validation 的参考实现。Hibernate Validator 提供了 JSR303规范中所有内置constraint的实现,除此之外还有一些附加的constraint。SpringBoot 中的 bean validation 集成了Hibernate Validator 和 tomcat-embed-el(可参照传送门),做了一套自己的Spring’s JSR-303规范。具体区别其实就是@Validated
和@Valid
的区别。
二、添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
三、注解
1、常用注解
Bean Validation
内嵌的注解很多,基本实际开发中已经够用了,注解如下:
注解 | 详细信息 |
---|---|
@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) | 被注释的元素必须符合指定的正则表达式 |
以上是
Bean Validation
的内嵌的注解,但是Hibernate Validator
在原有的基础上也内嵌了几个注解,如下。
注解 | 详细信息 |
---|---|
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |
2、@Valid和@Validated
2.1、@Validated:
- Spring提供的(Spring的JSR-303规范,是标准JSR-303的一个变种)
- 支持分组校验
- 可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
- 由于无法加在成员属性(字段)上,所以无法单独完成级联校验,需要配合@Valid
2.2、@Valid:
- JDK提供的(标准JSR-303规范),Hibernate-validator对其进行了实现
- 不支持分组校验
- 可以用在方法、构造函数、方法参数和成员属性(字段)上
- 可以加在成员属性(字段)上,能够独自完成级联校验
- 配合BindingResult可以直接提供参数验证结果。
总结: @Validated用到分组时使用,一个学校对象里还有很多个学生对象需要使用@Validated在Controller方法参数前加上,@Valid加在学校中的学生属性上,不加则无法对学生对象里的属性进行校验!
四、如何使用?
1、简单校验
简单的校验即是没有嵌套属性,直接在需要的元素上标注约束注解即可。如下:
@Data
public class ArticleDTO {
@NotNull(message = "文章id不能为空")
@Min(value = 1,message = "文章ID不能为负数")
private Integer id;
@NotBlank(message = "文章内容不能为空")
private String content;
@NotBlank(message = "作者Id不能为空")
private String authorId;
@Future(message = "提交时间不能为过去时间")
private Date submitTime;
}
复制
同一个属性可以指定多个约束,比如
@NotNull
和@MAX
,其中的message
属性指定了约束条件不满足时的提示信息。
以上约束标记完成之后,要想完成校验,需要在controller
层的接口标注@Valid
注解以及声明一个BindingResult
类型的参数来接收校验的结果。
下面简单的演示下添加文章的接口,如下:
/**
* 添加文章
*/
@PostMapping("/add")
public String add(@Valid @RequestBody ArticleDTO articleDTO, BindingResult bindingResult) throws JsonProcessingException {
//如果有错误提示信息
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 objectMapper.writeValueAsString(map);
}
return "success";
}
复制
仅仅在属性上添加了约束注解还不行,还需在接口参数上标注
@Valid
注解并且声明一个BindingResult
类型的参数来接收校验结果。
2、分组校验
背景:当一个实体修改时需要提供id而插入时不需要提供id这时应该怎么做呢?
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制
@Valid:作为标准的JSR-303规范,还没有吸收分组的功能
所以这里使用@Validated来指定接口分组
2.1、定义分组
定义分组的作用在于划分验证情况,不同的分组代表不同的情况,不同的情况字段校验规则不同
/**
* 添加组
*
* @author BLOOM
* @date 2023/01/28
*/
public interface AddGroup {
}
/**
* 更新组
*
* @author BLOOM
* @date 2023/01/28
*/
public interface UpdateGroup {
}
**注意:**除了自定义的分组情况以外,还会有默认分组 DefaultGroup
2.2、标识分组
@PostMapping("/add")
public Result add(@Validated({AddGroup.class}) @RequestBody User user) {
return Result.ok();
}
这里的意思是当校验前端传来的User对象时,其User实体的字段上的校验注解只有同样声明了AddGroup分组或者默认分组(什么都不指定)时才生效,即当要验证User对象参数时,只有同样声明了AddGroup.class分组的注解或者采用默认分组的注解才会生效
比如,此时对于id字段,只会走 @Null注解,不会走 @NotNull注解
@Data
public class User {
// 对于上面的例子这个注解不会生效,只有对同样声明了UpdateGroup.class的校验才会
@NotNull(message = "用户id不能为空", groups = {UpdateGroup.class})
@Null(message = "用户id必须为空", groups = {AddGroup.class})
private Long id;
@NotNull(message = "用户账号不能为空")
@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
private String account;
@NotNull(message = "用户密码不能为空")
@Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
private String password;
@NotNull(message = "用户邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}
测试:传入如下数据
{
"id": 49,
"account": "nostrud fugiat",
"password": "laboris",
"email": "b.velr@qq.com"
}
结果
{
"code": 400,
"message": "数据校验异常",
"data": {
"id": "用户id必须为空"
}
}
3、嵌套参数校验
嵌套校验简单的解释就是一个实体中包含另外一个实体,并且这两个或者多个实体都需要校验。
举个栗子:文章可以有一个或者多个分类,作者在提交文章的时候必须指定文章分类,而分类是单独一个实体,有分类ID
、名称
等等。大致的结构如下:
public class ArticleDTO{
...文章的一些属性.....
//分类的信息
private CategoryDTO categoryDTO;
}
复制
此时文章和分类的属性都需要校验,这种就叫做嵌套校验。
嵌套校验很简单,只需要在嵌套的实体属性标注
@Valid
注解,则其中的属性也将会得到校验,否则不会校验。
如下文章分类实体类校验:
/**
* 文章分类
*/
@Data
public class CategoryDTO {
@NotNull(message = "分类ID不能为空")
@Min(value = 1,message = "分类ID不能为负数")
private Integer id;
@NotBlank(message = "分类名称不能为空")
private String name;
}
复制
文章的实体类中有个嵌套的文章分类CategoryDTO
属性,需要使用@Valid
标注才能嵌套校验,如下:
@Data
public class ArticleDTO {
@NotBlank(message = "文章内容不能为空")
private String content;
@NotBlank(message = "作者Id不能为空")
private String authorId;
@Future(message = "提交时间不能为过去时间")
private Date submitTime;
/**
* @Valid这个注解指定CategoryDTO中的属性也需要校验
*/
@Valid
@NotNull(message = "分类不能为空")
private CategoryDTO categoryDTO;
}
复制
Controller
层的添加文章的接口同上,需要使用@Valid
或者@Validated
标注入参,同时需要定义一个BindingResult
的参数接收校验结果。
嵌套校验针对分组查询仍然生效,如果嵌套的实体类(比如
CategoryDTO
)中的校验的属性和接口中@Validated
注解指定的分组不同,则不会校验。
JSR-303
针对集合
的嵌套校验也是可行的,比如List
的嵌套校验,同样需要在属性上标注一个@Valid
注解才会生效,如下:
@Data
public class ArticleDTO {
/**
* @Valid这个注解标注在集合上,将会针对集合中每个元素进行校验
*/
@Valid
@Size(min = 1,message = "至少一个分类")
@NotNull(message = "分类不能为空")
private List<CategoryDTO> categoryDTOS;
}
复制
总结:嵌套校验只需要在需要校验的元素(单个或者集合)上添加
@Valid
注解,接口层需要使用@Valid
或者@Validated
注解标注入参。
五、校验结果处理
1、方式一:直接获取BindingResult
BindingResult接口实现类封装了参数校验的信息
需要注意的是@Valid 和 BindingResult 是一 一对应的,如果有多个@Valid,那么每个@Valid后面都需要添加BindingResult用于接收bean中的校验信息
封装校验错误信息
@RestController
@RequestMapping("user")
public class UserController {
@PostMapping("/add")
public Result add(@RequestBody @Valid User user, BindingResult result) {
// 如果校验有错误
if (result.hasErrors()) {
// 封装校验信息
Map<String, Object> map = new HashMap<>();
// 获取字段校验错误信息对象集合
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
// 遍历集合
fieldErrors.forEach(item -> {
// 获取产生错误字段
String field = item.getField();
// 获取提示消息
String message = item.getDefaultMessage();
map.put(field, message);
});
return Result.fail(map);
}
return Result.ok();
}
}
{
"code": 201,
"message": "失败",
"data": {
"account": "账号长度必须是6-11个字符"
}
}
2、方式二:全局异常处理
2.1、全局异常处理规范
2.2、演示全局校验异常处理
首先需要取消掉参数中的BindingResult 参数
@RestController
@RequestMapping("user")
public class UserController {
@PostMapping("/add")
public Result add(@RequestBody @Valid User user) {
return Result.ok();
}
}
然后定义全局处理异常
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 获取BindingResult
BindingResult result = e.getBindingResult();
// 获取校验信息
Map<String, Object> map = new HashMap<>();
result.getFieldErrors().forEach(item -> {
String message = item.getDefaultMessage();
String field = item.getField();
map.put(field, message);
});
return Result.build(map, 400, "数据校验异常");
}
}
结果
{
"code": 400,
"message": "数据校验异常",
"data": {
"account": "账号长度必须是6-11个字符"
}
}
六、自定义校验注解
1、编写错误消息
properties文件
com.bloom.validation.annotation.ListValue.message=参数必须时0或1
2、编写一个自定义校验器
/**
* 列表值约束验证器
*
* @author BLOOM
* @date 2023/01/29
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化数据
*
* @param constraintAnnotation 约束注释
*/
@Override
public void initialize(ListValue constraintAnnotation) {
// 获取约束注解中的值
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
/**
* 是有效
*
* @param integer 需要校验的值
* @param constraintValidatorContext 约束验证器上下文
* @return boolean
*/
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
// 检测参数是否合法
return set.contains(integer);
}
}
3、编写一个自定义校验注解并关联校验器
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 指定校验器
@Constraint(
validatedBy = {ListValueConstraintValidator.class}
)
public @interface ListValue {
String message() default "{com.bloom.valiation.annotation.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 需要验证的默认值
int[] vals() default {};
}
4、各属性贴上注解
@Data
public class User {
@NotNull(message = "用户id不能为空", groups = {UpdateGroup.class})
@Null(message = "用户id必须为空", groups = {AddGroup.class})
private Long id;
@NotNull(message = "用户账号不能为空")
@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
private String account;
@NotNull(message = "用户密码不能为空")
@Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
private String password;
@NotNull(message = "用户邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
// 自定义校验注解
@ListValue(vals = {0, 1})
private Integer sex;
}
5、测试
传入参数
{
"account": "nostrud fugiat",
"password": "laboris",
"email": "b.velr@qq.com",
"sex":3
}
输出结果
{
"code": 400,
"message": "数据校验异常",
"data": {
"sex": "参数必须时0或1"
}
}