SpringBoot 手动验证

SpringBoot 后端手动参数校验:超越 Controller,全场景优雅验证实践

SpringBoot 后端手动参数校验:超越Controller,全场景优雅验证实践

在 SpringBoot 开发中,我们习惯用 @Valid/@Validated + 注解式校验(如 @NotNull@NotBlank)完成 Controller 层入参验证,但实际业务场景中,非 Controller 层的验证需求无处不在

  • 读取本地配置文件、Excel 文件后,需要校验文件数据合法性;
  • 从数据库查询数据后,需要校验实体字段是否满足业务规则;
  • 微服务间调用、消息队列消费数据后,需要校验传输对象完整性;
  • 工具类、Service 层内部逻辑处理时,需要校验入参有效性。

注解式校验依赖 Spring MVC 自动触发,无法在非 Web 层自动生效。此时,手动调用 Validation API 完成校验 就成为最通用、最可靠的解决方案。本文将带你掌握 SpringBoot 手动校验的核心用法、高阶技巧与工程化实践,实现全场景统一的参数验证。

一、核心基础:Bean Validation 手动校验API

Java 定义了 Bean Validation (JSR 380) 规范,Hibernate Validator 是其官方参考实现,SpringBoot 已默认集成,无需额外引入依赖。

手动校验的核心是两个核心类:

  1. ValidatorFactory:校验器工厂,用于获取 Validator 实例;
  2. Validator:核心校验器,提供对象校验、字段校验、分组校验等能力。

1. 基础依赖(SpringBoot 自带,无需重复引入)

如果项目中移除了默认依赖,手动引入即可:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Bean Validation 核心依赖 -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>
<!-- Hibernate Validator 实现 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1</version>
</dependency>

2. 定义待校验实体类

先创建一个普通业务实体,标注标准校验注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;

/**
* 业务配置实体(可用于文件读取、数据库查询、消息消费)
*/
@Data
public class BusinessConfig {

@NotBlank(message = "配置编码不能为空")
private String configCode;

@NotNull(message = "配置值不能为null")
@Size(min = 2, max = 100, message = "配置值长度必须在2-100之间")
private String configValue;

@NotBlank(message = "配置所属模块不能为空")
private String module;
}

二、核心用法:手动校验的3种基础场景

场景1:非Controller层完整对象校验(最常用)

适用于 Service 层、工具类、文件解析、数据库数据校验 等场景,直接校验整个对象的所有字段。

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.ConstraintViolation;
import org.springframework.stereotype.Component;
import java.util.Set;

/**
* 手动校验工具类(通用)
*/
@Component
public class ValidationUtil {

// 单例校验器:避免重复创建工厂,提升性能
private static final Validator VALIDATOR;

static {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
VALIDATOR = factory.getValidator();
}

/**
* 校验对象,返回校验结果
*/
public static <T> Set<ConstraintViolation<T>> validate(T obj) {
return VALIDATOR.validate(obj);
}

/**
* 校验对象,有错误直接抛出业务异常
*/
public static <T> void validateAndThrow(T obj) {
Set<ConstraintViolation<T>> violations = validate(obj);
if (!violations.isEmpty()) {
// 获取第一个错误信息
String errorMsg = violations.iterator().next().getMessage();
throw new RuntimeException("参数校验失败:" + errorMsg);
}
}
}

实战使用(Service 层校验数据库数据):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.stereotype.Service;

@Service
public class BusinessConfigService {

/**
* 从数据库读取配置后校验
*/
public void checkDbConfig() {
// 模拟从数据库查询数据
BusinessConfig config = new BusinessConfig();
config.setConfigCode(""); // 非法值
config.setConfigValue("1"); // 非法值
config.setModule("test");

// 手动校验,直接抛出异常
ValidationUtil.validateAndThrow(config);
}
}

执行后直接抛出异常:RuntimeException: 参数校验失败:配置编码不能为空,完美适配非 Web 层校验需求。

场景2:单个字段手动校验

适用于只需要校验对象的某个字段,无需校验全对象,提升效率:

1
2
3
4
5
6
7
8
9
10
11
// 校验指定字段
public static <T> void validateField(T obj, String fieldName) {
Set<ConstraintViolation<T>> violations = VALIDATOR.validateProperty(obj, fieldName);
if (!violations.isEmpty()) {
String errorMsg = violations.iterator().next().getMessage();
throw new RuntimeException("字段校验失败:" + errorMsg);
}
}

// 使用:仅校验 configValue 字段
ValidationUtil.validateField(config, "configValue");

场景3:分组校验(多场景差异化验证)

适用于同一个对象,不同场景校验规则不同(例如:新增时必填字段,修改时非必填)。

步骤1:定义分组标识

1
2
3
4
// 新增分组
public interface AddGroup {}
// 修改分组
public interface UpdateGroup {}

步骤2:实体类绑定分组

1
2
3
4
5
6
7
8
@Data
public class BusinessConfig {
@NotBlank(message = "配置编码不能为空", groups = {AddGroup.class})
private String configCode;

@NotNull(message = "配置值不能为null", groups = {AddGroup.class, UpdateGroup.class})
private String configValue;
}

步骤3:手动分组校验

1
2
3
4
5
6
7
8
9
10
public static <T> void validateByGroup(T obj, Class<?>... groups) {
Set<ConstraintViolation<T>> violations = VALIDATOR.validate(obj, groups);
if (!violations.isEmpty()) {
String errorMsg = violations.iterator().next().getMessage();
throw new RuntimeException("分组校验失败:" + errorMsg);
}
}

// 使用:新增场景校验
ValidationUtil.validateByGroup(config, AddGroup.class);

三、高阶技巧:工程化最佳实践

1. 全局单例校验器(性能优化)

ValidatorFactory 是线程安全的,项目中只需要创建一次,不要每次校验都新建工厂,避免资源浪费。
推荐在项目启动时初始化,上文的 static 代码块就是最优实践。

2. 统一自定义异常

非 Controller 层抛出的异常,需要全局捕获,统一返回格式:

1
2
3
4
5
6
7
8
9
10
11
/**
* 自定义校验异常
*/
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
}

// 工具类中替换异常
throw new ValidationException(errorMsg);

3. 通用返回结果封装

配合全局异常处理器,让校验结果和接口返回格式统一:

1
2
3
4
5
6
7
8
9
10
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ValidationException.class)
public R<String> handleValidationException(ValidationException e) {
return R.fail(400, e.getMessage());
}
}

4. 无实体类校验(原生值校验)

对于简单类型参数(无实体类),直接手动判断即可,结合工具类简化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 原生参数校验工具方法
*/
public static void notBlank(String str, String msg) {
if (str == null || str.trim().isEmpty()) {
throw new ValidationException(msg);
}
}

public static void notNull(Object obj, String msg) {
if (obj == null) {
throw new ValidationException(msg);
}
}

// 使用
ValidationUtil.notBlank(fileContent, "文件内容不能为空");

四、全场景适用:非Controller层实战案例

案例1:Excel/本地文件解析校验

1
2
3
4
5
6
7
8
9
10
11
@Service
public class FileParseService {
public void parseExcel(String filePath) {
// 1. 读取文件
List<BusinessConfig> configList = ExcelUtil.read(filePath, BusinessConfig.class);
// 2. 逐条手动校验
for (BusinessConfig config : configList) {
ValidationUtil.validateAndThrow(config);
}
}
}

案例2:消息队列消费数据校验

1
2
3
4
5
6
7
8
9
10
11
12
@Service
@RocketMQMessageListener(topic = "business_topic", consumerGroup = "business_group")
public class BusinessConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
// 1. 解析消息
BusinessConfig config = JSON.parseObject(message, BusinessConfig.class);
// 2. 手动校验(核心:非Controller层强制校验)
ValidationUtil.validateAndThrow(config);
// 3. 执行业务逻辑
}
}

案例3:工具类通用校验

1
2
3
4
5
6
7
8
9
10
11
12
public class DbDataUtil {
/**
* 校验数据库查询的用户数据合法性
*/
public static void checkUserData(User user) {
// 手动校验 + 自定义规则
ValidationUtil.validateAndThrow(user);
if (user.getAge() < 18) {
throw new ValidationException("用户年龄必须大于18岁");
}
}
}

五、总结:手动校验的核心价值

  1. 全场景通用:不依赖 Spring MVC,Controller/Service/工具类/文件解析/消息消费都能用;
  2. 灵活可控:支持全对象、单字段、分组校验,满足差异化校验需求;
  3. 性能优异:单例校验器,无额外性能损耗;
  4. 代码统一:统一校验逻辑、统一异常处理,提升项目可维护性。