JSR303

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在原有的基础上也内嵌了几个注解,如下。

注解详细信息
@Email被注释的元素必须是电子邮箱地址
@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"
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值