参数校验是后端开发中一个高频且重要的环节。虽然利用 @NotNull、@Size 等标准注解能够解决大多数简单场景,但面对枚举值校验、多字段联合校验等复杂业务逻辑时,这些基础注解就有些力不从心了。为了应对这些复杂场景,往往需要编写额外的校验代码,导致校验逻辑分散,不利于维护。
因此,我开发了一款基于 SpEL(Spring Expression Language) 表达式、扩展自 javax.validation 规范的开源参数校验组件,旨在以统一的注解方式处理几乎所有复杂校验场景。
项目地址:https://github.com/stick-i/spel-validator
它能解决哪些问题?
-
枚举值字段校验:通过调用枚举类的静态方法进行校验。
@SpelAssert(assertTrue = " T(cn.sticki.enums.UserStatusEnum).getByCode(#this.userStatus) != null ", message = "用户状态不合法")
private Integer userStatus;
-
多字段联合校验:根据一个字段的值,决定是否校验另一个字段。
@NotNull
private Integer contentType;
@SpelNotNull(condition = “#this.contentType == 1“, message = “语音内容不能为空“)
private Object audioContent;
@SpelNotNull(condition = “#this.contentType == 2“, message = “视频内容不能为空“)
private Object videoContent;
-
复杂逻辑校验:在表达式中调用外部静态方法执行自定义逻辑。
// 中文算两个字符,英文算一个字符,要求总长度不超过 10
// 调用外部静态方法进行校验
@SpelAssert(assertTrue = “T(cn.sticki.util.StringUtil).getLength(#this.userName) <= 10“, message = “用户名长度不能超过10“)
private String userName;
-
调用 Spring Bean(需额外配置):在表达式中直接引用Spring容器中的Bean。
// 这里只是简单举例,实际开发中不建议这样判断用户是否存在
@SpelAssert(assertTrue = “@userService.getById(#this.userId) != null“, message = “用户不存在“)
private Long userId;
组件核心特点
- 无缝集成:作为
javax.validation 的扩展,只新增注解而不修改原有校验体系,可以轻松集成到现有 Spring Boot 项目中。
- 基于SpEL表达式:利用强大的SpEL,支持非常复杂的校验逻辑,并支持对象内字段间的联动校验。
- 支持调用Spring Bean:在开启支持后,可在表达式中直接使用注入的Spring Bean。
- 支持自定义注解:允许开发者根据具体业务需求,定义自己的校验注解和验证器。
- 统一异常处理:校验失败时,异常会统一并入
javax.validation 的异常体系,无需额外处理。
- 易于上手:使用方式与标准JSR-380注解高度一致,学习成本低。
环境要求
当前已在 JDK 8 环境中完成测试,理论上支持 JDK 8 及以上版本。
快速入门指南
1. 添加项目依赖
当前最新版本为 0.0.2-beta。
<dependency>
<groupId>cn.sticki</groupId>
<artifactId>spel-validator</artifactId>
<version>Latest Version</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot-starter-web.version}</version>
</dependency>
2. 在Controller层启用校验
在接口参数上使用 @Valid 或 @Validated 注解。
@RestController
@RequestMapping(“/example“)
public class ExampleController {
/**
* 简单校验示例
*/
@PostMapping(“/simple“)
public Resp<Void> simple(@RequestBody @Valid SimpleExampleParamVo simpleExampleParamVo) {
return Resp.ok(null);
}
}
3. 在实体类上应用校验
在类上标注 @SpelValid,在字段上使用 @SpelNotNull 等约束注解。
@Data
@SpelValid
public class SimpleExampleParamVo {
@NotNull
private Boolean switchAudio;
/**
* 当 switchAudio 为 true 时,校验 audioContent,audioContent 不能为null
*/
@SpelNotNull(condition = “#this.switchAudio == true“, message = “语音内容不能为空“)
private Object audioContent;
}
4. 添加全局异常处理(可选但建议)
统一处理校验失败抛出的异常。
@RestControllerAdvice
public class ControllerExceptionAdvice {
@ExceptionHandler({BindException.class, MethodArgumentNotValidException.class})
public Resp<Void> handleBindException(BindException ex) {
String msg = ex.getFieldErrors().stream()
.map(error -> error.getField() + “ “ + error.getDefaultMessage())
.reduce((s1, s2) -> s1 + “,“ + s2)
.orElse(““);
return new Resp<>(400, msg);
}
}
5. 测试校验效果
-
场景一:@SpelNotNull 校验不通过
- 请求体:
{ “switchAudio“: true, “audioContent“: null }
- 响应体:
{ “code“: 400, “message“: “audioContent 语音内容不能为空“, “data“: null }
-
场景二:校验通过
- 请求体:
{ “switchAudio“: false, “audioContent“: null }
- 响应体:
{ “code“: 200, “message“: “成功“, “data“: null }
-
场景三:标准 @NotNull 校验不通过
- 请求体:
{ “switchAudio“: null, “audioContent“: null }
- 响应体:
{ “code“: 400, “message“: “switchAudio 不能为null“, “data“: null }
详细使用说明
重要提示:本组件旨在扩展 javax.validation,处理其难以应对的复杂场景。在标准注解可满足需求时,应优先使用标准注解以获得更好性能,因为基于SpEL的解析会带来一定的性能开销。
如何触发校验?
校验生效需要同时满足两个条件:
- 接口参数上使用了
@Valid 或 @Validated 注解。
- 待校验的实体类上使用了
@SpelValid 注解。
仅满足条件1时,只有 @NotNull 等标准注解会生效;仅满足条件2时,不会执行任何校验。这是因为 @SpelValid 本身是一个约束注解,需要被 @Valid 或 @Validated 扫描到才能激活其校验逻辑。
内置约束注解列表
| 注解 |
说明 |
对标 javax.validation |
@SpelAssert |
逻辑断言校验 |
无 |
@SpelNotNull |
非 null 校验 |
@NotNull |
@SpelNotEmpty |
集合、字符串、数组大小非空校验 |
@NotEmpty |
@SpelNotBlank |
字符串非空串校验 |
@NotBlank |
@SpelNull |
必须为 null 校验 |
@Null |
@SpelSize |
集合、字符串、数组长度校验 |
@Size |
每个约束注解都包含三个公共属性:
message:校验失败时的提示信息。
group:分组条件,支持SpEL表达式。只有当校验分组与此处指定的分组有交集时,校验才会执行。
condition:约束开启条件,支持SpEL表达式。当表达式为空或计算结果为 true 时,才会执行校验。
启用Spring Bean支持
默认情况下,SpEL解析器无法识别表达式中的 @beanName 引用。若需要在表达式中调用Spring Bean,请在项目启动类上添加 @EnableSpelValidatorBeanRegistrar 注解。
@EnableSpelValidatorBeanRegistrar
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
如何自定义约束注解?
自定义约束注解的过程与 javax.validation 非常相似,主要分为三步:
- 定义注解,并使用
@SpelConstraint(validatedBy = YourValidator.class) 指定验证器。
- 在自定义注解中添加
message、group、condition 这三个固定字段。
- 实现验证器类,继承
SpelConstraintValidator<A> 接口。
定义约束注解示例:

实现验证器示例:

具体实现细节可参考源码中的 cn.sticki.validator.spel.SpelConstraint 类。
示例项目与问题讨论
为了帮助大家更快上手,我提供了一个简单的示例项目(仍在完善中):
关于性能的说明
目前组件内部使用了较多反射操作,可能会带来一定的性能损耗。未来的优化计划包括增加缓存机制,以尽量降低这部分影响。欢迎有性能优化经验的开发者参与这个 开源实战 项目,共同改进。
开发中遇到的一个现象
在使用 IntelliJ IDEA(2024.1 Ultimate Edition)时,发现一个有趣的现象:即使为注解字段都标记了 @Language(“SpEL“),IDE 也并非总能正确识别并提供代码提示。

如上图所示,condition 字段的表达式被识别了,而 assertTrue 字段的表达式却没有。不清楚这是否是 IDE 的特定行为,欢迎了解原因的朋友在评论区或项目 Issues 中探讨。
结语
这款 SpEL Validator 组件是我为了解决实际开发中的复杂校验痛点而构建的。它充分利用了 SpEL 表达式的灵活性,将原本需要散落在各处的校验逻辑,以一种声明式、统一的方式进行了归集。
项目已开源,GitHub 地址:https://github.com/stick-i/spel-validator 。欢迎大家下载使用、体验反馈,更期待能收到 issue 提交和 pull request,共同完善这个工具。如果你在 云栈社区 或其他技术平台看到关于参数校验的深入讨论,也欢迎将观点和实践反馈到项目中。