Spring Validation
什么是JSR
JSR(Java Specification Request) 是 Java Community Process(JCP) 框架下的标准化提案机制,用于定义和演化 Java Platform Specifications(JSR 流程正式批准的 Java 技术标准文档,其实就是相当于是一种正式的规范)。
什么是JCP,简单地说,JCP是一个由 Java 开发者、供应商和其他组织组成的国际社区,JSR是这个社区的提案机制,JSR提案是为了形成正式的规范。
形成的规范,比如下面这样:
| JSR 编号 | 名称 | 描述 | 发布时间 | 包前缀 |
|---|---|---|---|---|
| JSR 380 | Bean Validation 2.0 | Bean 验证规范(支持 Java 8+) | 2017 | javax.validation.* |
| JSR 349 | Bean Validation 1.1 | Bean 验证规范(CDI 集成等) | 2013 | javax.validation.* |
| JSR 303 | Bean Validation 1.0 | Bean 验证规范(最初版本) | 2009 | javax.validation.* |
| JSR 370 | Java EE 8 | Java EE 8 平台规范 | 2017 | 多个包 |
| JSR 366 | Java EE 8 (Platform) | Java EE 8 平台(Servlet 4.0 等) | 2017 | 多个包 |
| JSR 376 | Java Platform Module System | Java 9 模块系统(Project Jigsaw) | 2017 | java.lang.module |
| JSR 315 | Servlet 3.0 | Web 应用的 Servlet 规范 | 2009 | javax.servlet.* |
| JSR 340 | Servlet 3.1 | Servlet 规范更新 | 2013 | javax.servlet.* |
| JSR 369 | Servlet 4.0 | Servlet 规范(HTTP/2 支持) | 2017 | javax.servlet.* |
| JSR 220 | JPA 1.0 | Java 持久化 API(最初版本) | 2006 | javax.persistence.* |
| JSR 317 | JPA 2.0 | JPA 规范更新 | 2009 | javax.persistence.* |
| JSR 338 | JPA 2.1 | JPA 规范(主流版本) | 2013 | javax.persistence.* |
| JSR 250 | Common Annotations 1.0 | 通用注解(如 @Resource) | 2006 | javax.annotation.* |
| JSR 330 | Dependency Injection | 依赖注入规范(@Inject) | 2009 | javax.inject.* |
其中Bean校验相关的就是JSR 303(Bean Validation 1.0)、JSR 349(Bean Validation 1.1)、JSR 380(Bean Validation 2.0)了。
他们定义了Java Bean验证的标准API和注解集(如 @NotNull、@Size、@Email 等注解)。
但是,JSR规范本身只是接口标准,并不包含具体实现。
而Hibernate Validator是Bean Validation规范的官方参考实现,提供了规范中定义的所有标准注解的实现。Hibernate Validator是Java生态中最流行的Bean Validation实现。
Hibernate Validator除了实现JSR规范的标准注解外,还提供了一些自定义扩展注解,例如:
@Length(用于字符串长度验证)@Range(数值范围验证)@URL(URL格式验证)@CreditCardNumber(信用卡号验证)等
注意,这些扩展注解在org.hibernate.validator包下,不属于javax.validation或jakarta.validation标准包。javax.validation或jakarta.validation标准包?为什么说或呢?因为2017年Oracle 将 Java EE 捐赠给 Eclipse 基金会,在法律上有限制捐赠之后,validator不能使用 Oracle 的 javax.* 商标了,所以它改了包名。

我们现在用得比较多的也就是Bean Validation 2.0。所以,这里我们就介绍Bean Validation 2.0。
JSR标准的注解
JSR标准的注解定义在javax.validation.constraints或者jakarta.validation.constraints中。
一共定义了 22个注解(其中有好几个是Bean Validation 2.0定义的,比如@Positive、@Email等。其中 @Email 在 Bean Validation 1.0 中没有的,但是 Hibernate 扩展中扩展了,后面Bean Validation 2.0 把@Email吸纳为了标准)。
空和非空检查
-
@NotBlank :只能用于字符串不为 null ,并且字符串 #trim() 以后 length 要大于 0 ,即,如果值是tab 或者多个空格等都是被认定为空的。
-
@NotEmpty :如果一个字段被标记为 @NotEmpty,那么该字段的值不能是 null,同时也不能是空(例如空字符串、空集合、空数组等)。
-
@NotNull :不能为 null 。
-
@Null :必须为 null 。
后面的其他校验都是允许为null的,如果是null就直接通过了,上面这四个数对null有约束的,下面的几个都是对null没有约束的,即,null可以通过校验。
数值检查
-
@DecimalMax(value) :被注释的元素必须是一个数字,其值必须小于等于指定的最大值,如果字段值为 null,校验会通过。
-
@DecimalMin(value) :被注释的元素必须是一个数字,其值必须大于等于指定的最小值,如果字段值为 null,校验会通过。
-
@Digits(integer, fraction) :用于校验数字的整数部分和小数部分的位数,如果字段值为 null,校验会通过。比如,@Digits(integer = 3, fraction = 2)修饰一个属性,那么。合法示例:123.45(整数3位,小数2位)、12.3(整数2位,小数1位)、0.01,非法示例:1234.56(整数部分4位 > 3)、12.345(小数部分3位 > 2)。
-
@Positive :判断正数。如果字段值为 null,校验会通过。
-
@PositiveOrZero :判断正数或 0 。如果字段值为 null,校验会通过。
-
@Max(value) :该字段的值只能小于或等于该值。如果字段值为 null,校验会通过。
-
@Min(value) :该字段的值只能大于或等于该值。如果字段值为 null,校验会通过。
-
@Negative :判断负数。如果字段值为 null,校验会通过。
-
@NegativeOrZero :判断负数或 0 。如果字段值为 null,校验会通过。
Boolean 值检查
-
@AssertFalse 所注解的元素必须是Boolean或者boolean类型,且值为false。如果字段值为 null,校验会通过。
-
@AssertTrue 所注解的元素必须是Boolean或者boolean类型,且值为true。如果字段值为 null,校验会通过。
长度检查
- @Size(max, min) :检查该字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等。如果字段值为 null,校验会通过。
日期检查
-
@Future :被注释的元素必须是一个将来的时间。如果字段值为 null,校验会通过。
-
@FutureOrPresent :判断日期是否是将来或现在时间。如果字段值为 null,校验会通过。
-
@Past :检查该字段的时间是在过去。如果字段值为 null,校验会通过。
-
@PastOrPresent :判断日期是否是过去或现在时间。如果字段值为 null,校验会通过。
它们既适用于日期,也适用于时间,具体取决于字段的类型。
public class Example {
// ✅ 纯日期场景
@Past
private LocalDate birthDate; // 出生日期(过去日期)
@Future
private LocalDate deliveryDate; // 配送日期(未来日期)
// ✅ 日期时间场景
@Past
private LocalDateTime orderTime; // 下单时间(过去时刻)
@Future
private LocalDateTime appointmentTime; // 预约时间(未来时刻)
// ✅ 纯时间场景(较少用)
@Future
private LocalTime reminderTime; // 提醒时间(今天未来的时间)
}
其它检查
-
@Email :被注释的元素必须是电子邮箱地址。如果字段值为 null,校验会通过。
-
@Pattern(value) :被注释的元素必须符合指定的正则表达式。如果字段值为 null,校验会通过。
Hibernate Validator附加的约束注解
注解定义在org.hibernate.validator.constraints包中。这个包会扩展一些Hibernate Validator 一些好用的,但是没有在JSR规范中的注解。
这里有很多,我就简单介绍几个常用的:
-
Range(min = 1, max = 100):有效的值:1, 2, 3, …, 99, 100。用于校验数值类型的范围,相当于是 @Min 和 @Max 的组合。用于指定被注释的元素必须在合适的范围内。
-
@Length(min = 2, max = 50):专门用于校验字符串的长度,功能类似于标准的 @Size 注解,但语义更专一。用于指定被注释的字符串的大小必须在指定的范围内。边界值也是可以通过校验的。
-
@URL(protocol=“https”,host=“example.com”,port=443,regexp=“.*”,flags=Pattern.Flag.CASE_INSENSITIVE) :被注释的字符串必须是一个有效的 URL 。
例如:
// 基本校验(只检查 URL 格式)
@URL
private String website;
// ✅ 有效: "http://example.com", "https://www.google.com"
// ✅ 有效: "ftp://files.example.com", "mailto:test@example.com"
// ❌ 无效: "example.com", "www.google", "not-a-url"
// 指定端口号
@URL(protocol = "http", port = 8080)
private String localUrl;
// ✅ 有效: "http://localhost:8080", "http://example.com:8080"
// ❌ 无效: "http://example.com", "http://example.com:80"
// 只允许特定路径的 URL
@URL(regexp = ".*/api/.*")
private String apiUrl;
// ✅ 有效: "https://example.com/api/users"
// ✅ 有效: "http://localhost:8080/api/products"
// ❌ 无效: "https://example.com/home", "http://example.com"
依赖引入
<!-- Web 基础 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 验证功能。在 Spring Boot 2.3+ 版本后,需要自己显式引入 spring-boot-starter-validation 依赖。Spring Boot 2.3 之前spring-boot-starter-validation 作为 spring-boot-starter-web 的传递依赖自动引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Spring Boot 2.3 Release Notes 中的说明:
Spring Boot 2.3 has removed the dependency on validation starter from spring-boot-starter-web.
If you need validation features, you should add spring-boot-starter-validation as a dependency.
Spring Boot 2.3 及以后(≥ 2.3.0)spring-boot-starter-web 不再包含validation必须显式引入validation依赖。
说明:这里说的Spring Boot 2.3其实是指2.3.0版本的spring-boot-starter-web依赖,因为这些spring-boot-starter-web等springboot自己的依赖版本都是和springboot的版本配套的,所以习惯这么叫。
可能会看到的一些效果
我使用的是springboot2.4.5,引入了spring-boot-starter-web,但是我没有引入spring-boot-starter-validation,看到可以使用javax.validation.constraints中的注解(可以import),但是不能import到org.hibernate.validator.constraints中的注解,原因是什么?并且我虽然import到javax.validation.constraints中的注解,但是使用了没有任何校验的效果,但是我在pom.xml文件中引入了spring-boot-starter-validation依赖之后,发现javax.validation.constraints中的注解可以有校验效果了。
答:
- 可以import到javax.validation.constraints中的注解,是因为 starter-web 依赖了 spring-boot-starter,spring-boot-starter 依赖了 spring-boot-starter-validation。
- 引入spring-boot-starter-validation之前,javax.validation.constraints中的注解没有生效,是因为spring-boot-starter 虽然依赖了 spring-boot-starter-validation,但是starter-validation 只包含 API,不包含实现!
- 引入了spring-boot-starter-validation后,就生效了,是因为,引入之后,Spring中就有实现了,SpringBoot的自动装配效果就让它生效了。
Spring Validation、hibernate validation的关系
Spring Validation是对hibernate validation的二次封装,Spring Validation与 Spring MVC 无缝集成,支持分组校验实现不同场景使用不同的校验规则。
Java Bean Validation API (JSR-303/JSR-380) (规范标准)
↑
| 实现
|
Hibernate Validator (实现)
↑
| 封装
|
Spring Validation Framework
@Valid 和 @Validated
@Valid
@Valid 注解,是 JSR 所定义的注解。
它不支持分组功能。支持级联验证。
常见:
- 对controller层的形参中的复杂对象进行校验。无需类上面添加@Validated注解或者@Valid注解,只要在方法形参前面添加@Valid 注解就行,实体类内部添加校验注解就行。
- 实体类成员变量是复杂类型的。比如
private UserDTO user;和private List<ItemDTO> items;这样比较复杂的成员变量,你要校验的时候,就需要使用成员变量声明上添加 @Valid 注解,这样才能进行级联校验。即,会对这个成员变量中的UserDTO和ItemDTO进行校验。
不常用:
-
@Valid也可以用于在Service层使用参数校验,但是必须在类上添加Spring的@Validated注解。注意,你Service的方法形参写@Valid,但是类上面不写@Validated,只在Controller层的类上面写@Validated,是没有用的哈,不是调用链上有写就行的哈。
“在Service层(非Controller层)使用方法参数校验时,必须在类上添加Spring的@Validated注解,并且该类需要由Spring容器管理。这是因为:
- Controller层:Spring MVC框架通过
HandlerMethodValidationInterceptor自动支持@Valid校验,无需显式添加@Validated。校验失败时抛出MethodArgumentNotValidException或者BindException,具体什么时候抛出什么,后面会提到。 - Service层:Spring没有默认启用方法参数校验,需要通过
@Validated显式开启AOP拦截。校验失败时抛出ConstraintViolationException。 - 异常差异的原因:Controller层校验面向Web请求,包含完整的
BindingResult信息;Service层校验面向业务方法,使用通用的ConstraintViolation集合。两者由不同的拦截器实现。 - @Validated作用:它标记哪些Bean需要启用方法级别的参数校验拦截
- 校验原理:Spring为
@Validated标注的Bean创建AOP代理,添加MethodValidationInterceptor,在执行方法前调用Validator进行参数校验。”
- Controller层:Spring MVC框架通过
-
在方法上使用。效果是对方法的返回值进行验证,这种的做法用得很少。
例子1
// User.java - 嵌套对象
package com.example.dto;
import lombok.Data;
import javax.validation.constraints.*;
@Data
public class User {
@NotBlank(message = "用户名不能为空")
private String name;
@Min(value = 18, message = "年龄必须≥18")
private Integer age;
}
// Item.java - 另一个嵌套对象
package com.example.dto;
import lombok.Data;
import javax.validation.constraints.*;
@Data
public class Item {
@NotBlank(message = "商品名不能为空")
private String itemName;
@NotNull(message = "价格不能为空")
@Positive(message = "价格必须大于0")
private Double price;
}
// OrderRequest.java - 主请求对象
package com.example.dto;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.util.List;
@Data
public class OrderRequest {
@NotBlank(message = "订单号不能为空")
private String orderNo;
@NotNull(message = "用户信息不能为空")
@Valid // 关键:启用对User对象的校验
private User user;
@NotEmpty(message = "商品列表不能为空")
@Valid // 关键:启用对List中每个Item的校验
private List<Item> items;
}
package com.example.controller;
import com.example.dto.OrderRequest;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/test")
public class TestController {
/**
* 创建订单 - 测试级联校验
* 只需要在参数前加 @Valid
*/
@PostMapping("/createOrder")
public String createOrder(@Valid @RequestBody OrderRequest request) {
return "校验通过,订单号:" + request.getOrderNo();
}
}
测试数据:
{
"orderNo": "ORD001",
"user": {
"name": "张三",
"age": 17 // 会校验失败:年龄必须≥18
},
"items": [
{
"itemName": "手机",
"price": 2999.00
},
{
"itemName": "", // 会校验失败:商品名不能为空
"price": null // 会校验失败:价格不能为空
}
]
}
输出:
{
"code": 2001001002,
"message": "请求参数不合法:商品名不能为空;年龄必须≥18;价格不能为空",
"data": null
}
例子2
package com.yimeng.multiplemodules.demoadmin.controller;
import com.yimeng.multiplemodules.demoadmin.domain.Order;
import com.yimeng.multiplemodules.demoadmin.service.OrderService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/order")
@Validated // 要求添加@Validated,batchCreate上的@Valid才能生效,这一点需要注意一下,但是List的这种校验很少见。但是create、detail、list三个方法不需要类上添加@Validated注解
public class OrderController {
@Resource
private OrderService orderService;
@PostMapping("/create")
public String create(@RequestBody Order order) {
orderService.createOrder(order); // Service会校验
return "订单创建成功";
}
@GetMapping("/detail")
public Order detail(@RequestParam String orderNo) {
return orderService.getOrder(orderNo); // Service会校验
}
@GetMapping("/list")
public List<Order> list() {
return orderService.listOrders();
}
// 如果是要校验List中的每个元素,那么需要写两个@Valid,List前写一个,并且范型前也要写一个。Order内部要添加校验注解(如@NotBlank、@NotNull等)
@PostMapping("/batch-create")
public String batchCreate(@Valid @RequestBody List<@Valid Order> orders) {
// orderService.batchCreateOrders(orders);
return "批量创建订单成功,共" + orders.size() + "个订单";
}
}
package com.yimeng.multiplemodules.demoadmin.service;
import com.yimeng.multiplemodules.demoadmin.domain.Order;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
public interface OrderService {
// 复杂类型就必须要用@Valid,在controller层复杂参数你使用@Validated也行,因为controller层做了兼容,但是这里不行,还是得用@Valid
void createOrder(@Valid Order order); // 校验订单对象
Order getOrder(@NotNull @Positive(message = "订单ID必须大于0") String orderNo); // 校验字符串参数
// 返回值进行校验
@NotNull(message = "返回的订单不能为null")
@Size(min = 1, message = "至少返回一个订单")
List<Order> listOrders();
}
package com.yimeng.multiplemodules.demoadmin.service.impl;
import com.yimeng.multiplemodules.demoadmin.domain.Order;
import com.yimeng.multiplemodules.demoadmin.service.OrderService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; // 关键
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Service
@Validated // 如果不加@Validated注解,那么接口上的校验注解将会无效。虽然,这个注解加在接口上也行,但是建议加在实现类上,实现了决定是否要启用校验。具体怎么校验的@NotNull等注解,建议加在接口上。这样对调用方来说比较友好,查看接口就知道如何正确调用
public class OrderServiceImpl implements OrderService {
@Override
public void createOrder(Order order) {
// 校验通过才执行
System.out.println("创建订单:" + order.getOrderNo());
}
@Override
public Order getOrder(String orderNo) {
// orderNo已自动校验不为空
Order order = new Order();
order.setOrderNo(orderNo);
order.setAmount(new BigDecimal("100.00"));
return order;
}
@Override
public List<Order> listOrders() {
// 这个方法只测试返回值校验
// 如果返回null,会触发@NotNull校验失败
// 如果返回空列表,会触发@Size(min=1)校验失败
// 测试时可以修改这里:
// return null; // 触发 @NotNull 校验
return new ArrayList<>(); // 触发 @Size(min=1) 校验
// 正常返回
// List<Order> orders = new ArrayList<>();
// Order order = new Order();
// order.setOrderNo("ORD001");
// order.setAmount(new BigDecimal("100.00"));
// orders.add(order);
// return orders;
}
}
package com.yimeng.multiplemodules.demoadmin.domain;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import java.math.BigDecimal;
@Data
public class Order {
@NotBlank(message = "订单号不能为空")
private String orderNo;
@NotNull(message = "金额不能为空")
@Positive(message = "金额必须大于0")
private BigDecimal amount;
}
结果:
接口:/order/detail?orderNo=0
结果:
{
"code": 2001001002,
"message": "请求参数不合法:订单ID必须大于0",
"data": null
}
接口:/order/detail?orderNo=10
结果:
{
"orderNo": "10",
"amount": 100
}
接口:/order/list
结果:
{
"code": 2001001002,
"message": "请求参数不合法:至少返回一个订单",
"data": null
}
接口:/order/batch-create
请求体:
[
{
"amount": 0,
"orderNo": "1"
},
{
"amount": 0,
"orderNo": ""
}
]
结果:
{
"code": 2001001002,
"message": "请求参数不合法:金额必须大于0;订单号不能为空;金额必须大于0",
"data": null
}
@Validated
@Validated 注解,是Spring 框架提供的增强版。@Validated 是 Spring 框架独有的注解,不是 Hibernate 的,也不是 JSR 标准的一部分。他支持分组功能,但是不能做到级联校验,即,要级联校验,你还是得使用@Valid才行。但是注意,我看到在controller层的方法形参前,对复杂对象进行级联校验的时候,我们使用@Validated和@Valid的效果一样。这个是Spring做了一个兼容设计,让你controller层方法形参前写@Validated也能达到在方法形参前写@Valid一样的效果。底层把 @Validated 当作 @Valid 处理了。虽然,Spring做了兼容,但是尽量我们在controller层要对方法形参中的复杂对象进行级联校验的时候,还是用@Valid比较好,这样规范一点。还有就是要注意,虽然controller层做了兼容,但是在实体类中的复杂成员变量,你加 @Validated 是无效的,idea的编译都无法通过。并且你在Service层的形参前添加@Validated,也无效,即只有在controller层的方法形参前,你用@Validated和@Valid是一样的,抛出的异常也是一样的,和使用@Valid一样都是MethodArgumentNotValidException。
@Validated最显著的特性是支持分组校验,这是标准@Valid注解所不具备的。
常用:
- controller类上面添加@Validated,启用单参数校验。启用之后,就可以对方法形参的简单参数进行校验了,形参前可以添加@Min等注解对简单参数进行校验。注意,如果形参是复杂对象,那么就要添加@Valid了,这个是属于前面说的级联校验了,controller层的级联校验不需要在类上添加@Validated的。controller类上面添加@Validated,启用单参数校验之后,简单参数的校验没有通过,会抛出ConstraintViolationException异常。
- 需要分组校验时,在controller层的方法形参前使用。如果是复杂对象,但是是要进行分组的,我们可以在方法形参前用类似@Validated(ValidationGroups.Create.class)这样的注解进行分组级联校验(可以看到这里也用@Validated代替了@Valid,因为Spring做了兼容,在controller层使用@Validated和@Valid都能进行级联校验。这里这个分组使用@Validated代替了@Valid进行复杂对象的校验才是Spring做控制层@Validated兼容@Valid的原因吧)。
注意:String、Boolean、Integer、Long、LocalDateTime等都属于基本类型/简单参数。
不常用的场景:
- 在controller层外的类上使用@Validated。比如,前面说过的,在Service层要想要支持校验,就必须在类上面写@Validated注解,这样Service层方法的简单类型形参前面写的@Min这样的注解才有用,复杂的对象前面的@Valid才有用。
例子1
package com.yimeng.multiplemodules.demoadmin.controller;
import com.yimeng.multiplemodules.demoadmin.domain.Product;
import com.yimeng.multiplemodules.demoadmin.group.ValidationGroups;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
@RestController
@RequestMapping("/product")
@Validated // 启用单参数校验。开启简单参数校验需要在类上添加这个注解。
public class ProductController {
// 1. 简单参数校验
@GetMapping("/stock")
public String checkStock(
@NotNull(message = "商品ID不能为空")
@Min(value = 1000, message = "商品ID最小1000")
Long productId) {
return "商品库存正常";
}
// 2. 默认分组校验(分组校验没有要求类上一定要加@Validated)
@PutMapping("/update")
public String updateProduct(
@Validated(ValidationGroups.Update.class) @RequestBody Product product) {
return "更新成功";
}
// 3. 创建分组校验(分组校验没有要求类上一定要加@Validated)
@PostMapping("/create")
public String createProduct(
@Validated(ValidationGroups.Create.class)
@RequestBody Product product) {
return "创建成功";
}
}
package com.yimeng.multiplemodules.demoadmin.domain;
import com.yimeng.multiplemodules.demoadmin.group.ValidationGroups;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
@Data
public class Product {
// 默认分组(这个注解没有配置groups,那么这个校验注解就属于默认分组(Default.class),因为ValidationGroups.Create.class和ValidationGroups.Update.class都继承了javax.validation.groups.Default,所以,@Validated(ValidationGroups.Create.class)和@Validated(ValidationGroups.Update.class会校验这个字段)
@NotNull(message = "商品ID不能为空")
private Long id;
// 创建分组(只对开启@Validated(ValidationGroups.Create.class)的controller方法有效。)
@NotBlank(message = "商品名称必填", groups = ValidationGroups.Create.class)
// @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class})// 如果要校验多个,可以这样写
private String name;
// 更新分组(只对开启@Validated(ValidationGroups.Update.class)的controller方法有效。)
@Positive(message = "价格必须大于0", groups = ValidationGroups.Update.class)
private Double price;
}
package com.yimeng.multiplemodules.demoadmin.group;
import javax.validation.groups.Default;
public interface ValidationGroups {
interface Create extends Default {} // 创建时校验组
interface Update extends Default{} // 更新时校验组
}
执行效果:
接口:http://localhost:8080/product/stock?productId=99
结果:
{
"code": 2001001002,
"message": "请求参数不合法:商品ID最小1000",
"data": null
}
接口:http://localhost:8080/product/stock?productId=1001
结果:
商品库存正常
接口:http://localhost:8080/product/create
请求体:
{
"id": null,
"name": " ",
"price": -1 // 看到没有校验price,只校验了name和id
}
结果:
{
"code": 2001001002,
"message": "请求参数不合法:商品ID不能为空;商品名称必填",
"data": null
}
接口:http://localhost:8080/product/update
请求体:
{
"id": null,
"name": " ",
"price": -1 // 看到校验了price和id,没有校验name
}
结果:
{
"code": 2001001002,
"message": "请求参数不合法:价格必须大于0;商品ID不能为空",
"data": null
}
Controller层三种校验场景
| 场景 | 参数注解 | 绑定方式 | 是否需要@Validated | 异常类型 |
|---|---|---|---|---|
| @RequestBody | @RequestBody @Valid | RequestResponseBodyMethodProcessor | ❌ 不需要 | MethodArgumentNotValidException |
| @ModelAttribute | @Valid | ServletModelAttributeMethodProcessor | ❌ 不需要 | BindException |
| 单参数 | @Min、@NotBlank等 | 各种ArgumentResolver | ✅ 需要 | ConstraintViolationException |
注意:BindException是 MethodArgumentNotValidException 的父类。
// Controller层的三种校验场景:
// 1. @RequestBody + @Valid(JSON请求)
@PostMapping("/json")
public Result jsonMethod(@RequestBody @Valid DTO dto) {
// 失败抛出:MethodArgumentNotValidException
// 不需要@Validated
}
// 2. 无注解对象参数 + @Valid(表单/GET请求)
@PostMapping("/form")
public Result formMethod(@Valid DTO dto) {
// 自动作为@ModelAttribute
// 失败抛出:BindException
// 不需要@Validated
}
// 3. 单参数校验(@RequestParam/@PathVariable等)
@GetMapping("/single/{id}")
public Result singleMethod(
@PathVariable @Min(1) Long id,
@RequestParam @NotBlank String name) {
// 失败抛出:ConstraintViolationException
// 需要类上添加@Validated ✅
}
全局异常处理器
package com.yimeng.multiplemodules.democommon.exception;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.yimeng.multiplemodules.democommon.domain.CommonResult;
import com.yimeng.multiplemodules.democommon.enums.ServiceExceptionEnum;
@ControllerAdvice(basePackages = "com.yimeng.multiplemodules.demoadmin.controller")
public class GlobalExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* 处理 SpringMVC 请求参数缺失异常
* 触发场景:
* 1. @RequestParam(required = true) 且前端未传该参数
* 2. 例如:@RequestParam String name 但请求中没有name参数
* 注意:这是Spring MVC框架异常,不是校验框架异常
*/
@ResponseBody
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public CommonResult missingServletRequestParameterExceptionHandler(HttpServletRequest req, MissingServletRequestParameterException ex) {
logger.info("========进入了 missingServletRequestParameterExceptionHandler,出现MissingServletRequestParameterException异常======");
logger.error("[missingServletRequestParameterExceptionHandler]", ex);
return CommonResult.error(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());
}
/**
* 处理参数校验失败异常(ConstraintViolationException)
* 触发场景:
* 1. Controller类上有@Validated,且方法参数使用@NotNull、@Min等单参数校验失败
* 2. Service层类上有@Validated,且方法参数校验失败
* 3. 例如:@GetMapping public void test(@Min(1) Long id) 传id=0
* 特点:针对单个简单参数的校验异常
*/
@ResponseBody
@ExceptionHandler(value = ConstraintViolationException.class)
public CommonResult constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
logger.info("========进入了 constraintViolationExceptionHandler,出现ConstraintViolationException异常======");
logger.error("[constraintViolationExceptionHandler]", ex);
StringBuilder detailMessage = new StringBuilder();
for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
detailMessage.append(constraintViolation.getMessage());
}
return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
/**
* 处理参数绑定异常(BindException)
* 触发场景:
* 1. GET请求使用@ModelAttribute绑定对象时校验失败
* 2. 表单提交时数据绑定失败
* 例如:@GetMapping public void test(@Valid Model model)
* 注意:这是数据绑定过程中的校验异常
*/
@ResponseBody
@ExceptionHandler(value = BindException.class)
public CommonResult bindExceptionHandler(HttpServletRequest req, BindException ex) {
logger.info("========进入了 bindExceptionHandler,出现BindException异常======");
logger.error("[bindExceptionHandler]", ex);
StringBuilder detailMessage = new StringBuilder();
for (ObjectError objectError : ex.getAllErrors()) {
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
detailMessage.append(objectError.getDefaultMessage());
}
return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
/**
* 处理请求参数校验失败异常(MethodArgumentNotValidException)
* 触发场景:
* 1. POST/PUT请求使用@RequestBody,且对象校验失败
* 2. 使用@Valid或@Validated校验复杂对象失败
* 例如:@PostMapping public void create(@Valid @RequestBody User user)
* 特点:这是Controller层处理复杂对象校验的主要异常类型
*/
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public CommonResult MethodArgumentNotValidExceptionHandler(HttpServletRequest req, MethodArgumentNotValidException ex) {
logger.info("========进入了 MethodArgumentNotValidExceptionHandler,出现MethodArgumentNotValidException异常======");
logger.error("[MethodArgumentNotValidException]", ex);
StringBuilder detailMessage = new StringBuilder();
for (ObjectError objectError : ex.getBindingResult().getAllErrors()) {
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
detailMessage.append(objectError.getDefaultMessage());
}
return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
/**
* 处理其它 Exception 异常
* @param req 请求
* @param e 异常
* @return 返回统一错误结果
*/
@ResponseBody
@ExceptionHandler(value = Exception.class)
public CommonResult exceptionHandler(HttpServletRequest req, Exception e) {
logger.info("========进入了 exceptionHandler,出现Exception异常。======");
logger.error("[exceptionHandler]", e);
return CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(),
ServiceExceptionEnum.SYS_ERROR.getMessage());
}
}
package com.yimeng.multiplemodules.democommon.enums;
/**
* 业务异常枚举
*
* @author shiliang liu
* @version 1.0.0
* @since 2026-01-20
*/
public enum ServiceExceptionEnum {
// ========== 系统级别 ==========
SYS_ERROR(2001001000, "服务端发生异常"),
MISSING_REQUEST_PARAM_ERROR(2001001001, "参数缺失"),
INVALID_REQUEST_PARAM_ERROR(2001001002, "请求参数不合法"),
// ========== 用户模块 ==========
USER_NOT_FOUND(1001002000, "用户不存在"),
// ========== 订单模块 ==========
// ========== 商品模块 ==========
;
/**
* 错误码
*/
private final int code;
/**
* 错误提示
*/
private final String message;
ServiceExceptionEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
自定义校验注解和校验规则(例1)
package com.yimeng.multiplemodules.demoadmin.annotation;
import com.yimeng.multiplemodules.demoadmin.validator.StrongPasswordValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* 密码强度校验注解
* 简单版:只检查长度和是否包含数字字母
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = StrongPasswordValidator.class)
@Documented
public @interface StrongPassword {
// 默认错误信息
String message() default "密码强度不足";
// 校验分组
Class<?>[] groups() default {};
// 负载信息
Class<? extends Payload>[] payload() default {};
// 最小长度(默认8位)
int minLength() default 8;
// 最大长度(默认20位)
int maxLength() default 20;
// 是否需要包含数字(默认需要)
boolean requireDigit() default true;
// 是否需要包含字母(默认需要)
boolean requireLetter() default true;
// 是否允许特殊字符(默认允许)
boolean allowSpecialChar() default true;
}
package com.yimeng.multiplemodules.demoadmin.validator;
import com.yimeng.multiplemodules.demoadmin.annotation.StrongPassword;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 密码强度校验器
*/
public class StrongPasswordValidator
implements ConstraintValidator<StrongPassword, String> {
private int minLength;
private int maxLength;
private boolean requireDigit;
private boolean requireLetter;
private boolean allowSpecialChar;
@Override
public void initialize(StrongPassword constraintAnnotation) {
// 获取注解配置
this.minLength = constraintAnnotation.minLength();
this.maxLength = constraintAnnotation.maxLength();
this.requireDigit = constraintAnnotation.requireDigit();
this.requireLetter = constraintAnnotation.requireLetter();
this.allowSpecialChar = constraintAnnotation.allowSpecialChar();
}
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
// 1. 如果密码为空,由 @NotBlank 处理
if (password == null) {
return true;
}
// 2. 检查长度
if (password.length() < minLength) {
setErrorMessage(context, "密码长度至少" + minLength + "位");
return false;
}
if (password.length() > maxLength) {
setErrorMessage(context, "密码长度不能超过" + maxLength + "位");
return false;
}
// 3. 检查是否包含数字
if (requireDigit && !containsDigit(password)) {
setErrorMessage(context, "密码必须包含数字");
return false;
}
// 4. 检查是否包含字母
if (requireLetter && !containsLetter(password)) {
setErrorMessage(context, "密码必须包含字母");
return false;
}
// 5. 检查是否允许特殊字符
if (!allowSpecialChar && containsSpecialChar(password)) {
setErrorMessage(context, "密码不能包含特殊字符");
return false;
}
return true;
}
// 检查是否包含数字
private boolean containsDigit(String str) {
for (char c : str.toCharArray()) {
if (Character.isDigit(c)) {
return true;
}
}
return false;
}
// 检查是否包含字母
private boolean containsLetter(String str) {
for (char c : str.toCharArray()) {
if (Character.isLetter(c)) {
return true;
}
}
return false;
}
// 检查是否包含特殊字符
private boolean containsSpecialChar(String str) {
String specialChars = "!@#$%^&*()_+-=[]{}|;:'\",.<>?/`~";
for (char c : str.toCharArray()) {
if (specialChars.indexOf(c) != -1) {
return true;
}
}
return false;
}
// 设置自定义错误信息
private void setErrorMessage(ConstraintValidatorContext context, String message) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message)
.addConstraintViolation();
}
}
package com.yimeng.multiplemodules.demoadmin.domain;
import com.yimeng.multiplemodules.demoadmin.annotation.StrongPassword;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class UserRegisterDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
@StrongPassword(
minLength = 6,
maxLength = 16,
allowSpecialChar = false,
message = "密码必须是6-16位字母数字组合"
)
private String password;
}
测试1(controller来测试,controller会自动进行校验的。):
package com.yimeng.multiplemodules.demoadmin.controller;
import com.yimeng.multiplemodules.demoadmin.domain.UserRegisterDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
public class TestController {
@PostMapping("/test-password")
public String testPassword(@Valid @RequestBody UserRegisterDTO dto) {
return "密码校验通过: " + dto.getUsername();
}
}
测试2(使用测试类来测试,没有controller,那么需要手动来校验):
package com.yimeng.multiplemodules.demoadmin.validation;
import com.yimeng.multiplemodules.demoadmin.domain.UserRegisterDTO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.util.Set;
@SpringBootTest
class StrongPasswordValidatorTest {
@Autowired
private Validator validator;
@Test
void testValidPassword() {
UserRegisterDTO dto = new UserRegisterDTO();
dto.setUsername("test");
dto.setPassword("Abc12345"); // 有效密码
Set<ConstraintViolation<UserRegisterDTO>> violations =
validator.validate(dto);
System.out.println("有效密码测试:");
if (violations.isEmpty()) {
System.out.println("✓ 密码校验通过");
} else {
violations.forEach(v -> System.out.println("✗ " + v.getMessage()));
}
}
@Test
void testInvalidPassword() {
UserRegisterDTO dto = new UserRegisterDTO();
dto.setUsername("test");
dto.setPassword("123"); // 太短,没有字母
Set<ConstraintViolation<UserRegisterDTO>> violations =
validator.validate(dto);
System.out.println("\n无效密码测试:");
if (!violations.isEmpty()) {
violations.forEach(v -> System.out.println("✗ " + v.getMessage()));
}
}
@Test
void testPasswordScenarios() {
System.out.println("\n各种密码测试:");
testPassword("123456", "应该失败:没有字母");
testPassword("abcdef", "应该失败:没有数字");
testPassword("Abc123", "应该通过:有字母和数字");
testPassword("Abc@123", "应该通过:有特殊字符");
testPassword("A1", "应该失败:太短");
testPassword("A1b2c3d4e5f6g7h8i9j0", "应该失败:太长");
}
private void testPassword(String password, String scenario) {
UserRegisterDTO dto = new UserRegisterDTO();
dto.setUsername("test");
dto.setPassword(password);
Set<ConstraintViolation<UserRegisterDTO>> violations =
validator.validate(dto);
System.out.println(String.format("%-30s: %s",
scenario,
violations.isEmpty() ? "✓ 通过" : "✗ " + violations.iterator().next().getMessage()
));
}
}
自定义校验注解和校验规则(例2)
package com.yimeng.multiplemodules.demoadmin.controller;
import com.yimeng.multiplemodules.demoadmin.domain.UserDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequestMapping("/user/V2")
public class UserControllerV2 {
/**
* 创建用户 - 演示枚举值校验
*/
@PostMapping("/create")
public String createUser(@Valid @RequestBody UserDTO userDTO) {
return "用户创建成功";
}
}
package com.yimeng.multiplemodules.demoadmin.domain;
import com.yimeng.multiplemodules.democommon.enums.UserGender;
import com.yimeng.multiplemodules.democommon.validation.EnumValue;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@NotNull(message = "年龄不能为空")
private Integer age;
@EnumValue(enumClass = UserGender.class, enumMethod = "getCode", message = "性别不合法")
private Integer gender;
}
package com.yimeng.multiplemodules.democommon.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValueValidator.class)
@Documented
public @interface EnumValue {
String message() default "值不在枚举范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 枚举类
Class<? extends Enum<?>> enumClass();
// 是否允许为空
boolean nullable() default false;
// 获取枚举值的方法名(默认使用name())
String enumMethod() default "name";
}
package com.yimeng.multiplemodules.democommon.enums;
import lombok.Getter;
@Getter
public enum UserGender {
UNKNOWN(0, "未知"),
MALE(1, "男"),
FEMALE(2, "女");
private final Integer code;
private final String desc;
UserGender(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
}
package com.yimeng.multiplemodules.democommon.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Method;
/**
* 枚举值校验器
*/
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
private Class<? extends Enum<?>> enumClass;
private boolean nullable;
private String enumMethod;
@Override
public void initialize(EnumValue constraintAnnotation) {
this.enumClass = constraintAnnotation.enumClass();
this.nullable = constraintAnnotation.nullable();
this.enumMethod = constraintAnnotation.enumMethod();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) {
return nullable;
}
try {
// 获取枚举的所有值
Object[] enumValues = enumClass.getEnumConstants();
if (enumValues == null) {
return false;
}
// 反射调用指定的方法获取枚举值
Method method = enumClass.getMethod(enumMethod);
for (Object enumValue : enumValues) {
Object enumValueStr = method.invoke(enumValue);
if (value.toString().equals(enumValueStr.toString())) {
return true;
}
}
return false;
} catch (Exception e) {
return false;
}
}
}
请求:
{
"age": 0,
"gender": 1,
"username": "1"
}
结果:
用户创建成功
请求:
{
"age": 0,
"gender": 3,
"username": "1"
}
结果:
{
"code": 2001001002,
"message": "请求参数不合法:性别不合法",
"data": null
}
跨字段校验
@AssertTrue注解 + Lombok
package com.yimeng.multiplemodules.demoadmin.controller;
import com.yimeng.multiplemodules.demoadmin.domain.UserQueryDTOV1;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/user")
public class UserQueryControllerV1 {
/**
* 查询用户列表 - 使用@AssertTrue跨字段校验
*/
@PostMapping("/query")
public String queryUsers(@Valid @RequestBody UserQueryDTOV1 queryDTO) {
// 如果校验失败(如开始时间晚于结束时间),会抛出MethodArgumentNotValidException
// 不会执行到这里
return "查询成功";
}
}
package com.yimeng.multiplemodules.demoadmin.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.*;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserQueryDTOV1 {
@NotBlank(message = "用户名不能为空")
private String username;
@NotNull(message = "开始时间不能为空")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime;
@NotNull(message = "结束时间不能为空")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime;
// 跨字段校验:开始时间不能晚于结束时间
@AssertTrue(message = "开始时间不能晚于结束时间")
public boolean isTimeRangeValid() {
if (startTime == null || endTime == null) {
return true; // 空值由@NotNull处理
}
return !startTime.isAfter(endTime);
}
}
请求:
{
"endTime": "2026-01-02 00:00:00",
"startTime": "2026-01-02 02:23:12",
"username": "张三"
}
结果:
{
"code": 2001001002,
"message": "请求参数不合法:开始时间不能晚于结束时间",
"data": null
}
工作原理:
- 校验触发时机:当Spring验证框架处理DTO对象时
- 执行流程:
- Hibernate Validator扫描所有带有
@AssertTrue注解的方法 - 反射调用该方法(方法名任意,只要返回boolean)
- 如果返回
false,则校验失败
- Hibernate Validator扫描所有带有
使用自定义注解
package com.yimeng.multiplemodules.demoadmin.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.yimeng.multiplemodules.democommon.validation.DateRangeV2;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.*;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DateRangeV2(
startField = "startTime",
endField = "endTime",
message = "查询时间范围无效:开始时间不能晚于结束时间"
)
public class UserQueryDTOV2 {
@NotBlank(message = "用户名不能为空")
private String username;
@NotNull(message = "开始时间不能为空")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime;
@NotNull(message = "结束时间不能为空")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime;
}
package com.yimeng.multiplemodules.demoadmin.controller;
import com.yimeng.multiplemodules.demoadmin.domain.UserQueryDTOV2;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v2/user")
public class UserQueryControllerV2 {
/**
* 查询用户列表 - 使用@DateRangeV2自定义注解跨字段校验
*/
@PostMapping("/query")
public String queryUsers(@Valid @RequestBody UserQueryDTOV2 queryDTO) {
// 如果校验失败(如开始时间晚于结束时间),会抛出MethodArgumentNotValidException
// 不会执行到这里
return "查询用户列表成功";
}
}
package com.yimeng.multiplemodules.democommon.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateRangeValidatorV2.class)
@Documented
public @interface DateRangeV2 {
String message() default "开始时间不能晚于结束时间";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String startField();
String endField();
}
package com.yimeng.multiplemodules.democommon.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
public class DateRangeValidatorV2 implements ConstraintValidator<DateRangeV2, Object> {
private String startField;
private String endField;
private String message;
@Override
public void initialize(DateRangeV2 constraintAnnotation) {
this.startField = constraintAnnotation.startField();
this.endField = constraintAnnotation.endField();
this.message = constraintAnnotation.message();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
try {
Field start = value.getClass().getDeclaredField(startField);
Field end = value.getClass().getDeclaredField(endField);
start.setAccessible(true);
end.setAccessible(true);
LocalDateTime startTime = (LocalDateTime) start.get(value);
LocalDateTime endTime = (LocalDateTime) end.get(value);
if (startTime == null || endTime == null) {
return true;
}
if (startTime.isAfter(endTime)) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(startField)
.addConstraintViolation();
return false;
}
return true;
} catch (Exception e) {
return false;
}
}
}
请求:
{
"endTime": "2026-01-02 00:00:00",
"startTime": "2026-01-02 00:12:12",
"username": "李四"
}
结果:
{
"code": 2001001002,
"message": "请求参数不合法:查询时间范围无效:开始时间不能晚于结束时间",
"data": null
}
原理:
┌─────────────────────────────────────────────────────────────────┐
│ HTTP请求到达 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ DispatcherServlet接收请求 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 找到对应的Controller方法并准备参数 │
│ 发现@Valid注解,触发校验机制 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Spring Validation启动 │
│ (集成Hibernate Validator作为实现) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ 递归遍历对象属性 │
│ 对每个字段/类检查是否有注解 │
└───────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ 发现 @DateRange 注解 │
│ (注解在类级别,标记整个类) │
└───────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ 执行自定义校验的核心流程 │
│ (见下方详细步骤1-6) │
└───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 校验结果处理 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 校验通过 │ ───────────> │进入Controller│ │
│ └─────────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 校验失败 │ ───────────> │ 抛出异常 │ │
│ └─────────────┘ │ MethodArgumentNotValidException│
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
步骤1:查找Constraint定义
┌─────────────────────────────────────────────────────────────┐
│ @Target({ElementType.TYPE}) │
│ @Retention(RetentionPolicy.RUNTIME) │
│ @Constraint(validatedBy = DateRangeValidator.class) ← 关键! │
│ public @interface DateRange { │
│ String message(); │
│ String startField(); │
│ String endField(); │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
步骤2:Validator实例化(首次调用时)
┌─────────────────────────────────────────────────────────────┐
│ ConstraintValidatorManager缓存检查 │
│ │
│ if (缓存中没有DateRangeValidator的实例) { │
│ // 通过反射创建新实例 │
│ Class<?> validatorClass = DateRangeValidator.class; │
│ DateRangeValidator validator = │
│ (DateRangeValidator) validatorClass.newInstance(); │
│ │
│ // 放入缓存,下次直接使用 │
│ cache.put(key, validator); │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
步骤3:调用initialize()方法
┌─────────────────────────────────────────────────────────────┐
│ // Hibernate Validator内部调用 │
│ validator.initialize(annotation); ← 传入注解实例 │
│ │
│ public class DateRangeValidator implements │
│ ConstraintValidator<DateRange, Object> { │
│ │
│ private String startField; │
│ private String endField; │
│ │
│ @Override │
│ public void initialize(DateRange annotation) { │
│ // 从注解中获取参数值 │
│ this.startField = annotation.startField(); ← "startTime"
│ this.endField = annotation.endField(); ← "endTime"
│ this.message = annotation.message(); │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
步骤4:调用isValid()执行校验
┌─────────────────────────────────────────────────────────────┐
│ // Hibernate Validator调用 │
│ boolean isValid = validator.isValid(dtoObject, context); │
│ │
│ @Override │
│ public boolean isValid(Object value, │
│ ConstraintValidatorContext context) { │
│ try { │
│ // value就是被校验的DTO对象 │
│ // 1. 通过反射获取字段值 │
│ Field startField = value.getClass() │
│ .getDeclaredField(this.startField); // "startTime"
│ startField.setAccessible(true); │
│ LocalDateTime startTime = │
│ (LocalDateTime) startField.get(value); │
│ │
│ Field endField = value.getClass() │
│ .getDeclinedField(this.endField); // "endTime"
│ endField.setAccessible(true); │
│ LocalDateTime endTime = │
│ (LocalDateTime) endField.get(value); │
│ │
│ // 2. 执行业务校验逻辑 │
│ if (startTime.isAfter(endTime)) { │
│ // 3. 校验失败,构建错误信息 │
│ context.disableDefaultConstraintViolation(); │
│ context.buildConstraintViolationWithTemplate( │
│ this.message) │
│ .addPropertyNode(startField) │
│ .addConstraintViolation(); │
│ return false; │
│ } │
│ return true; │
│ } catch (Exception e) { │
│ return false; │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
步骤5:ConstraintValidatorContext的作用
┌─────────────────────────────────────────────────────────────┐
│ // 这个接口用于构建详细的错误信息 │
│ public interface ConstraintValidatorContext { │
│ // 禁用默认的约束违例信息 │
│ void disableDefaultConstraintViolation(); │
│ │
│ // 构建新的约束违例信息 │
│ ConstraintViolationBuilder │
│ buildConstraintViolationWithTemplate(String message);│
│ } │
│ │
│ // 使用示例: │
│ context.disableDefaultConstraintViolation(); │
│ context.buildConstraintViolationWithTemplate( │
│ "开始时间不能晚于结束时间") │
│ .addPropertyNode("startTime") // 指定哪个字段出错 │
│ .addConstraintViolation(); │
└─────────────────────────────────────────────────────────────┘
│
▼
步骤6:返回结果给验证框架
┌─────────────────────────────────────────────────────────────┐
│ // Hibernate Validator接收返回值 │
│ if (!isValid) { │
│ // 从context中获取构建好的错误信息 │
│ Set<ConstraintViolation<?>> violations = │
│ context.getConstraintViolations(); │
│ │
│ // 将这些违例添加到最终结果中 │
│ validationResult.addAll(violations); │
│ } │
│ │
│ // 最终,如果有任何违例,抛出异常 │
│ if (!validationResult.isEmpty()) { │
│ throw new MethodArgumentNotValidException( │
│ parameter, validationResult); │
│ } │
└─────────────────────────────────────────────────────────────┘
提问
-
@Valid 能不能放在方法上,表示对方法返回值进行校验。
答:可以。
-
@Validated 能不能放在类中的某个方法上,而不是放整个在类上,表示对这一个方法进行校验。是不是要求方法一定是public的。
答:不能放在某个方法上,必须放在类上。是要求方法必须是public的,这样才能AOP。
-
使用@Validated的时候,是不是要求类必须是Spring管理的
答:是的,因为要使用AOP。
-
@Valid校验的实体类内部可以使用的注解不会只能是JSR的注解吧,能不能使用Hibernate Validator的注解,能不能使用自己定义的校验注解,自己定义校验规则?
答:@Valid校验的实体类内部可以使用JSR的注解,也可以使用Hibernate扩展的注解,还能使用自己定义的校验注解,并且使用你自己定义的规则对这个注解进行校验。
-
如果一个实体类有多个成员变量上的注解校验不通过,会提示某一个校验注解的message信息还是全部不通过的注解的message。
答:校验器会检查所有约束条件,不会因为第一个失败就停止。它会把所有的不通过的校验的提示内容都给你组合起来一起给你,然后给你一个异常。
我总结的最佳实践
最佳实践:
- 控制层校验。如果有简单参数校验,类上添加@Validated,形参前添加@Min(1)等校验注解,如果有复杂参数校验,形参前面添加@Valid,如果需要使用分组校验,那么我们就使用@Validated代替@Valid(controller层的@Validated是可以做复杂类型的实体类校验的),并且实体类中添加@Min(1)等校验注解,如果实体类中有复杂类型属性、数组属性、集合属性,要在属性上面添加@Valid,才能进行嵌套校验。
- excel导入时的校验,你可以在实体类的属性上添加校验注解,然后手动调用validator.validate(dto);进行基础校验,然后自己写代码进行业务校验。因为导入不好在controller方法形参上进行自动校验,形参可能是这样的
public Result importExcel(MultipartFile file),而不是你要的实体类,而手动调用validator.validate(dto);其实就是相当于去使用dto上的校验注解进行校验,只是非导入的Spring MVC参数校验是内部自动调用validator.validate(dto);的,这里我们是自己手动调用的而已。Spring MVC 自动校验的实质就是,Spring 内部自动执行了validator.validate(dto);而我们手动调用其实就是模拟了SpringMVC自动校验的这个过程:Set<ConstraintViolation<ImportDTO>> violations = validator.validate(dto); - 创建全局异常处理器,对MethodArgumentNotValidException、BindException、ConstraintViolationException进行处理。
- 如果是新增、修改不同操作要进行的校验是不同的,我们可以把校验注解进行分类,比如@NotNull(groups = UpdateGroup.class)给校验添加到UpdateGroup组,然后在controller层的更新API的复杂参数前添加@Validated(UpdateGroup.class)这样的注解。
- 跨字段校验、项目中可能会重复使用的特定格式校验、条件性校验(当字段A为特定值时,字段B必填)、不涉及业务逻辑的特定格式校验(比如密码强度校验)这些情况,我们可以选择自定义校验注解来完成校验。
更多推荐



所有评论(0)