1. 摘要

本文针对Spring Boot项目中 必填字段漏赋值导致的脏数据问题,提出了架构级的解决方案:在Service层通过Bean Validation实现运行期强约束校验。

文章基于Spring Boot 4.0.6 + JDK 21技术栈,从项目搭建、依赖引入、代码实现到测试验证,完整演示了Service层参数校验的落地流程,并深入拆解了@Validated@Valid的底层工作原理,厘清了Spring AOP代理机制与方法级校验的关系,同时提供了可直接复刻运行的完整项目代码。

2. 引言

你是否遇到过这样的问题:业务代码逻辑完全正确,测试也通过了,上线后却发现数据库里出现了大量accountnull的操作记录?排查半天,最终发现只是因为某一行代码漏写了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才能:

  1. 通过 @Validated 生成代理对象拦截方法调用;
  2. 通过 @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;

}

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐