

你是否遇到过这样的场景:评审代码时,同事对着接口里那些“又长又乱”的参数校验逻辑直摇头?为了校验一个字段,不仅要在服务层写满if-else,还得在各种自定义校验器中跳来跳去。一旦涉及跨字段、条件判断或者需要查询数据库的复杂规则,代码就会变得格外臃肿和难以维护。
面对这个普遍痛点,其实有一种更为优雅的解决方案——基于 Spring Expression Language (SpEL) 的声明式校验组件。今天介绍的 SpEL Validator 正是这样一款工具。它并非要取代我们熟悉的 Jakarta Validation(即 @NotNull, @Size 等标准注解),而是作为一个强有力的补充,让你能用注解的形式,将复杂的业务校验规则直接写在实体类或参数上,让规则和数据真正结合在一起。
项目信息
1. 它解决了什么痛点?
SpEL Validator 主要针对那些用标准校验注解难以简洁表达的场景:
-
多字段联合、条件式校验
例如,根据 contentType 的值决定是否校验 audioContent 或 videoContent 字段。
@NotNull
private int contentType;
@SpelNotNull(condition = “#this.contentType == 1“, message = “语音内容不能为空“)
private Object audioContent;
@SpelNotNull(condition = “#this.contentType == 2“, message = “视频内容不能为空“)
private Object videoContent;
-
枚举合法性、复杂断言
例如,调用静态工具方法校验枚举值是否存在,或进行更复杂的逻辑判断。
@SpelAssert(assertTrue = “T(cn.sticki.enums.UserStatusEnum).getByCode(#this.userStatus) != null“,
message = “用户状态不合法“)
private Integer userStatus;
-
需要调用 Spring Bean 的业务校验
例如,校验用户ID是否在数据库中存在(需启用 @EnableSpelValidatorBeanRegistrar)。
@SpelAssert(assertTrue = “@userService.getById(#this.userId) != null“, message = “用户不存在“)
private Long userId;
一句话概括:在注解里直接写表达式,优雅地定义“何时校验”以及“校验什么”,将过去需要写成独立校验器或样板代码的逻辑,内聚在领域模型的注解中,让校验规则与数据结构紧密贴合。对于需要实现复杂业务规则的项目,这是一种提升代码可读性和维护性的有效实践,你可以在 Java 技术板块找到更多关于现代 Java 开发范式的讨论。
2. 用法非常轻量
引入依赖:根据你的 Spring Boot 版本二选一。
- 适用于 Spring Boot 2.x (javax)
<dependency>
<groupId>cn.sticki</groupId>
<artifactId>spel-validator-javax</artifactId>
<version>Latest Version</version>
</dependency>
- 适用于 Spring Boot 3.x (jakarta)
<dependency>
<groupId>cn.sticki</groupId>
<artifactId>spel-validator-jakarta</artifactId>
<version>Latest Version</version>
</dependency>
两步开启校验:
- 在方法参数上使用
@Valid 或 @Validated。
- 在需要校验的类或参数上使用
@SpelValid。
示例代码:
@RestController
@RequestMapping(“/example“)
public class ExampleController {
@PostMapping(“/simple“)
public Resp<Void> simple(@RequestBody @Valid SimpleParam p) {
return Resp.ok(null);
}
}
@Data
@SpelValid
public class SimpleParam {
@NotNull
private Boolean switchAudio;
@SpelNotNull(condition = “#this.switchAudio == true“, message = “语音内容不能为空“)
private Object audioContent;
}
校验失败时,异常仍然会走 Spring/Jakarta 的标准通道(抛出 BindException 或 MethodArgumentNotValidException),因此你原有的全局异常处理和统一返回体包装逻辑完全无需改动。
3. 注解能力一览
启动注解
@SpelValid:激活 SpEL 约束系统,可标记在类、字段、方法参数或构造参数上。
通用约束注解(均支持 condition, message, group 属性)
@SpelAssert (功能类似 @AssertTrue)
@SpelNotNull / @SpelNull / @SpelNotEmpty / @SpelNotBlank
@SpelSize (适用于字符串、集合、Map、数组)
@SpelMin / @SpelMax (支持 Number 与 CharSequence,可通过 inclusive 控制是否包含边界)
@SpelDigits (控制整数和小数位数,支持 Number 与 CharSequence)
@SpelPast / @SpelPastOrPresent / @SpelFuture / @SpelFutureOrPresent (支持 JDK8 time API、Date、Calendar 及各 Chrono* 类型)
所有注解都内置了国际化消息键,你可以根据需要覆盖默认消息。
4. 表达式写法与上下文
- SpEL Validator 基于强大的 Spring 表达式语言 (SpEL),支持算术、关系、逻辑、三元、成员访问、集合操作、空安全导航 (
?.)、空合并 (?:) 等丰富操作。
- 表达式的根对象 (
#this) 就是当前被验证的对象本身,你可以直接用 #this.fieldName 来引用同一对象内的其他字段。
- 可以调用静态方法:
T(全限定类名).method(...)
- 可以调用 Spring 容器中的 Bean:
@beanName.method(...) (需要额外启用 @EnableSpelValidatorBeanRegistrar)
这使得业务校验逻辑能够自然地复用领域知识,无论是调用枚举工具类、进行数字运算,还是查询领域服务,都可以在注解表达式内完成。
5. 分组与条件:两层可组合的校验逻辑
- 条件开关 (
condition):所有约束注解都可以携带 condition 属性,它是一个 SpEL 表达式,只有当表达式计算结果为 true 时,该条校验规则才会生效。
- 动态分组 (
group + spelGroups):
- 在
@SpelValid(spelGroups = “#this.type“) 中,spelGroups 定义了动态分组的表达式。
- 在每个约束注解的
group 属性中,可以指定一个字符串数组。
- 当
spelGroups 表达式计算出的值,与某个约束注解的 group 数组中的任一值匹配(equals)时,该约束生效。如果 group 为空数组,则该约束默认生效。
- 这套分组机制与 Jakarta Validation 原生的
groups 属性并行不悖,前者服务于 SpEL 驱动的动态分组需求,后者依然可用于标准注解的静态分组。
6. 国际化与消息插值
- 多语言消息资源文件已内置在
spel-validator-constrain 模块的 resources 目录下,默认支持中文、英文、日文、韩文等。
- 你可以通过
ResourceBundleMessageResolver.addBasenames(“YourBundle“) 方法,将自己的国际化资源包加入优先级队列,以覆盖默认的消息键。
- 在注解的
message 属性中,可以使用 {your.message.key} 来引用资源文件中的键。如果消息文本本身需要包含 {、} 或 \ 字符,请分别转义为 \\{、\\} 和 \\。
7. 性能与工程可用性
根据项目 FAQ 中的压测数据(基于示例项目、JDK 8 / Spring Boot 2.7 环境):
- 预热后,单次校验耗时通常在 0~1 毫秒。
- 在 1500 ~ 9000 QPS 的压力下,平均每次校验耗时在 0.11 ~ 0.13 毫秒。
- 其中,SpEL 表达式的解析占据了大部分时间(33%~65%),但整体性能在可接受范围内,且仍有优化空间。
在工程性方面,它做了不少贴心设计:
- 使用了字段与注解层的缓存(基于
ConcurrentHashMap),有效减少了反射和注解扫描的开销。
- 错误完全融入 Jakarta Validation 体系,不影响现有的异常处理流程。
- 除了在 Web 层自动触发,你也可以在服务内部直接调用
validateObject 方法进行校验,便于单元测试或离线任务的复用。
8. 和 Jakarta/Bean Validation 的关系
- 不是替代,而是扩展:你完全可以继续使用所有熟悉的
@NotNull、@Size 等标准注解。SpEL Validator 是在你遇到“跨字段、条件式、复杂逻辑、需要调用 Bean 或静态方法”这些标准注解的短板时,用来填补空白的有力工具。
- 触发机制严格遵循规范:只有当
@Valid/@Validated 和 @SpelValid 同时存在时,SpEL 校验才会被触发。
- 分组模型心智一致:Jakarta 原生的
groups 属性依然有效;SpEL Validator 提供的 spelGroups/group 机制,则更侧重于满足表达式驱动的、动态的分组需求。
9. 可扩展性:自定义约束
自定义约束的步骤与 Jakarta Validation 几乎一致:
- 定义注解,必须包含
message、condition、group 这三个属性。
- 实现
SpelConstraintValidator<YourAnnotation> 接口,在 isValid 方法中编写校验逻辑;可以通过覆写 supportType 方法来限制该注解支持的数据类型。
- 在你的自定义注解上使用
@SpelConstraint(validatedBy = YourValidator.class) 关联校验器。
得益于 SpelValidExecutor 的统一调度,你的自定义注解将天然具备 condition 条件判断、动态分组和国际化消息支持的能力。探索和定制这类 开源实战 项目,是提升技术深度的好途径。
10. 适用边界与注意事项
- 适用场景:优先使用标准注解解决简单校验。SpEL 注解最适合用于“条件式、跨字段、复杂业务逻辑”的增量增强。
- 表达式无副作用:编写 SpEL 表达式时,应避免修改被校验对象的状态。
Validator 接口的实现强调线程安全与不可变性。
- 调用 Spring Bean:如果需要在校验表达式中调用 Spring Bean,别忘了在配置类上添加
@EnableSpelValidatorBeanRegistrar 注解。
结语
SpEL Validator 在不破坏现有校验体系的前提下,让“条件式、跨字段、复杂逻辑校验”回归到直观、声明式的写法,让规则靠近数据,让表达式描述业务。它与 Jakarta Validation 兼容并济:你可以继续信赖 @NotNull 和 @Size,同时把那些“过去怎么写都别扭”的校验场景,交给 @SpelNotNull、@SpelAssert、@SpelSize 这套基于 SpEL 的注解来解决。
如果你的项目存在以下任一情况,那么 SpEL Validator 值得一试:
- 参数校验规则依赖于其他字段的值或动态分组。
- 需要调用静态方法或领域服务来完成复杂的业务判断。
- 希望在保持原有异常处理和返回体格式的前提下,增强校验能力。
- 追求更高可读性、更贴近业务语义的“校验即声明”编码风格。
项目文档涵盖了快速入门、注解索引、SpEL 使用要点、国际化配置、FAQ 及更新日志,源码结构清晰,测试完备,接入成本很低。现在就尝试一下,把那些散落在各处的“如果…就校验…”逻辑,优雅地收拢回你的领域模型吧。更多此类提升开发效率的 后端 & 架构 知识,也欢迎在技术社区交流探讨。
本文介绍的工具旨在提供一种解决复杂校验的思路。在实际项目中,请根据团队规范和具体场景选择最合适的方案。技术分享与讨论,欢迎来到 云栈社区。

