Bean Validation 与 JSR 的关系
参考资料: https://jcp.org/en/jsr/summary?id=bean%20validation
JSR380 | Bean Validation 2.0 |
---|
JSR349 | Bean Validation 1.1 | JSR303 | Bean Validation 1.0 |
JSR 规范
- javax.validation.api
从Java 8开始, Java EE 改名为 Jakarta EE, javax.validation下的相关包移动到了jakarta.validation包下。 - hibernate-validator
Bean Validation 下注解
参考 https://blog.csdn.net/dear_little_bear/article/details/104556183
-
两个标识注解
注解 | 功能 |
---|
@Valid | 标记用于验证级联的属性、方法参数或方法返回类型 | @Validated | spring 提供的扩展注解,方便的用于分组校验 |
-
22个约束注解
注解 | 功能 |
---|
@AssertFalse | 检查元素是否为 false,支持数据类型:boolean | @AssertTrue | 检查元素是否为 true,支持数据类型:boolean | @DecimalMax(value=, inclusive=) | inclusive:boolean,默认 true,表示包含或等于 value:当 inclusive=false 时,检查带注解的值是否小于指定的最大值。 当 inclusive=true 检查该值是否小于或等于指定的最大值。 | @DecimalMin(value=, inclusive=) | 与 @DecimalMax 注解功能相反 | @Digits(integer=, fraction=) | 检查值是否为最多包含 integer 位整数和 fraction 位小数的数字 | @Email | 检查指定的字符序列是否为有效的电子邮件地址 | @Max(value=) | 检查值是否小于或等于指定的最大值 | @Min(value=) | 检查值是否大于或等于指定的最大值 | @NotBlank | 检查字符序列是否为空,以及去空格后的长度是否大于 0。 与 @NotEmpty 的不同之处在于,此约束只能应用于字符序列。 支持数据类型:CharSequence | @NotNull | 检查值是否不为 null 支持数据类型:任何类型 | @Null | 检查值是否为 null | @NotEmpty | 检查元素是否为 null 或 空 支持数据类型:CharSequence, Collection, Map, arrays | @Size(min=, max=) | 检查元素个数是否在 min(含)和 max(含)之间 支持数据类型:CharSequence,Collection,Map, arrays | @Negative | 检查元素是否严格为负数。零值被认为无效。 | @NegativeOrZero | 检查元素是否为负或零 | @Positive | 检查元素是否严格为正。零值被视为无效 | @PositiveOrZero | 检查元素是否为正或零。 | @Future | 检查日期是否在未来 | @FutureOrPresent | 检查日期是现在或将来 | @Past | 检查日期是否在过去 | @PastOrPresen | 检查日期是否在过去或现在 | @Pattern(regex=, flags=) | 根据给定的 flag 匹配,检查字符串是否与正则表达式 regex 匹配 |
-
Hibernate Validator 附加的注解
约束 | 功能 |
---|
@URL(protocol=,host=,port=,regexp=,flags=) | 被注释的字符串必须是一个有效的 URL | @Range(min=, max=) | 元素必须在合适的范围内 | @Length(min=, max=) | 字符串的大小必须在指定的范围内 |
Spring Boot 项目中使用 Bean Validation
基本校验
-
引入依赖 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.5.2</version>
</dependency>
-
实例类上标注校验注解,如@NotBlank @Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
@TableId
private Long brandId;
@NotBlank
private String name;
private String logo;
private String descript;
private Integer showStatus;
private String firstLetter;
private Integer sort;
}
-
controller 中,请求的方法参数添加注解@Valid,表明参数需要校验 @RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
-
postman 测试 后端控制台报异常:
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.feng.common.utils.R com.feng.mall.product.controller.BrandController.save(com.feng.mall.product.entity.BrandEntity): [Field error in object ‘brandEntity’ on field ‘name’: rejected value []; codes [NotBlank.brandEntity.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [brandEntity.name,name]; arguments []; default message [name]]; default message [must not be blank]] ]
默认的异常信息定义在 ValidationMessages.properties配置文件中,如
javax.validation.constraints.NotBlank.message = must not be blank
-
自定义异常提示信息 @NotBlank(message = "品牌名必须提交")
private String name;
测试时,后台显示的异常信息如下:
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.feng.common.utils.R com.feng.mall.product.controller.BrandController.save(com.feng.mall.product.entity.BrandEntity): [Field error in object ‘brandEntity’ on field ‘name’: rejected value []; codes [NotBlank.brandEntity.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [brandEntity.name,name]; arguments []; default message [name]]; default message [品牌名必须提交]] ]
-
使用 BindingResult 参数感知校验异常,并封装为自定义格式
- BindingResult参数必须紧跟在校验参数的后面,一旦校验出错,可以通过该参数获取到校验出错的信息,可以根据获取到的错误信息,自定义封装返回异常。
- 代码:
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, 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 R.error(400, "提交的数据不合法").put("data", map);
}
brandService.save(brand);
return R.ok();
}
- postman 测试结果
统一异常处理
对于基本校验处,使用 BindingResult 参数,在每个请求方法中处理,会显得很繁琐,并且也不利于维护,对业务有侵入。因此,引入下面的统一校验,对于出现的异常统一进行处理。
-
抽取一个统一异常处理类 MallExceptionControllerAdvice @Slf4j
@ResponseBody
@ControllerAdvice(basePackages = "com.feng.mall.product.controller")
public class MallExceptionControllerAdvice {
@ExceptionHandler(value = Exception.class)
public R handleValidException(Exception e) {
log.error("数据校验出现问题:{},异常类型:{}", e.getMessage(), e.getClass());
return R.error();
}
}
-
将 controller 代码还原回原来样子 @RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
-
测试 后台日志:
数据校验出现问题 :Validation failed for argument [0] in public com.feng.common.utils.R com.feng.mall.product.controller.BrandController.save(com.feng.mall.product.entity.BrandEntity) with 2 errors: [Field error in object ‘brandEntity’ on field ‘sort’: rejected value [null]; codes [NotNull.brandEntity.sort,NotNull.sort,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [brandEntity.sort,sort]; arguments []; default message [sort]]; default message [排序字段不能为空]] [Field error in object ‘brandEntity’ on field ‘firstLetter’: rejected value [null]; codes [NotEmpty.brandEntity.firstLetter,NotEmpty.firstLetter,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [brandEntity.firstLetter,firstLetter]; arguments []; default message [firstLetter]]; default message [检索首字母不能为空]] ,异常类型 :class org.springframework.web.bind.MethodArgumentNotValidException
-
表明统一异常处理类已经生效,并且捕获到了异常。为了能精确捕获异常,我们将统一处理异常的方法中的Exception 改为 MethodArgumentNotValidException,并且获取到BindingResult ,封装异常。 @Slf4j
@ResponseBody
@ControllerAdvice(basePackages = "com.feng.mall.product.controller")
public class MallExceptionControllerAdvice {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e) {
log.error("数据校验出现问题:{},异常类型:{}", e.getMessage(), e.getClass());
BindingResult result = e.getBindingResult();
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 R.error(400, "提交的数据不合法").put("data", map);
}
return R.error();
}
}
-
再次测试,通过统一异常处理类,得到了我们想要的结果。(校验失败的结果)
分组校验
想象一种场景:我们新增一条 BrandEntity 数据 和 修改一条 BrandEntity 数据,校验的字段可能是不一样的。比如主键字段,新增的时候,不要求填写,而修改的时候,就必须带有主键字段。因此,我们可以使用分组校验功能。根据不同情况,使用不同的校验规则。
- 如果要使用分组校验规则,需要给实体类的字段上,标注分组校验参数groups
-
添加分组,对于指定的组,只是一个空接口。如下有三个组,分别触发三种情况的校验: public interface AddGroup {
}
public interface UpdateGroup {
}
public interface UpdateStatusGroup {
}
-
实体类字段上,标注分组,如:BrandEntity @Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull(message = "修改必须指定品牌id", groups = {UpdateGroup.class, UpdateStatusGroup.class})
@Null(message = "新增不能制定品牌id", groups = AddGroup.class)
@TableId
private Long brandId;
@NotBlank(message = "品牌名必须提交", groups = {AddGroup.class, UpdateGroup.class})
private String name;
@NotEmpty(message = "url地址不能为空")
@URL(message = "logo必须是一个合法的url地址")
private String logo;
private String descript;
private Integer showStatus;
@NotEmpty(message = "检索首字母不能为空")
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")
private String firstLetter;
@NotNull(message = "排序字段不能为空")
@Min(value = 0, message = "排序字段的值必须大于等于0")
private Integer sort;
}
如 brandId 字段,@NotNull 触发的情况是在满足 UpdateGroup 和 UpdateStatusGroup 情况下;而 @Null 触发校验,是在 AddGroup 这种情况下。没有标注 group属性的注解,在分组校验的情况下,不生效。 -
具体如何触发校验,用到了 @Validated 注解,该注解同@Valid注解一样可以标注在方法参数上,但是该注解可以指定分组,表明校验的时机。 ```java @RequestMapping("/save") public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){ brandService.save(brand);
return R.ok();
}
```
参数 BrandEntity 会在新增数据的时候进行校验
-
postman测试,分组校验规则生效
自定义校验注解
当普通的校验规则不能满足业务需求时候,这个就需要自定义校验规则了。
-
编写自定义校验注解 校验注解,必须满足规范
String message() default "{javax.validation.constraints.NotBlank.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
自定义校验注解 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.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.feng.common.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default {};
}
出错后,message 的提示信息,放在了 ValidationMessages.properties 配置文件中,如 com.feng.common.valid.ListValue.message=\u5fc5\u987b\u63d0\u4ea4\u6307\u5b9a\u7684\u503c
文件中定义的提示信息,使用了unicode码,实际中文是"必须提交指定的值",因为直接写中文,给出的提示信息乱码。没明白为啥不能直接写中文。希望知道的告诉我一声。谢谢! -
编写自定义的校验器 import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
private Set<Integer> set = new HashSet<>();
@Override
public void initialize(ListValue constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
int[] vals = constraintAnnotation.vals();
if (vals.length > 0) {
for (int val : vals) {
set.add(val);
}
}
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
-
关联自定义的校验器和校验注解
@Constraint(validatedBy = { ListValueConstraintValidator.class })
-
使用自定义的校验注解@ListValue @ListValue(vals={0, 1}, groups = { AddGroup.class })
private Integer showStatus;
vals={0, 1} 里的值,会通过 public void initialize(ListValue constraintAnnotation) 方法中的 constraintAnnotation.vals() 获取到;而 showStatus 的值,会通过 public boolean isValid(Integer value, ConstraintValidatorContext context) 方法中的参数 value获取到,并且在 isValid方法中完成校验。 -
postman测试 因为 showStatus 字段上标注的注解,限定值只能为 1 和 2,@ListValue(vals={0, 1}, groups = { AddGroup.class }) ,但是我们测试的时候,showSatus字段值,给的是3,校验失败,给出错误提示。 -
因为我们编写的校验器,现在只能校验类型是 Integer 的数据,如果我们想校验 Double类型的数据,此时需要我们自定义Double 类型的校验器。如: public class ListValueConstraintValidatorDouble implements ConstraintValidator<ListValue, Double> {
}
-
如果该注解 @ListValue 要想校验两种数据类型,我们只需要修改代码,将代码: @Constraint(validatedBy = { ListValueConstraintValidator.class })
改为 @Constraint(validatedBy = { ListValueConstraintValidator.class, ListValueConstraintValidatorDouble.class })
即可。
|