Spring Boot 条件验证

Spring Boot 条件验证:使用 @AssertTrue 实现灵活的业务规则校验

前言

在 Web 开发中,表单验证是一个常见且重要的环节。Spring Boot 提供了强大的 Bean Validation 支持,但常规的注解如 @NotBlank@Pattern 等只能进行单字段验证。当遇到字段间存在依赖关系验证规则依赖于其他字段值的场景时,这些基础注解就显得力不从心了。

本文将介绍如何使用 @AssertTrue 注解实现灵活的条件验证,让你的验证逻辑更加优雅和可维护。

一、传统验证方式的痛点

场景描述

假设我们有一个广告申请表单,包含以下业务规则:

  • 当办理对象为企业时,必须填写企业名称、统一社会信用代码、法定代表人等信息
  • 当办理对象为个人时,必须填写个人姓名、证件类型、证件号等信息
  • 开始时间必须小于结束时间

常见解决方案的缺陷

方案1:在 Controller 层手动判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PostMapping("/save")
public Result save(@RequestBody AdvertHistoryPostParam param) {
if ("1".equals(param.getObjectType())) {
// 企业验证逻辑
if (StringUtils.isEmpty(param.getAdvertUnitName())) {
return Result.error("请输入设置单位");
}
// ... 更多验证
} else if ("2".equals(param.getObjectType())) {
// 个人验证逻辑
if (StringUtils.isEmpty(param.getAdvertPersonName())) {
return Result.error("请输入个人姓名");
}
// ... 更多验证
}
// 业务逻辑
}

问题:

  • 验证逻辑与业务代码耦合,违背单一职责原则
  • 代码重复,难以维护
  • 违反”贫血模型”原则,业务规则散落在各处

方案2:使用分组验证

1
2
3
4
@Validated({Default.class, CompanyGroup.class}) 
public Result save(@RequestBody AdvertHistoryPostParam param) {
// 但分组无法动态切换
}

问题:

  • 分组在编译期确定,无法根据请求参数动态选择
  • 需要创建多个分组接口,增加代码复杂度

二、@AssertTrue 注解的魅力

@AssertTrue 是 Bean Validation 规范中的注解,用于标记一个返回 boolean 值的方法。当方法返回 false 时,会触发验证失败。

核心原理

1
2
3
4
5
@AssertTrue(message = "验证失败时的提示信息")
public boolean isValid() {
// 自定义验证逻辑
return true; // true 表示验证通过,false 表示验证失败
}

关键特性:

  1. 可以访问类的所有字段
  2. 支持复杂的条件判断
  3. 在 Spring 的 @Validated 触发时自动执行

三、实战案例:广告申请表单验证

1. 定义实体类

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
@Data
public class AdvertPostParam {

// 办理对象:1-企业,2-个人
@NotBlank(message = "办理对象不能为空", groups = {Insert.class, Update.class})
private String objectType;

// 企业相关字段
private String advertUnitName;

// 个人相关字段
private String advertPersonName;

// ==================== 条件验证方法 ====================

@AssertTrue(message = "设置单位不能为空", groups = {Insert.class})
public boolean isAdvertUnitNameValid() {
return !"0".equals(objectType) || StringUtils.hasText(advertUnitName);
}


@AssertTrue(message = "请输入个人姓名", groups = {Insert.class})
public boolean isAdvertPersonNameValid() {
return !"1".equals(objectType) || StringUtils.hasText(advertPersonName);
}

}

2. Controller 层使用

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/advert")
public class AdvertController {

@PostMapping("/save")
public Result save(@Validated(Insert.class) @RequestBody AdvertHistoryPostParam param) {
// 业务逻辑
// 所有验证都会自动执行,验证失败会抛出 MethodArgumentNotValidException
return Result.success();
}
}

3. 全局异常处理

1
2
3
4
5
6
7
8
9
10
11
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(";"));
return Result.error(message);
}
}

四、高级应用场景

1. 多字段联合验证

1
2
3
4
5
6
7
8
9
10
11
12
@Data
public class OrderForm {
private String paymentMethod; // 支付方式:1-微信,2-支付宝,3-银行卡
private String bankCardNumber;
private String bankName;

@AssertTrue(message = "选择银行卡支付时,必须填写卡号和开户行")
public boolean isBankCardValid() {
if (!"3".equals(paymentMethod)) return true;
return StringUtils.hasText(bankCardNumber) && StringUtils.hasText(bankName);
}
}

2. 嵌套对象条件验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
public class ApplicationForm {
private String applicationType; // 1-个人申请,2-企业申请
private PersonInfo personInfo;
private CompanyInfo companyInfo;

@AssertTrue(message = "个人申请时必须填写个人信息")
public boolean isPersonInfoValid() {
if (!"1".equals(applicationType)) return true;
return personInfo != null && personInfo.isValid();
}

@AssertTrue(message = "企业申请时必须填写企业信息")
public boolean isCompanyInfoValid() {
if (!"2".equals(applicationType)) return true;
return companyInfo != null && companyInfo.isValid();
}
}

3. 集合元素验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
public class BatchOperationForm {
private String operationType; // 1-批量导入,2-单条添加
private List<Item> items;
private Item singleItem;

@AssertTrue(message = "批量导入时至少需要一条数据")
public boolean isItemsValid() {
if (!"1".equals(operationType)) return true;
return items != null && !items.isEmpty();
}

@AssertTrue(message = "单条添加时必须填写详情")
public boolean isSingleItemValid() {
if (!"2".equals(operationType)) return true;
return singleItem != null;
}
}

五、最佳实践与注意事项

1. 命名规范

1
2
3
4
5
6
7
// ✅ 推荐:清晰的命名
@AssertTrue(message = "企业名称不能为空")
public boolean isCompanyNameValid() { ... }

// ❌ 不推荐:模糊的命名
@AssertTrue(message = "验证失败")
public boolean check1() { ... }

2. 职责分离

1
2
3
4
5
6
7
8
9
10
11
12
// ✅ 推荐:每个验证方法只负责一个业务规则
@AssertTrue(message = "手机号格式不正确")
public boolean isPhoneValid() { ... }

@AssertTrue(message = "身份证号格式不正确")
public boolean isIdCardValid() { ... }

// ❌ 不推荐:一个方法包含多个验证规则
@AssertTrue(message = "信息不完整")
public boolean isAllInfoValid() {
return isValidPhone() && isValidIdCard() && isValidAddress();
}

3. 性能考虑

1
2
3
4
5
6
7
// ✅ 推荐:提前返回,避免不必要的验证
@AssertTrue(message = "身份证号格式不正确")
public boolean isIdCardValid() {
if (!isPerson()) return true; // 提前返回
if (!StringUtils.hasText(advertContactIdNumber)) return false;
return advertContactIdNumber.matches(ID_CARD_PATTERN);
}

4. 配合其他注解使用

1
2
3
4
5
6
7
8
9
10
11
public class UserForm {
// @NotBlank 处理非空验证
@NotBlank(message = "用户名不能为空")
private String username;

// @AssertTrue 处理条件验证
@AssertTrue(message = "启用账号时必须设置密码")
public boolean isPasswordValid() {
return !"1".equals(status) || StringUtils.hasText(password);
}
}

六、优缺点分析

优点

  1. 验证逻辑内聚:业务规则与实体类绑定,符合面向对象设计
  2. Controller 简洁:只需一个 @Validated 注解
  3. 灵活性强:支持任意复杂的验证逻辑
  4. 可复用:验证方法可以在多个地方复用
  5. 易于测试:验证方法可以单独进行单元测试

缺点

  1. 实体类臃肿:多个验证方法会增加类的代码量
  2. 难以定位:验证失败时,需要查看多个方法才能定位问题
  3. 性能开销:所有 @AssertTrue 方法都会被执行
  4. 不支持动态禁用:无法在运行时动态禁用某些验证

七、与其他方案的对比

方案 优点 缺点 适用场景
Controller 手动验证 灵活、直观 代码重复、耦合度高 简单的验证逻辑
分组验证 类型安全 无法动态切换、代码复杂 静态的业务场景
自定义注解 复用性强 实现复杂 通用的验证规则
@AssertTrue 简单、灵活、内聚 实体类臃肿 条件依赖验证

八、总结

@AssertTrue 注解为 Spring Boot 表单验证提供了强大的条件验证能力。它巧妙地平衡了灵活性简洁性,让开发者能够将复杂的业务规则优雅地封装在实体类内部,保持 Controller 层的整洁。

在实际项目中,建议根据业务复杂度选择合适的验证方案:

  • 简单的单字段验证:使用 @NotBlank@Pattern 等基础注解
  • 通用的验证规则:创建自定义注解
  • 复杂的条件依赖验证:使用 @AssertTrue

合理运用 @AssertTrue,可以让你的代码更加优雅、可维护,真正实现”业务规则即代码”的理念。