找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2090

积分

0

好友

274

主题
发表于 3 天前 | 查看: 16| 回复: 0

参数校验是后端开发中一个高频且重要的环节。虽然利用 @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的解析会带来一定的性能开销。

如何触发校验?

校验生效需要同时满足两个条件:

  1. 接口参数上使用了 @Valid@Validated 注解。
  2. 待校验的实体类上使用了 @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 非常相似,主要分为三步:

  1. 定义注解,并使用 @SpelConstraint(validatedBy = YourValidator.class) 指定验证器。
  2. 在自定义注解中添加 messagegroupcondition 这三个固定字段。
  3. 实现验证器类,继承 SpelConstraintValidator<A> 接口。

定义约束注解示例:
自定义SpEL校验注解代码示例

实现验证器示例:
自定义SpEL校验器实现代码示例

具体实现细节可参考源码中的 cn.sticki.validator.spel.SpelConstraint 类。

示例项目与问题讨论

为了帮助大家更快上手,我提供了一个简单的示例项目(仍在完善中):

关于性能的说明

目前组件内部使用了较多反射操作,可能会带来一定的性能损耗。未来的优化计划包括增加缓存机制,以尽量降低这部分影响。欢迎有性能优化经验的开发者参与这个 开源实战 项目,共同改进。

开发中遇到的一个现象

在使用 IntelliJ IDEA(2024.1 Ultimate Edition)时,发现一个有趣的现象:即使为注解字段都标记了 @Language(“SpEL“),IDE 也并非总能正确识别并提供代码提示。
IDE对SpEL表达式的识别差异示例
如上图所示,condition 字段的表达式被识别了,而 assertTrue 字段的表达式却没有。不清楚这是否是 IDE 的特定行为,欢迎了解原因的朋友在评论区或项目 Issues 中探讨。

结语

这款 SpEL Validator 组件是我为了解决实际开发中的复杂校验痛点而构建的。它充分利用了 SpEL 表达式的灵活性,将原本需要散落在各处的校验逻辑,以一种声明式、统一的方式进行了归集。

项目已开源,GitHub 地址:https://github.com/stick-i/spel-validator 。欢迎大家下载使用、体验反馈,更期待能收到 issue 提交和 pull request,共同完善这个工具。如果你在 云栈社区 或其他技术平台看到关于参数校验的深入讨论,也欢迎将观点和实践反馈到项目中。





上一篇:开源流程引擎选型指南:2023年Camunda、Flowable、Activiti横向对比与推荐
下一篇:干货指南:使用SpEL Validator优雅实现Java参数校验,告别复杂逻辑
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 11:37 , Processed in 0.564042 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表