JSR-303
什么是JSR? JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
JSR-303定义的是什么标准? JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
也可以看哈这篇文章:mp.weixin.qq.com/s/g04HMhrjb…
使用场景
- 校验方法参数,构造器
- 校验返回值
注解
- 空和非空检查
- @NotBlank :只能用于字符串不为 null ,并且字符串 #trim() 以后 length 要大于 0 。
- @NotEmpty :集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 null 。
- @NotNull :不能为 null 。
- @Null :必须为 null 。
- 数值检查
- @DecimalMax(value) :被注释的元素必须是一个数字,其值必须小于等于指定的最大值。
- @DecimalMin(value) :被注释的元素必须是一个数字,其值必须大于等于指定的最小值。
- @Digits(integer, fraction) :被注释的元素必须是一个数字,其值必须在可接受的范围内。
- @Positive :判断正数。
- @PositiveOrZero :判断正数或 0 。
- @Max(value) :该字段的值只能小于或等于该值。
- @Min(value) :该字段的值只能大于或等于该值。
- @Negative :判断负数。
- @NegativeOrZero :判断负数或 0 。
- Boolean 值检查
- @AssertFalse :被注释的元素必须为 true 。
- @AssertTrue :被注释的元素必须为 false 。
- 长度检查
- @Size(max, min) :检查该字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等。
- 日期检查
- @Future :被注释的元素必须是一个将来的日期。
- @FutureOrPresent :判断日期是否是将来或现在日期。
- @Past :检查该字段的日期是在过去。
- @PastOrPresent :判断日期是否是过去或现在日期。
- 其它检查
- @Email :被注释的元素必须是电子邮箱地址。
- @Pattern(value) :被注释的元素必须符合指定的正则表达式。
@Valid 和 @Validated:
- @Valid 注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。因为能用在成员变量上,所以支持嵌套校验。如图
- @Validated 注解,是 Spring Validation 锁定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。同时,@Validated 有 value 属性,支持分组校验。
Spring Validation 仅对 @Validated 注解,实现声明式校验。
什么意思?
只有在类上或方法上使用@Validated注解,上述的@NotBlank等注解,用于方法参数上才能生效。
所以,绝大多数场景下,我们使用 @Validated 注解即可。而在有嵌套校验的场景,我们使用 @Valid 注解添加到成员属性上即可。
基本使用
spring boot 版本:2.7.5
需要注意的是,spring的校验是通过AOP实现的,所以只有在不同对象之间的调用才会生效。
如果是调用的同一个对象的内部方法,即可以通过this直接调用方法,则注解不能生效。
校验参数
@Data
public class UserVo {
/**
* 账号
*/
@NotEmpty(message = "登录账号不能为空")
@Size(min = 5, max = 16, message = "账号长度为 5-16 位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;
/**
* 密码
*/
@NotEmpty(message = "密码不能为空")
@Size(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
}
在Controller中使用:
@RestController
@RequestMapping("/users")
@Validated
@Slf4j
public class UserController {
@GetMapping("/get")
public int get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {
log.info("[get][id: {}]", id);
return 1;
}
@PostMapping("/add")
public String add(@Valid @RequestBody UserVo userVo) {
log.info("[add][addDTO: {}]", userVo);
return JSON.toJSONString(userVo);
}
}
但最好用于service:
@Validated
public interface UserService {
public Integer get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id);
public Integer add(@Valid UserVo userVo);
}
校验返回值
@Validated
public interface UserService {
public @NotNull Integer getOne(Integer one);
public @Min(value = 10) Integer getOne2(Integer one);
}
异常处理
对于抛出的异常,可以使用全局异常捕获的方式进行处理。
@Slf4j
@ControllerAdvice(basePackages = "pers.h.controller")
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = ConstraintViolationException.class)
public CommonResult constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
log.debug("[constraintViolationExceptionHandler]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(constraintViolation.getMessage());
}
// 包装 CommonResult 结果
return CommonResult.error(400,
"参数校验异常:"+ detailMessage.toString());
}
@ResponseBody
@ExceptionHandler(value = BindException.class)
public CommonResult bindExceptionHandler(HttpServletRequest req, BindException ex) {
log.debug("[bindExceptionHandler]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ObjectError objectError : ex.getAllErrors()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(objectError.getDefaultMessage());
}
// 包装 CommonResult 结果
return CommonResult.error(400,
"参数校验异常:"+ detailMessage.toString());
}
}
自定义校验
参考文章:www.iocoder.cn/Spring-Boot…
- 创建接口,用于定义注解的数据类型
public interface IntArrayValuable {
/**
* @return int 数组
*/
int[] array();
}
- 创建枚举类
package pers.h.constraints;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
@AllArgsConstructor
@Getter
public enum SexEnum implements IntArrayValuable {
UNKNOWN(0, "未知"),
MAN(1, "男"),
WOMAN(2, "女");
private final Integer code;
public final String desc;
@Override
public int[] array() {
return Arrays.stream(values()).mapToInt(SexEnum::getCode).toArray();
}
}
- 创建自定义约束注解
package pers.h.constraints;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = InEnumValidator.class)
public @interface InEnum {
/**
* @return 实现 IntArrayValuable 接口的
*/
Class<? extends IntArrayValuable> value();
/**
* @return 提示内容
*/
String message() default "必须在指定范围 {value}";
/**
* @return 分组
*/
Class<?>[] groups() default {};
/**
* @return Payload 数组
*/
Class<? extends Payload>[] payload() default {};
/**
* Defines several {@code @InEnum} constraints on the same element.
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
InEnum[] value();
}
}
- 创建约束注解的校验器
package pers.h.constraints;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
public class InEnumValidator implements ConstraintValidator<InEnum, Integer> {
/**
* 值数组
*/
private Set<Integer> values;
@Override
public void initialize(InEnum annotation) {
IntArrayValuable[] values = annotation.value().getEnumConstants();
if (values.length == 0) {
this.values = Collections.emptySet();
} else {
this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toSet());
}
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
// 校验通过
if (values.contains(value)) {
return true;
}
// 校验不通过
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
// context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
// .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
// return false;
throw new IllegalArgumentException(context.getDefaultConstraintMessageTemplate()
.replaceAll("\\{value}", values.toString()));
}
}
- 使用
package pers.h.model;
import lombok.Data;
import pers.h.constraints.InEnum;
import pers.h.constraints.SexEnum;
import javax.validation.constraints.NotNull;
@Data
public class UserUpdateGenderDTO {
/**
* 用户编号
*/
@NotNull(message = "用户编号不能为空")
private Integer id;
/**
* 性别
*/
@NotNull(message = "性别不能为空")
@InEnum(value = SexEnum.class, message = "性别必须是 {value}")
private Integer gender;
}
分组校验
分组校验,即相同的 Bean 对象,根据校验分组,使用不同的校验规则。
典型的场景是:在新增一条数据时,我们往往不需要主键,但是,在更新一条数据时,主键必不可少。
- 创建分组接口
/**
* 校验数据分组 ,数据新增场景
*/
public interface ValidationGroupAdd {
}
/**
* 校验数据分组,数据更新场景
*/
public interface ValidationGroupUpdate {
}
- bean
@Data
public class UserVo2 {
/**
* 账号
*/
@NotBlank(message = "登录账号不能为空", groups = ValidationGroupUpdate.class)
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空", groups = ValidationGroupAdd.class)
private String password;
@Valid
@NotNull(groups = ValidationGroupAdd.class, message = "部门不能为空!")
private Dept dept;
@Data
public static class Dept {
@NotBlank(message = "部门id不能为空", groups = ValidationGroupAdd.class)
private String deptId;
@NotBlank(message = "部门名称不能为空", groups = ValidationGroupAdd.class)
private String deptName;
}
}
- 接口
public interface UserService {
@Validated(value = ValidationGroupAdd.class)
String addOne(@Valid UserVo2 userVo);
@Validated(value = ValidationGroupUpdate.class)
String updateOne(@Valid UserVo2 userVo);
}
手动校验
@Autowired
private Validator validator;
@Test
public void testValidator() {
// 打印,查看 validator 的类型
System.out.println(validator);
// 创建 UserAddDTO 对象
UserVo addDTO = new UserVo();
// 校验
Set<ConstraintViolation<UserVo>> result = validator.validate(addDTO);
// 打印校验结果 // <4>
for (ConstraintViolation<UserVo> constraintViolation : result) {
// 属性:消息
System.out.println(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage());
}
}
输出:
org.springframework.validation.beanvalidation.LocalValidatorFactoryBean@6b2e46af
username:登录账号不能为空
password:密码不能为空
- 如果校验通过,则返回的 Set<ConstraintViolation<?>> 集合为空。
国际化
-
创建资源文件。
直接在resources目录下创建ValidationMessages.properties系列文件。
文件内容如下:# ValidationMessages.properties id.NotNull = id\u4e0d\u80fd\u4e3a\u7a7a\uff01# ValidationMessages_en_US.properties id.NotNull = userId cannot be empty !# ValidationMessages_zh_CN.properties id.NotNull = id\u4e0d\u80fd\u4e3a\u7a7a\uff01 -
在Bean中使用配置
@Data public class UserVoI18 { @NotNull(message = "{id.NotNull}") private Integer id; } -
使用时,可以通过
Accept-Language来指定语言环境
参考文章
www.iocoder.cn/Spring-Boot…
blog.51cto.com/u_3631118/2…
blog.csdn.net/luo15242208…