Jakarta Bean验证
验证用户输入以维护数据完整性是应用程序逻辑的重要组成部分。即使是最简单的应用程序,数据验证也是必不可少的。
Jakarta Bean Validation 模型通过对 JavaBeans 组件(例如托管 bean)的字段、方法或类进行注解形式的约束来支持。
入门使用
1.检查是否有相应依赖,如果没有则引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>2.DTO添加校验注解
@NotBlank(message = "手机号不能为空")
private String phone;3.Controller添加注解@Validated使其生效
@PostMapping
@ApiOperation("新增员工")
public Result save(@Validated @RequestBody EmployeeDTO employeeDTO){
employeeService.save(employeeDTO);
return Result.success();
}
// JSON 请求体 → MethodArgumentNotValidException
@PostMapping("/user/json")
public Result createByJson(@Validated @RequestBody User user) {
return Result.success();
}
// 表单提交 → BindException
@PostMapping("/user/form")
public Result createByForm(@Validated User user) { // 没有 @RequestBody
return Result.success();
}4.接收校验错误结果,参数在校验失败的时候会抛出的MethodArgumentNotValidException或者BindException两种异常,可以在全局的异常处理器中捕捉到这两种异常 第三种异常不常见 通过aop触发
/**
* 自定义验证异常
*/
//处理表单/Query参数绑定到对象时的校验失败
@ExceptionHandler(BindException.class)
public Result validatedBindException(BindException e)
{
log.error(e.getMessage(), e);
String message = e.getAllErrors().get(0).getDefaultMessage();
return Result.error(message);
}
/**
* 自定义验证异常
*/
//处理 JSON 请求体绑定到对象时的校验失败
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object validExceptionHandler(MethodArgumentNotValidException e)
{
log.error(e.getMessage(), e);
// getFieldError()不指定参数,会默认取第一个错误信息。
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.error(message);
}
//Service或其他地方上启用了方法校验 通过Spring AOP 的方法校验异常
@ExceptionHandler(ConstraintViolationException.class)
public Result constraintViolationException(
ConstraintViolationException e) {
log.error(e.getMessage(), e);
String message = e.getConstraintViolations()
.stream()
.findFirst()
.map(ConstraintViolation::getMessage)
.orElse("参数校验失败");
return Result.error(message);
}内置注解约束
| 约束 | 描述 | 例子 |
|---|---|---|
@AssertFalse | 字段或属性的值必须为false。 | @AssertFalse boolean isUnsupported; |
@AssertTrue | 字段或属性的值必须为true。 | @AssertTrue boolean isActive; |
@DecimalMax | 字段或属性的值必须是小于或等于值元素中数字的十进制值。 | @DecimalMax("30.00") BigDecimal discount; |
@DecimalMin | 字段或属性的值必须是大于或等于值元素中数字的十进制值。 | @DecimalMin("5.00") BigDecimal discount; |
@Digits | 字段或属性的值必须是指定范围内的数字。 integer<instance-digits> 元素指定数字的最大整数位数, fraction<fraction-digits> 元素指定数字的最大小数位数。 | @Digits(integer=6, fraction=2) BigDecimal price; |
@Email | 该字段或属性的值必须是有效的电子邮件地址。 | @Email String emailaddress; |
@Future | 该字段或属性的值必须是未来的日期。 | @Future Date eventDate; |
@FutureOrPresent | 该字段或属性的值必须是现在或未来的日期或时间。 | @FutureOrPresent Time travelTime; |
@Max | 字段或属性的值必须是小于或等于值元素中数字的整数值。 | @Max(10) int quantity; |
@Min | 字段或属性的值必须是大于或等于值元素中数字的整数值。 | @Min(5) int quantity; |
@Negative | 该字段或属性的值必须为负数。 | @Negative int basementFloor; |
@NegativeOrZero | 该字段或属性的值必须为负数或零。 | @NegativeOrZero int debtValue; |
@NotBlank | 字段或属性的值必须至少包含一个非空白字符。 | @NotBlank String message; |
@NotEmpty | 字段或属性的值不能为空。系统会评估字符集或数组的长度,以及集合或映射的大小。 | @NotEmpty String message; |
@NotNull | 字段或属性的值不能为空。 | @NotNull String username; |
@Null | 该字段或属性的值必须为空。 | @Null String unusedString; |
@Past | 该字段或属性的值必须是过去的日期。 | @Past Date birthday; |
@PastOrPresent | 该字段或属性的值必须是过去或现在的日期或时间。 | @PastOrPresent Date travelDate; |
@Pattern | 字段或属性的值必须与regexp元素中定义的正则表达式匹配。 | @Pattern(regexp="\(\d{3}\)\d{3}-\d{4}") String phoneNumber; |
@Positive | 该字段或属性的值必须为正数。 | @Positive BigDecimal area; |
@PositiveOrZero | 该字段或属性的值必须为正数或零。 | @PositiveOrZero int totalGoals; |
@Size | 字段或属性的大小将被评估,并且必须与指定的边界相匹配。如果字段或属性是字符串String,则评估字符串的大小。如果字段或属性是数组,则评估数组的大小Collection。使用可选的 <bounds>或 <bounds> 元素来指定边界。Collection`MapMapmax`min | @Size(min=2, max=240) String briefMessage; |
在以下示例中,使用内置约束对字段施加@NotNull约束:
public class Name {
@NotNull
private String firstname;
@NotNull
private String lastname;
}您还可以对单个 JavaBeans 组件对象设置多个约束。例如,您可以对 <field>`firstname和 lastname
public class Name {
@NotNull
@Size(min=1, max=16)
private String firstname;
@NotNull
@Size(min=1, max=16)
private String lastname;
}高级特性
重复注释
从 Bean Validation 2.0 开始,您可以使用重复注解在验证目标上多次指定相同的约束:
public class Account {
@Max (value = 2000, groups = Default.class, message = "max.value")
@Max (value = 5000, groups = GoldCustomer.class, message = "max.value")
private long withdrawalAmount;
}这样可以通过不同的接口方法,使用不同的分组注解,实现不同场景的差异化校验。
完整实例
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
public class Order {
public interface CreateGroup {} // 下单场景
public interface UpdateGroup {} // 修改订单
public interface CancelGroup {} // 取消订单
public interface PayGroup {} // 支付场景
public interface ApproveGroup {} // 审批场景
@NotNull(groups = {CreateGroup.class, UpdateGroup.class}, message = "订单号不能为空")
private String orderNo;
@NotNull(groups = {CreateGroup.class, PayGroup.class}, message = "金额不能为空")
private BigDecimal amount;
@NotNull(groups = CancelGroup.class, message = "取消原因不能为空")
private String cancelReason;
@NotNull(groups = ApproveGroup.class, message = "审批人不能为空")
private String approver;
}通过在注解中明确指定使用的分组 @Validated(XxxGroup.class) 来对对象使用不同的校验策略
import com.zzyl.nursing.dto.Order;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/order")
public class OrderController {
/**
* 下单 - 使用 CreateGroup
*/
@PostMapping("/create")
public String create(@Validated(Order.CreateGroup.class) @RequestBody Order order) {
return "下单成功,订单号:" + order.getOrderNo();
}
/**
* 修改订单 - 使用 UpdateGroup
*/
@PutMapping("/update")
public String update(@Validated(Order.UpdateGroup.class) @RequestBody Order order) {
return "修改成功,订单号:" + order.getOrderNo();
}
/**
* 取消订单 - 使用 CancelGroup
*/
@PostMapping("/cancel")
public String cancel(@Validated(Order.CancelGroup.class) @RequestBody Order order) {
return "取消成功,原因:" + order.getCancelReason();
}
/**
* 支付 - 使用 PayGroup
*/
@PostMapping("/pay")
public String pay(@Validated(Order.PayGroup.class) @RequestBody Order order) {
return "支付成功,金额:" + order.getAmount();
}
/**
* 审批 - 使用 ApproveGroup
*/
@PostMapping("/approve")
public String approve(@Validated(Order.ApproveGroup.class) @RequestBody Order order) {
return "审批成功,审批人:" + order.getApprover();
}
}注意:当你要校验 普通方法参数(不是 @RequestBody 对象)时,可以这样校验
@RestController
@RequestMapping("/order")
@Validated
public class OrderController {
@GetMapping("/detail")
public String detail(
@NotNull Long id,
@Min(1) Integer page) {
return "ok";
}
}自定义约束
Jakarta Bean Validation 定义了注解、接口和类,允许开发人员创建自定义约束。
使用内置约束创建新约束
Jakarta Bean Validation 包含多个内置约束,这些约束可以组合起来创建新的、可重用的约束。这简化了约束定义,允许开发人员定义一个由多个内置约束组成的自定义约束,然后使用单个注解将其应用于组件属性。
完整例子
这个是内置注解约束 @Pattern
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package javax.validation.constraints;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)//可以定义多个regexp
@Documented
@Constraint(
validatedBy = {}
)
public @interface Pattern {
String regexp();//定义正则的字段
Flag[] flags() default {};
String message() default "{javax.validation.constraints.Pattern.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
public static enum Flag {
UNIX_LINES(1),
CASE_INSENSITIVE(2),
COMMENTS(4),
MULTILINE(8),
DOTALL(32),
UNICODE_CASE(64),
CANON_EQ(128);
private final int value;
private Flag(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
Pattern[] value();
}
}自定义注解约束
@Pattern.List({
/* A number of format “+1-NNN-NNN-NNNN” */
@Pattern(regexp = "\\+1-\\d{3}-\\d{3}-\\d{4}")
})//这里加上内置字段定义电话号的校验格式
@Constraint(validatedBy = {})//用于将一个自定义注解标记为约束注解
//validatedBy = {} 指定由哪个校验器类(实现了 ConstraintValidator 接口的类)来执行实际的校验逻辑
//当 {} 为空时:表示没有关联的校验器实现类,校验逻辑必须完全由其他手段提供
//例如:注解本身只是一个标记(如 @USPhoneNumber 只是一个复合注解,内部用了 @Pattern,实际校验由 @Pattern 完成)
@Documented
@Target({ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
public @interface USPhoneNumber {
String message() default "Not a valid US Phone Number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
USPhoneNumber[] value();
}
}自定义注解之后 使用时不需要每次使用@Pattern(regexp = "\\+1-\\d{3}-\\d{3}-\\d{4}")一长串的注解对字段进行约束
可以使用更加见名知意的注解进行约束
@USPhoneNumber
protected String phone;自定义校验类
不依赖其他注解的校验类 自己定义参数 自己定义校验方法
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = USPhoneNumberValidator.class) // 指定校验器类
@Documented
public @interface USPhoneNumber {
String message() default "Not a valid US Phone Number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 自定义属性
boolean required() default true; //是否需要校验
String format() default "+1-NNN-NNN-NNNN";//校验的字符串
}校验类
- 类在 classpath 下(任何包都可以)
- 有无参构造器(默认就有)
- 实现了
ConstraintValidator接口
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
public class USPhoneNumberValidator implements ConstraintValidator<USPhoneNumber, String> {
private boolean required;
private String format;
@Override
public void initialize(USPhoneNumber constraintAnnotation) {
// 获取注解中的参数值
this.required = constraintAnnotation.required();
this.format = constraintAnnotation.format();
// 可以在这里做一些初始化工作
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 处理 null 或空值
if (value == null || value.trim().isEmpty()) {
return !required; // 如果非必填,返回 true;如果必填,返回 false
}
// 正则表达式校验美国电话号码
String regex = "^\\+1-\\d{3}-\\d{3}-\\d{4}$";
boolean isValid = Pattern.matches(regex, value);
if (!isValid) {
// 可以自定义错误消息中的动态参数
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
String.format("Phone number must follow format: %s", format)
).addConstraintViolation();
}
return isValid;
}
}使用自定义注解
public class User {
@USPhoneNumber(required = true, message = "请填写有效的美国电话号码")
private String phoneNumber;
@USPhoneNumber(required = false) // 可选字段
private String alternatePhone;
// getter/setter
}测试类触发校验
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
public class Test {
public static void main(String[] args) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
//Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
//validator.validate(dept);
User user = new User();
user.setPhoneNumber("123-456-7890"); // 错误格式
Set<ConstraintViolation<User>> violations = validator.validate(user);
for (ConstraintViolation<User> violation : violations) {
System.out.println(violation.getMessage());
// 输出: 请填写有效的美国电话号码
}
}
}注解触发校验
// 支持分组校验
public void createUser(@Validated(Default.class) User user) {
// ...
}补充:如果在 Spring 环境中,可以让校验器支持 Spring 的依赖注入:
@Component // 让 Spring 管理
public class USPhoneNumberValidator implements ConstraintValidator<USPhoneNumber, String> {
@Autowired
private PhoneNumberService phoneService; // 可以注入其他服务
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 可以调用 Service 进行更复杂的校验(如查询数据库验证号码是否存在)
return phoneService.isValidUSNumber(value);
}
}还需要配置 Spring 使用自己的 ConstraintValidatorFactory:
@Configuration
public class ValidationConfig {
@Bean
public Validator validator(SpringConstraintValidatorFactory springFactory) {
try (ValidatorFactory factory = Validation.byDefaultProvider()
.configure()
.constraintValidatorFactory(springFactory) // 关键配置
.buildValidatorFactory()) {
return factory.getValidator();
}
}
@Bean
public SpringConstraintValidatorFactory springConstraintValidatorFactory(
AutowireCapableBeanFactory beanFactory) {
return new SpringConstraintValidatorFactory(beanFactory);
}
}级联验证
在校验 Department 时,也顺带校验 employees 列表中的每一个 Employee对象。
Employee对象里面的校验规则
public class Employee {
@USPhoneNumber
private String phone; // 手机号必须符合格式
@NotBlank
private String name;
}public class Department {
@NotBlank
private String deptName;
@Valid // 级联验证 或者使用@Validated
private List<Employee> employees;
}校验
@PostMapping("/dept")
public void create(@Validated @RequestBody Department dept) { }补充 Validated与Valid的区别
@Valid 是标准,@Validated 是 Spring 的扩展
主要是一个支持分组 一个不支持 如果你的代码中 @Validated 没指定分组,等价于默认分组,效果和 @Valid 完全一样
| 特性 | @Valid | @Validated |
|---|---|---|
| 来源 | Jakarta EE 标准 | Spring 框架 |
| 级联验证 | 支持 | 支持 |
| 分组验证 | 不支持 | 支持(核心优势) |
| 放在方法参数上 | 可以 | 可以(但类需要加@Validated) |
| 放在类上 | 不能 | 可以(开启方法级别验证) |
| 放在字段上及联校验 | 可以(级联) | 可以(级联) |
注意
- @Validated 是 Spring 注解 基于AOP切面实现的,级联校验只在 Spring 管理的校验上下文中生效(比如 Controller、Service 参数上的 @Valid/@Validated 触发的校验链路)。如果直接调用 javax.validation.Validator.validate(department),只有 @Valid 会被识别,@Validated 不会被处理。
- 级联校验是一个链式反应,@Validated / @Valid 加在字段上只是定义了链的传导规则,本身不会主动触发任何校验。必须有一个外部动作启动校验,链条才会传导下去。简单来说就是如果Department对象不执行校验 他内部的Employee对象也不会自己执行及联校验 因为没有启动入口
@Valid 本身不做任何校验,它只是一个信号,告诉运行时的校验框架"请对这个位置做级联校验"。有没有人读取这个信号,决定了它是否生效。
@Valid并不是加上就一定会自动校验,它必须在 Spring 或 Validation 框架触发校验的场景下 才会生效。@RestController @Validated public class UserController { @PostMapping public void save(@RequestBody @Valid UserDTO dto) { } }Spring MVC 会进行参数绑定,然后执行 Bean Validation
@RequestBody 参数
@PostMapping("/user")
public void create(@RequestBody @Valid UserDTO dto) {
}普通对象参数(Form表单)
@PostMapping("/user")
public void create(@Valid UserDTO dto) {
}这个直接调用不会进行校验 因为没有参数绑定
userService.save(dto);@Service
public class UserService {
public void save(@Valid UserDTO dto) {
}
}@Valid/@Validated需要依赖依赖 Spring 的 Method Validation AOP触发校验
@Service
@Validated
public class UserService {
public void save(@Valid/@Validated UserDTO dto) {
}
}另外返回值也可以校验 只是没有什么用
@Service
@Validated
public class UserService {
@Valid
public UserDTO getUser() {
return dto;
}
}Spring Validation 支持对 List 类型的参数进行容器元素校验,需要满足:
UserDTO类内部的字段上需要有校验注解(如@NotNull、@Size等)- 需要触发容器元素校验(Spring Boot 2.3+/Spring 5+ 默认支持)
// UserDTO 定义
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String name;
@Min(18)
private Integer age;
}
// Controller
@PostMapping
public void save(@RequestBody @Valid List<UserDTO> users) {
// 如果 users 中有任何一个元素校验失败,会抛出 MethodArgumentNotValidException
}如果spring版本过低需要及联校验
@PostMapping
public void save(@RequestBody @Valid List<@Valid UserDTO> users) {
// @Valid List<@Valid UserDTO> 中的第二个 @Valid 在某些版本中是显式声明
// 但现代 Spring Boot 中 @Valid List<UserDTO> 已足够
}
仅有 1 条评论
jcqkca