同事嫌参数校验太丑,我直接掏出了更优雅的 SpEL Validator

某次代码评审,同事吐槽代码里的接口参数校验"又长又乱":要么到处散落自定义校验器,要么硬塞 if/else 在服务层,跨字段、条件式、还要调用枚举/服务判断的场景尤其难看。

我当场就给大家介绍了一套开源的参数校验组件:SpEL Validator------基于 Spring 表达式,把"何时校验、校验什么"直接写进注解里。既不替代 Jakarta Validation,又能在复杂规则下写得自然、紧贴领域模型,规则和数据终于放到了一起。

开源地址:github.com/stick-i/spe...

在线文档:spel-validator.sticki.cn/

1. 它解决了什么痛点

  • 多字段联合、条件式校验
    例:contentType 决定校验 audioContent 或 videoContent
java 复制代码
@NotNull
private int contentType;

@SpelNotNull(condition = "#this.contentType == 1", message = "语音内容不能为空")
private Object audioContent;

@SpelNotNull(condition = "#this.contentType == 2", message = "视频内容不能为空")
private Object videoContent;
  • 枚举合法性、复杂断言
    例:调用静态方法校验枚举存在性/数据长度等
java 复制代码
@SpelAssert(assertTrue = "T(cn.sticki.enums.UserStatusEnum).getByCode(#this.userStatus) != null",
    message = "用户状态不合法")
private Integer userStatus;
  • 需要透传 Spring Bean 的业务校验
    例:依赖数据库/缓存查询(需启用 @EnableSpelValidatorBeanRegistrar
java 复制代码
@SpelAssert(assertTrue = "@userService.getById(#this.userId) != null", message = "用户不存在")
private Long userId;

一句话概括:在注解里直接写表达式,优雅表达"何时校验、校验什么",把过去要写成独立校验器/样板代码的逻辑,嵌入在域模型的注解中,让规则与数据天然贴合。

2. 用法非常轻量

引入依赖(选一):Spring Boot 2.x 用 javax 版;3.x 用 jakarta 版

  • javax(Spring Boot 2.x)
xml 复制代码
<dependency>
  <groupId>cn.sticki</groupId>
  <artifactId>spel-validator-javax</artifactId>
  <version>Latest Version</version>
</dependency>
  • jakarta(Spring Boot 3.x)
xml 复制代码
<dependency>
  <groupId>cn.sticki</groupId>
  <artifactId>spel-validator-jakarta</artifactId>
  <version>Latest Version</version>
</dependency>

两步开启

  • 方法参数上:@Valid@Validated
  • 被校验类/字段/参数上:@SpelValid

示例:

java 复制代码
@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 约束系统,可放在类、字段、方法参数、构造参数。

通用约束注解(均支持 conditionmessagegroup

  • 断言/空值/长度/数值/时间等
    • @SpelAssert(≈ @AssertTrue
    • @SpelNotNull / @SpelNull / @SpelNotEmpty / @SpelNotBlank
    • @SpelSize(字符串/集合/Map/数组)
    • @SpelMin / @SpelMax(支持 NumberCharSequence;支持 inclusive
    • @SpelDigits(整数/小数位数控制,支持 NumberCharSequence
    • @SpelPast / @SpelPastOrPresent / @SpelFuture / @SpelFutureOrPresent(覆盖 JDK8 time、Date/Calendar、各 Chrono* 类型)

注解均内置国际化消息键,可按需覆盖。

4. 表达式写法与上下文

  • 基于 SpEL,支持算术、关系、逻辑、三元、成员访问、集合、空安全、空合并等操作。
  • 根对象为当前被验证的对象,直接用 #this.fieldName 引用其他字段。
  • 可调用静态方法:T(全类名).method(...)
  • 可调用 Spring Bean:@beanName.method(...)(需启用 @EnableSpelValidatorBeanRegistrar

这使业务校验与领域知识复用更自然:调用 Enum 工具、Number/BigDecimal 处理、或领域服务,皆可在注解里完成。

5. 分组与条件:两层可组合

  • 条件开关(condition
    任何约束注解都可带 condition,决定该注解是否生效。
  • 分组(group + spelGroups
    @SpelValid(spelGroups = "#this.type") 启用分组语义;
    约束注解的 group 指定表达式数组。当 spelGroupsgroup 有交集(equals)时生效;group 为空则默认生效。
    这与 Jakarta 原生 groups 并行不冲突,前者服务于 SpEL 体系,后者仍可用于标准注解。

6. 国际化与消息插值

  • 多语言资源在 spel-validator-constrain/src/main/resources/cn/sticki/spel/validator 下提供默认翻译(zh、en、ja、ko 等)。
  • 你可以通过 ResourceBundleMessageResolver.addBasenames("YourBundle") 将自定义资源包加入优先队列,覆盖默认键。
  • 在注解 message 中使用 {your.key} 指定键;消息里如含 {}\ 需转义为 \\{ \\}\\

7. 性能与工程可用性

FAQ 的压测数据(基于示例项目、JDK8/SpringBoot 2.7):

  • 预热后单次校验通常 0~1ms
  • 强压 1500 ~ 9000 请求下,平均 0.11 ~ 0.13ms/次
  • 解析 SpEL 占大头(33%~65%),但整体可接受,且还有优化空间。

工程性细节:

  • 字段与注解层缓存(ConcurrentHashMap)减少反射与注解扫描开销。
  • 错误统一落入 Jakarta Validation 体系,继续沿用原有全局异常处理与返回体包装。
  • 既可按 Web 入口触发,也可在服务内部直接 validateObject,便于单元测试或离线任务复用。

8. 和 Jakarta/Bean Validation 的关系

  • 不是替代,而是扩展:继续使用你熟悉的 @NotNull/@Size 等标准注解;当遇到"跨字段/条件式/复杂逻辑/需调用 Bean 或静态方法"时,用对应的 SpEL 注解来补齐空白。
  • 触发机制严格遵循规范:只有 @Valid/@Validated + @SpelValid 同时存在,SpEL 校验才会生效。
  • 分组保持一致心智模型:Jakarta 原生 groups 仍可用;SpEL 的 spelGroups/group 更贴近表达式驱动的动态分组需求。

9. 可扩展性:自定义你的约束

  • 自定义步骤与 Jakarta 的方式几乎一致:
    1. 定义注解并包含 message/condition/group 三属性;
    2. 实现 SpelConstraintValidator<MyAnno>,在 isValid 中取字段值并校验;可覆写 supportType 收窄类型;
    3. 在注解上用 @SpelConstraint(validatedBy = XxxValidator.class) 关联校验器。
      得益于 SpelValidExecutor 的统一驱动,你的自定义注解天然具备 condition、分组、I18n 能力。

10. 适用边界与注意事项

  • 能用标准注解解决的,尽量先用标准注解;SpEL 注解适合"条件式/跨字段/复杂逻辑"的增量增强。
  • 表达式应避免副作用:不要修改对象状态;Validator 接口也强调线程安全不可变
  • 启用调用 Spring Bean 时,记得添加 @EnableSpelValidatorBeanRegistrar

结语

SpEL Validator 在不破坏现有校验体系的前提下,让"条件式/跨字段/复杂逻辑校验"回归到直观、声明式的写法,规则靠近数据、表达式靠近业务。它与 Jakarta Validation 既兼容又互补:你可以继续使用熟悉的 @NotNull/@Size,同时把"过去很难写顺"的场景交给 @SpelNotNull/@SpelAssert/@SpelSize/... 这套 SpEL 注解去解决。

若你的项目里存在以下任一特征,那么SpEL Validator便值得一试:

  • 参数规则取决于其他字段或分组;
  • 需要调用静态方法/领域服务完成复杂判断;
  • 希望在不破坏原有异常/返回体治理的前提下增强校验能力;
  • 追求更可读、更贴近业务的"校验即声明"。

项目文档已覆盖入门、注解索引、SpEL 要点、I18n、FAQ 与升级日志,源码结构清晰、测试与报告完善,接入成本低。在线文档:spel-validator.sticki.cn/

现在就把那些分散在各处的"如果...则校验..."逻辑收拢回模型,让校验像业务规则一样清晰可见吧。

相关推荐
无奈何杨2 小时前
CoolGuard增加枚举字段支持,条件编辑优化,展望指标取值不同
前端·后端
这里有鱼汤2 小时前
小白必看:QMT里的miniQMT入门教程
后端·python
brzhang2 小时前
当AI接管80%的执行,你“不可替代”的价值,藏在这20%里
前端·后端·架构
绝无仅有3 小时前
后端 Go 经典面试常见问题解析与总结
后端·面试·github
绝无仅有3 小时前
后端工程师面试常见问题与回答解析总结
后端·面试·github
程序员爱钓鱼5 小时前
Go语言实战案例 — 项目实战篇:简易博客系统(支持评论)
前端·后端·go
Grey Zeng11 小时前
Java SE 25新增特性
java·jdk·jdk新特性·jdk25
追逐时光者12 小时前
精选 4 款基于 .NET 开源、功能强大的 Windows 系统优化工具
后端·.net
雨白12 小时前
Java 线程通信基础:interrupt、wait 和 notifyAll 详解
android·java