JSR 303 进行后台数据校验

时间:2023-12-18 21:30:20

一、JSR 303

1、什么是 JSR 303?

  JSR 是 Java Specification Requests 的缩写,即 Java 规范提案。
  存在各种各样的 JSR,简单的理解为 JSR 是一种 Java 标准。
  JSR 303 就是数据检验的一个标准(Bean Validation (JSR 303))。
参考:
  https://www.jianshu.com/p/554533f88370

2、为什么使用 JSR 303?

  处理一段业务逻辑,首先要确保数据输入的正确性,所以需要先对数据进行检查,保证数据在语义上的正确性,再根据数据进行下一步的处理。
  前端可以通过 js 程序校验数据是否合法,后端同样也需要进行校验。而后端最简单的实现就是直接在业务方法中对数据进行处理,但是不同的业务方法可能会出现同样的校验操作,这样就出现了数据的冗余。为了解决这个情况,JSR 303 出现了。
  JSR 303 使用 Bean Validation,即在 Bean 上添加相应的注解,去实现数据校验。这样在执行业务方法前,都会根据注解对数据进行校验,从而减少自定义的校验逻辑,减少代码冗余。

3、JSR 303 常见操作?

(1)可以通过简单的注解校验 Bean 属性,比如 @NotNull、@Null 等。
(2)可以通过 Group 分组自定义需要执行校验的属性。
(3)可以自定义注解并指定校验规则。
(4)支持基于 JSR 303 的实现,比如 Hibernate Validator(额外添加一些注解)。

二、演示 JSR303 的简单使用

1、构建一个 SpringBoot 项目用来演示

(1)构建一个 SpringBoot 项目,以及使用 EasyCode 逆向生成相关的代码。
参考地址:
  https://www.cnblogs.com/l-y-h/p/12781586.html
模板代码地址:
  https://gitee.com/lyh-man/fast-template.git

(2)工具使用详情:
  SpringBoot 2.2.6 + JDK 1.8 + mysql 1.8 搭建基本开发环境
  IDEA + EasyCode + Lombok 插件 逆向生成基本代码
  Postman 发送请求,测试接口

2、未使用 JSR303 相关注解时

  没用 JSR 303 相关注解时,需要手动在业务方法里写处理数据的逻辑。
  修改 Controller ,简单测试一下未使用 JSR 303 相关注解时的做法。

@RestController
@RequestMapping("api")
public class EmpController {
@Resource
private EmpService empService; @PostMapping("/emp")
public Result createEmp(@RequestBody Emp emp) {
if (emp.getId() == null || emp.getName() == null) {
return Result.error().message("数据不存在");
}
return Result.ok().data("items", emp).message("数据插入成功");
}
}

JSR 303 进行后台数据校验

使用 postman 测试该接口,当 id 不存在时,会被检测到。

JSR 303 进行后台数据校验

id,name 都存在时,不会被捕获。

JSR 303 进行后台数据校验

  这里只是简单的测试一下逻辑,真实的数据检测肯定比这复杂的多,然后每个方法都需要写不同的数据处理逻辑,这样就会造成数据的冗余。而使用 JSR303 的相关注解,就很简单,继续往下看。

3、使用 JSR 303 相关注解处理逻辑

(1)使用步骤:
Step1:
  在相关的 Bean 上标注需要处理的注解,并指定需要提示的信息(若不指定,会从默认配置文件中读取默认的信息)。

Step2:
  在相关的方法上,使用 @Valid 注解(或者 @Validated 指定组名)标记需要被校验的数据,否则会不生效。
注意:
  检测到数据异常后,系统会向外抛出异常,如果做了统一异常处理,可以根据 postman 测试的结果,找到控制台打印出的 相应的异常,并处理。

Step3:
  处理异常。使用 BindingResult 可以获取到检测结果,然后进行处理。
  也可以使用 全局统一异常 处理(@RestControllerAdvice 与 @ExceptionHandler),处理检测结果。
注:
  统一异常处理参考:https://www.cnblogs.com/l-y-h/p/12781586.html#_label2

(2)使用:
Step1:
  在相关的 Bean 上标注注解,并写上指定信息。

import lombok.Data;

import javax.validation.constraints.NotNull;
import java.io.Serializable; @Data
public class Emp implements Serializable {
private static final long serialVersionUID = 281903912367009575L; @NotNull(message = "id 不能为 null")
private Integer id; @NotNull(message = "name 不能为 null")
private String name; private Double salary; private Integer age; private String email;
}

JSR 303 进行后台数据校验

Step2:
  修改 Controller 方法,使用 @Valid 注解标记需要检测的数据。

@RestController
@RequestMapping("api")
public class EmpController {
@Resource
private EmpService empService; @PostMapping("/emp")
public Result createEmp(@Valid @RequestBody Emp emp) {
return Result.ok().data("items", emp).message("数据插入成功");
}
}

JSR 303 进行后台数据校验

Step3:
  使用 postman 测试一下。会抛出 MethodArgumentNotValidException 异常。

JSR 303 进行后台数据校验

控制台打印的信息:

JSR 303 进行后台数据校验

Step4:
  可以使用 BindingResult 去处理捕获到的数据并进行相关处理。

@RestController
@RequestMapping("api")
public class EmpController {
@Resource
private EmpService empService; @PostMapping("/emp")
public Result createEmp(@Valid @RequestBody Emp emp, BindingResult result) {
if (result.hasErrors()) {
Map<String, String> map = new HashMap<>();
// 获取校验结果,遍历获取捕获到的每个校验结果
result.getFieldErrors().forEach(item ->{
// 获取校验的信息
String message = item.getDefaultMessage();
String field = item.getField();
// 存储得到的校验结果
map.put(field, message);
});
return Result.error().message("数据不合法").data("items", map);
}
return Result.ok().data("items", emp).message("数据插入成功");
}
}

JSR 303 进行后台数据校验

使用 Postman 测试。

JSR 303 进行后台数据校验

Step5:
  通过上面的步骤,已经可以捕获异常、处理异常,但是每次都是在业务方法中手动处理逻辑,这样的实现,代码肯定会冗余。可以将其抽出,使用 统一异常处理,每次异常发生时,将其捕获。
  根据 Step3 可以看到会抛出 MethodArgumentNotValidException 异常,所以需要将其捕获。
  需要使用 @RestControllerAdvice 与 @ExceptionHandler。

@RestControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass()); @ExceptionHandler(MethodArgumentNotValidException.class)
public Result handlerValidException(MethodArgumentNotValidException e) {
logger.error(e.getMessage(), e);
BindingResult result = e.getBindingResult();
Map<String, String> map = new HashMap<>();
// 获取校验结果,遍历获取捕获到的每个校验结果
result.getFieldErrors().forEach(item ->{
// 存储得到的校验结果
map.put(item.getField(), item.getDefaultMessage());
});
return Result.error().message("数据校验不合法").data("items", map);
}
}

JSR 303 进行后台数据校验

相应的业务方法里,不需要再用 BindingResult 去处理数据了(即 Step2 的状态)。

@RestController
@RequestMapping("api")
public class EmpController {
@Resource
private EmpService empService; @PostMapping("/emp")
public Result createEmp(@Valid @RequestBody Emp emp) {
return Result.ok().data("items", emp).message("数据插入成功");
}
}

JSR 303 进行后台数据校验

使用 Postman 测试。

JSR 303 进行后台数据校验

4、JSR 303 分组校验

(1)为什么使用 分组校验?
  通过上面的过程,可以了解到单个方法的校验规则。
  如果出现多个方法,都需要校验 Bean,且校验规则不同的时候,怎么办呢?
  分组校验就可以去解决该问题,每个分组指定不同的校验规则,不同的方法执行不同的分组,就可以得到不同的校验结果。

(2)基本认识
  JSR 303 的每个注解都默认具备三个属性:
    message 用来定义数据校验失败后的提示消息,默认读取配置文件的内容。
      全局搜索 ValidationMessages.properties,可以看到默认的信息。

    groups 用来定义分组,其是一个 class 数组,可以指定多个分组。

String message() default "{javax.validation.constraints.NotNull.message}";

Class<?>[] groups() default { };

Class<? extends Payload>[] payload() default { };

(3)使用分组步骤:
Step1:
  定义一个空接口,用于指定分组,内部不需要任何实现。

Step2:
  指定 注解时,通过 groups 指定分组。用于指定在某个分组条件下,才去执行校验规则。

Step3:
  在相关的业务方法上,通过 @Validated 注解指定分组,去指定校验。
注:
  使用分组校验后,Bean 注解上若不指定分组,则不会执行校验规则。

(4)使用:
Step1:
  创建分组接口。
  创建两个分组接口 AddGroup、UpdateGroup。
其中:
  AddGroup 用于指定 添加数据 时的校验规则(比如:id、name 均不为 null)。
  UpdateGroup 用于指定 修改数据 时的校验规则(比如:name 不允许为 null)。

JSR 303 进行后台数据校验

Step2:
  给 Bean 添加注解,并指定分组信息。

@Data
public class Emp implements Serializable {
private static final long serialVersionUID = 281903912367009575L; @NotNull(message = "id 不能为 null", groups = {AddGroup.class})
private Integer id; @NotNull(message = "name 不能为 null", groups = {AddGroup.class, UpdateGroup.class})
private String name; private Double salary; private Integer age; private String email;
}

JSR 303 进行后台数据校验

Step3:
  在业务方法上,通过 @Validated 注解指定分组,去指定校验。
如下例,定义两个方法,Post 请求会触发 createEmp 方法,Put 请求会触发 UpdateEmp 方法。

@RestController
@RequestMapping("api")
public class EmpController {
@Resource
private EmpService empService; @PostMapping("/emp")
public Result createEmp(@Validated({AddGroup.class}) @RequestBody Emp emp) {
return Result.ok().data("items", emp).message("数据插入成功");
} @PutMapping("/emp")
public Result UpdateEmp(@Validated({UpdateGroup.class}) @RequestBody Emp emp) {
return Result.ok().data("items", emp).message("数据插入成功");
}
}

JSR 303 进行后台数据校验

Step4:
  使用 Postman 测试,发送 Post 请求,触发 createEmp 方法,执行 AddGroup 校验规则。
  检测 id、name 是否合法。

JSR 303 进行后台数据校验

发送 Put 请求,触发 UpdateEmp 方法,执行 UpdateGroup 校验规则。
只检测 name 是否合法。

JSR 303 进行后台数据校验

5、JSR 303 自定义校验注解

(1)为什么使用自定义校验注解?
  上面的注解满足不了业务需求时,可以自定义校验注解,自定义校验规则。

(2)步骤:
Step1:
  需要自定义一个校验注解。
  可以创建一个 ValidationMessages.properties 用于保存默认的 message 信息。

Step2:
  需要自定义一个校验器,即自定义校验规则。
  实现 ConstraintValidator 接口,并重写相关方法。
注:
  initialize 方法用于初始化,可以获取 自定义的属性的值。
  isValid 方法用于校验,可以获取到实际的值,然后与自定义的属性值进行比较。

Step3:
  将校验注解 与 校验器 关联起来。
  @Constraint(validatedBy = {TestValidConstraintValidator.class})

(3)使用:
  如下例,自定义一个校验规则,判断数据长度是否合法。
  默认为 String 属性,当 String 为 Null 或者 长度大于 5 时,校验不通过。
  可以自定义 长度。
Step1:
  自定义一个校验注解,@TestValid,用于判断一个值的长度是否合法。

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.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME; /**
* 用于判断一个值的长度是否合法
*/
@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {TestValidConstraintValidator.class})
public @interface TestValid {
String message() default "{com.lyh.test.TestValid.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; /**
* 返回一个长度
* @return 默认为 5
*/
int length() default 5;
}

JSR 303 进行后台数据校验

配置文件内容:

JSR 303 进行后台数据校验

Step2:
  自定义一个校验器TestValidConstraintValidator, 用于检测值是否合法。

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext; /**
* 实现 ConstraintValidator 接口,
* 其中 ConstraintValidator 的泛型,一个需要指定自定义的注解,一个需要指定需要获取的值的类型。
* 比如:
* ConstraintValidator<TestValid, String> 中
* TestValid 表示自定义注解
* String 表示获取的值的类型
* 即定义规则,判断一个 String 的值的长度是否满足条件
*/
public class TestValidConstraintValidator implements ConstraintValidator<TestValid, String> { /**
* 用于保存自定义的(默认)长度
*/
private int length; /**
* 初始化方法,获取默认数据
* @param constraintAnnotation 注解对象
*/
@Override
public void initialize(TestValid constraintAnnotation) {
length = constraintAnnotation.length();
} /**
* 自定义校验规则,如果 String 为 Null 或者 长度大于 5,则校验失败(返回 false)
* @param value 需要校验的值
* @param context
* @return true 表示校验成功,false 表示校验失败
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null ? false : length > value.length();
} }

JSR 303 进行后台数据校验

Step3:
  使用注解。

@Data
public class Emp implements Serializable {
private static final long serialVersionUID = 281903912367009575L; @NotNull(message = "id 不能为 null", groups = {AddGroup.class})
private Integer id; @TestValid(groups = {AddGroup.class})
@NotNull(message = "name 不能为 null", groups = {AddGroup.class, UpdateGroup.class})
private String name; private Double salary; private Integer age; @TestValid(length = 10, message = "值不能为 Null 且长度不超过 10", groups = {AddGroup.class})
private String email;
}

JSR 303 进行后台数据校验

使用 Postman 测试。
  name、email 都不存在时,会被捕获数据异常。

JSR 303 进行后台数据校验

name 数据不合法、email 数据合法时,name 会被捕获。

JSR 303 进行后台数据校验

三、JSR 303 相关注解

1、空检查相关注解

注解                      注解详情
@Null 被指定的注解元素必须为 Null
@NotNull 任意类型,不能为 Null,但可以为空,比如空数组、空字符串。
@NotBlank 针对字符串,不能为 Null,且去除前后空格后的字符串长度要大于 0。
@NotEmpty 针对字符串、集合、数组,不能为 Null,且长度要大于 0。

JSR 303 进行后台数据校验

JSR 303 进行后台数据校验

JSR 303 进行后台数据校验

JSR 303 进行后台数据校验

2、长度检查

注解                      注解详情
@Size 针对字符串、集合、数组,判断长度是否在给定范围内。
@Length 针对字符串,判断长度是否在给定范围内。

JSR 303 进行后台数据校验

JSR 303 进行后台数据校验

3、布尔值检查

注解                      注解详情
@AssertTrue 针对布尔值,用来判断布尔值是否为 true
@AssertFalse 针对布尔值,用来判断布尔值是否为 false

JSR 303 进行后台数据校验

JSR 303 进行后台数据校验

4、日期检查

注解                      注解详情
@Past 针对日期,用来判断当前日期是否为 过去的日期
@Future 针对日期,用来判断当前日期是否为 未来的日期

5、数值检查

注解                      注解详情
@Max(value) 针对字符串、数值,用来判断是否小于等于某个指定值
@Min(value) 针对字符串、数值,用来判断是否大于等于某个指定值

JSR 303 进行后台数据校验

JSR 303 进行后台数据校验

6、其他

注解                      注解详情
@Pattern 验证字符串是否满足正则表达式
@Email 验证字符串是否满足邮件格式
@Url 验证是否满足 url 格式

JSR 303 进行后台数据校验