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; }
|
关键特性:
- 可以访问类的所有字段
- 支持复杂的条件判断
- 在 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 { @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) { 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; 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; 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; 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(message = "用户名不能为空") private String username; @AssertTrue(message = "启用账号时必须设置密码") public boolean isPasswordValid() { return !"1".equals(status) || StringUtils.hasText(password); } }
|
六、优缺点分析
优点
- 验证逻辑内聚:业务规则与实体类绑定,符合面向对象设计
- Controller 简洁:只需一个
@Validated 注解
- 灵活性强:支持任意复杂的验证逻辑
- 可复用:验证方法可以在多个地方复用
- 易于测试:验证方法可以单独进行单元测试
缺点
- 实体类臃肿:多个验证方法会增加类的代码量
- 难以定位:验证失败时,需要查看多个方法才能定位问题
- 性能开销:所有
@AssertTrue 方法都会被执行
- 不支持动态禁用:无法在运行时动态禁用某些验证
七、与其他方案的对比
| 方案 |
优点 |
缺点 |
适用场景 |
| Controller 手动验证 |
灵活、直观 |
代码重复、耦合度高 |
简单的验证逻辑 |
| 分组验证 |
类型安全 |
无法动态切换、代码复杂 |
静态的业务场景 |
| 自定义注解 |
复用性强 |
实现复杂 |
通用的验证规则 |
| @AssertTrue |
简单、灵活、内聚 |
实体类臃肿 |
条件依赖验证 |
八、总结
@AssertTrue 注解为 Spring Boot 表单验证提供了强大的条件验证能力。它巧妙地平衡了灵活性和简洁性,让开发者能够将复杂的业务规则优雅地封装在实体类内部,保持 Controller 层的整洁。
在实际项目中,建议根据业务复杂度选择合适的验证方案:
- 简单的单字段验证:使用
@NotBlank、@Pattern 等基础注解
- 通用的验证规则:创建自定义注解
- 复杂的条件依赖验证:使用
@AssertTrue
合理运用 @AssertTrue,可以让你的代码更加优雅、可维护,真正实现”业务规则即代码”的理念。