Spring Boot Service 层参数校验:@Validated 与 @Valid 的底层原理与实战(hello-service-validation)
1. 摘要
本文针对Spring Boot项目中 必填字段漏赋值导致的脏数据问题,提出了架构级的解决方案:在Service层通过Bean Validation实现运行期强约束校验。
文章基于Spring Boot 4.0.6 + JDK 21技术栈,从项目搭建、依赖引入、代码实现到测试验证,完整演示了Service层参数校验的落地流程,并深入拆解了@Validated与@Valid的底层工作原理,厘清了Spring AOP代理机制与方法级校验的关系,同时提供了可直接复刻运行的完整项目代码。
2. 引言
你是否遇到过这样的问题:业务代码逻辑完全正确,测试也通过了,上线后却发现数据库里出现了大量account为null的操作记录?排查半天,最终发现只是因为某一行代码漏写了operationLogDto.setAccount(userAddRequest.getAccount());。
这种因人为编码疏漏导致的脏数据问题,在企业级开发中极为常见。传统的解决方案依赖人工代码审查和测试覆盖,但无法从根本上杜绝问题——只要是人写的代码,就有可能漏写。
本文将从架构层面给出彻底的解决方案:利用Spring Validation在Service层实现必填字段的强约束校验。我们不仅会手把手教你如何用@Validated + @Valid实现Service层参数校验,还会深入底层,解答那个困扰很多开发者的核心问题:
为什么Controller层有时只加
@Valid就能生效,而Service层必须同时加@Validated和@Valid两个注解?
文章最后会附上完整的项目源码,所有代码均基于最新的Spring Boot 4.0.6和JDK 21编写,你可以直接下载运行,快速应用到自己的项目中。
3. 痛点直击:为什么我们需要 Service 层参数校验?
- 真实业务场景:操作日志必填字段频繁缺失
- 传统解决方案的致命缺陷:依赖人工,无法根治
- 架构级思路:用运行期强约束替代人工检查
4. 实战篇:Spring Boot 4 Service 层校验从零搭建(hello-service-validation)
4.1. 环境准备:Spring Boot 4.0.6 + JDK 21 项目初始化
- SpringBoot版本:4.0.6
- JDK版本:21

4.2. 核心依赖:引入 spring-boot-starter-validation
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
4.3. 核心实现:Service 层添加 @Validated 与 @Valid 注解
核心注解:@Validated 和 @Valid 。
package com.example.hello_service_validation.operation_log;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Slf4j
@Service
@Validated
public class OperationLogService {
public void insert(@Valid OperationLogDto dto) {
OperationLog operationLog = new OperationLog();
operationLog.setAccount(dto.getAccount());
// 其他字段...
log.info("操作记录写入数据库:OperationLog={}", operationLog);
}
}
4.4. 约束配置:用 @NotBlank 定义必填字段规则
核心注解: @NotBlank 。
package com.example.hello_service_validation.operation_log;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 操作记录(数据传输对象),实现参数校验。
*/
@Data
public class OperationLogDto {
@NotBlank(message = "用户账号不能为空")
private String account;
// 其他字段...
}
4.5. 效果验证:空字段请求触发校验异常(附完整日志)

2026-05-06T22:41:10.681+08:00 INFO 10156 --- [hello-service-validation] [nio-8080-exec-2] c.e.h.user.UserController : userAddRequest=UserAddRequest(account=, username=, phone=)
2026-05-06T22:41:10.682+08:00 INFO 10156 --- [hello-service-validation] [nio-8080-exec-2] c.e.h.user.UserService : 执行业务逻辑
2026-05-06T22:41:10.696+08:00 ERROR 10156 --- [hello-service-validation] [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: jakarta.validation.ConstraintViolationException: insert.dto.account: 用户账号不能为空] with root cause
jakarta.validation.ConstraintViolationException: insert.dto.account: 用户账号不能为空
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:168) ~[spring-context-7.0.7.jar:7.0.7]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-7.0.7.jar:7.0.7]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:719) ~[spring-aop-7.0.7.jar:7.0.7]
at com.example.hello_service_validation.operation_log.OperationLogService$$SpringCGLIB$$0.insert(<generated>) ~[classes/:na]
at com.example.hello_service_validation.user.UserService.addUser(UserService.java:25) ~[classes/:na]
at com.example.hello_service_validation.user.UserController.addUser(UserController.java:22) ~[classes/:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
5. 原理篇:为什么 Service 层必须两个注解同时用?
5.1. 问题
在Service层做的参数校验,为什么一定要加 @Validated 和 @Valid 才能实现校验,请问为什么?
5.2. AI分析
在Spring Boot项目中,Service层参数校验必须同时使用 @Validated(类上)和 @Valid(方法参数上),这是由Spring Validation的AOP代理机制和Bean Validation规范共同决定的。以下是核心原因拆解:
5.2.1. @Validated:开启Spring的方法级校验AOP代理
Spring的参数校验(尤其是Service层)是基于AOP(面向切面编程)实现的,而 @Validated 是Spring提供的“开关”注解,作用是:
- 标记目标类:告诉Spring容器“这个类需要进行方法级别的参数校验”。
- 触发AOP代理创建:Spring会为带有
@Validated的类生成一个代理对象(Proxy),当外部调用该类的方法时,代理对象会先拦截方法调用,执行校验逻辑,再放行到真实方法。
如果不加 @Validated:
Spring不会为该Service类创建校验AOP代理,方法调用直接走真实对象,即使参数上有 @Valid,校验逻辑也不会被触发。
5.2.2. @Valid:标记需要校验的具体参数
@Valid 是Java标准Bean Validation规范(JSR-380) 中的注解,作用是:
- 标记校验目标:告诉Spring“这个方法参数需要进行递归校验”(即校验参数对象内部的字段,如
OperationLogRecord里的@NotNull、@NotBlank等注解)。 - 触发校验执行:Spring的AOP拦截器在拦截到方法调用时,会检查参数上是否有
@Valid(或Spring的@Validated),如果有,则对该参数执行校验。
如果不加 @Valid:
即使类上有 @Validated,Spring也不知道“哪个参数需要校验”,只会直接放行方法调用,不会校验参数内部的字段。
5.2.3. 为什么Controller层有时只加 @Valid 也能生效?
你可能发现,在Controller层,有时只在参数上加 @Valid(不加类上的 @Validated)也能校验。这是因为:
- Spring MVC在处理HTTP请求时,默认会对Controller方法参数进行绑定和校验(这是Web层的特殊机制)。
- 但Service层是“纯业务层”,没有Web层的默认绑定逻辑,必须显式通过
@Validated+@Valid开启AOP校验。
5.2.4. 核心总结:两者分工不同
| 注解 | 位置 | 作用 |
|---|---|---|
@Validated |
类上 | 开启Spring的方法级校验AOP代理,让校验逻辑能够拦截方法调用。 |
@Valid |
参数上 | 标记具体需要校验的参数对象,触发对参数内部字段的Bean Validation校验。 |
只有两者结合,Spring才能:
- 通过
@Validated生成代理对象拦截方法调用; - 通过
@Valid知道要校验哪个参数,并执行具体的字段规则校验。
5.2.5. 补充:MethodValidationPostProcessor
Spring Boot中,MethodValidationPostProcessor 是负责处理 @Validated 的核心后置处理器。它会扫描所有带 @Validated 的Bean,为其创建AOP代理,并在方法调用时通过 MethodValidationInterceptor 执行校验逻辑。如果没有 @Validated,这个处理器就不会介入。
6. 附录:完整可运行项目源码

6.1. POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.6</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>hello-service-validation</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>default-testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
6.2. application.yml
spring:
application:
name: hello-service-validation
6.3. Application
package com.example.hello_service_validation;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HelloServiceValidationApplication {
public static void main(String[] args) {
SpringApplication.run(HelloServiceValidationApplication.class, args);
}
}
6.4. UserController
package com.example.hello_service_validation.user;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class UserController {
@Autowired
private UserService userService;
/**
* 新增用户
*/
@PostMapping("/users")
public String addUser(@RequestBody UserAddRequest userAddRequest) {
log.info("userAddRequest={}", userAddRequest);
userService.addUser(userAddRequest);
return "成功";
}
}
6.5. UserService
package com.example.hello_service_validation.user;
import com.example.hello_service_validation.operation_log.OperationLogDto;
import com.example.hello_service_validation.operation_log.OperationLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;
@Slf4j
@Service
public class UserService {
@Autowired
private OperationLogService operationLogService;
public void addUser(@RequestBody UserAddRequest userAddRequest) {
// 业务逻辑
log.info("执行业务逻辑");
// 记操作记录
OperationLogDto operationLogDto = new OperationLogDto();
operationLogDto.setAccount(userAddRequest.getAccount());
// 其他字段...
operationLogService.insert(operationLogDto);
}
}
6.6. UserAddRequest
package com.example.hello_service_validation.user;
import lombok.Data;
/**
* 新增用户
*/
@Data
public class UserAddRequest {
/**
* 用户账号。
* 示例:123456
*/
private String account;
/**
* 用户名称。
* 示例:张三
*/
private String username;
/**
* 手机号码
*/
private String phone;
}
6.7. OperationLogService
package com.example.hello_service_validation.operation_log;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Slf4j
@Service
@Validated
public class OperationLogService {
public void insert(@Valid OperationLogDto dto) {
OperationLog operationLog = new OperationLog();
operationLog.setAccount(dto.getAccount());
// 其他字段...
log.info("操作记录写入数据库:OperationLog={}", operationLog);
}
}
6.8. OperationLogDto
package com.example.hello_service_validation.operation_log;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 操作记录(数据传输对象),实现参数校验。
*/
@Data
public class OperationLogDto {
@NotBlank(message = "用户账号不能为空")
private String account;
// 其他字段...
}
6.9. OperationLog
package com.example.hello_service_validation.operation_log;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 操作记录(数据库表)。
* 谁,在什么时间,对什么对象,做了什么操作
*/
@Data
public class OperationLog {
/**
* ID
* 雪花算法ID,由数据库框架自动生成
*/
private String id;
/**
* 操作人ID
*/
private String operatorId;
/**
* 操作人名称
*/
private String operatorName;
/**
* 操作时间。
* 数据库新增记录是赋值为记录创建时间
*/
private LocalDateTime operateTime;
/**
* 用户账号。
* 示例:123456
*/
private String account;
/**
* 操作类型
*/
private String operateType;
/**
* 操作内容
*/
private String operateContent;
}
更多推荐




所有评论(0)