JSR 是 Java Specification Requests 的缩写,即 Java 规范提案。
存在各类各样的 JSR,简单的理解为 JSR 是一种 Java 标准。
JSR 303 就是数据检验的一个标准(Bean Validation (JSR 303))。
参考:
https://www.jianshu.com/p/554533f88370html
处理一段业务逻辑,首先要确保数据输入的正确性,因此须要先对数据进行检查,保证数据在语义上的正确性,再根据数据进行下一步的处理。
前端能够经过 js 程序校验数据是否合法,后端一样也须要进行校验。然后端最简单的实现就是直接在业务方法中对数据进行处理,可是不一样的业务方法可能会出现一样的校验操做,这样就出现了数据的冗余。为了解决这个状况,JSR 303 出现了。
JSR 303 使用 Bean Validation,即在 Bean 上添加相应的注解,去实现数据校验。这样在执行业务方法前,都会根据注解对数据进行校验,从而减小自定义的校验逻辑,减小代码冗余。前端
(1)能够经过简单的注解校验 Bean 属性,好比 @NotNull、@Null 等。
(2)能够经过 Group 分组自定义须要执行校验的属性。
(3)能够自定义注解并指定校验规则。
(4)支持基于 JSR 303 的实现,好比 Hibernate Validator(额外添加一些注解)。java
(1)构建一个 SpringBoot 项目,以及使用 EasyCode 逆向生成相关的代码。
参考地址:
https://www.cnblogs.com/l-y-h/p/12781586.html
模板代码地址:
https://gitee.com/lyh-man/fast-template.gitmysql
(2)工具使用详情:
SpringBoot 2.2.6 + JDK 1.8 + mysql 1.8 搭建基本开发环境
IDEA + EasyCode + Lombok 插件 逆向生成基本代码
Postman 发送请求,测试接口git
没用 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("数据插入成功"); } }
使用 postman 测试该接口,当 id 不存在时,会被检测到。sql
id,name 都存在时,不会被捕获。后端
这里只是简单的测试一下逻辑,真实的数据检测确定比这复杂的多,而后每一个方法都须要写不一样的数据处理逻辑,这样就会形成数据的冗余。而使用 JSR303 的相关注解,就很简单,继续往下看。api
(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; }
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("数据插入成功"); } }
Step3:
使用 postman 测试一下。会抛出 MethodArgumentNotValidException 异常。
控制台打印的信息:
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("数据插入成功"); } }
使用 Postman 测试。
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); } }
相应的业务方法里,不须要再用 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("数据插入成功"); } }
使用 Postman 测试。
(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)。
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; }
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("数据插入成功"); } }
Step4:
使用 Postman 测试,发送 Post 请求,触发 createEmp 方法,执行 AddGroup 校验规则。
检测 id、name 是否合法。
发送 Put 请求,触发 UpdateEmp 方法,执行 UpdateGroup 校验规则。
只检测 name 是否合法。
(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; }
配置文件内容:
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(); } }
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; }
使用 Postman 测试。
name、email 都不存在时,会被捕获数据异常。
name 数据不合法、email 数据合法时,name 会被捕获。
注解 注解详情 @Null 被指定的注解元素必须为 Null @NotNull 任意类型,不能为 Null,但能够为空,好比空数组、空字符串。 @NotBlank 针对字符串,不能为 Null,且去除先后空格后的字符串长度要大于 0。 @NotEmpty 针对字符串、集合、数组,不能为 Null,且长度要大于 0。
注解 注解详情
@Size 针对字符串、集合、数组,判断长度是否在给定范围内。
@Length 针对字符串,判断长度是否在给定范围内。
注解 注解详情 @AssertTrue 针对布尔值,用来判断布尔值是否为 true @AssertFalse 针对布尔值,用来判断布尔值是否为 false
注解 注解详情
@Past 针对日期,用来判断当前日期是否为 过去的日期
@Future 针对日期,用来判断当前日期是否为 将来的日期
注解 注解详情
@Max(value) 针对字符串、数值,用来判断是否小于等于某个指定值
@Min(value) 针对字符串、数值,用来判断是否大于等于某个指定值
注解 注解详情
@Pattern 验证字符串是否知足正则表达式
@Email 验证字符串是否知足邮件格式
@Url 验证是否知足 url 格式