引言

当你还在手写 if (param == null) throw new IllegalArgumentException(...) 时,Spring 已经帮你把这一切封装成了优雅的一行断言。

Spring 常用类深度解析系列开篇:那些我们每天都在用,却未必真正理解的类

一、先来认识一下 Spring Assert

1.1 它是什么?

Assert 是 Spring Framework 中位于 org.springframework.util 包下的一个断言工具类,从 Spring 1.1.x 版本就已存在。它提供了一组静态方法,用于在代码中进行条件检查,当条件不满足时抛出指定的运行时异常(通常是 IllegalArgumentExceptionIllegalStateException)。

不同于单元测试中的断言(JUnit 的 Assert 类),Spring Assert 的目标是在业务代码和框架代码中进行防御性参数校验,让你用一行方法调用来替代传统的 if-throw 代码块,显著提升代码可读性和整洁度。

1.2 为什么要用 Assert?

在日常开发中,方法入口处的参数校验必不可少:

// 传统写法,又臭又长
public void register(String username, String password, Integer age) {
    if (username == null || username.trim().isEmpty()) {
        throw new IllegalArgumentException("用户名不能为空");
    }
    if (password == null || password.length() < 6) {
        throw new IllegalArgumentException("密码长度不能少于6位");
    }
    if (age == null || age < 0 || age > 150) {
        throw new IllegalArgumentException("年龄必须在0-150之间");
    }
    // 业务逻辑...
}

这段代码中,业务逻辑被大量的参数校验淹没,不仅难以阅读,还容易遗漏或出错。

使用 Spring Assert 重构后:

// Spring Assert 方式
public void register(String username, String password, Integer age) {
    Assert.hasText(username, "用户名不能为空");
    Assert.isTrue(password != null && password.length() >= 6, "密码长度不能少于6位");
    Assert.isTrue(age != null && age >= 0 && age <= 150, "年龄必须在0-150之间");
    // 业务逻辑...
}

代码量减少 60%,意图一目了然。每个断言方法都包含了条件检查和异常消息,无需手写 ifthrow

二、核心方法全解析

Spring Assert 提供了 10+ 个静态方法,按功能可分为几大类。下面逐个解析,并给出典型使用场景。

2.1 非空判断:notNull / notEmpty / hasText

这是日常开发中使用频率最高的方法组。

方法 检查条件 抛出异常 适用场景
notNull(Object object, String message) object != null IllegalArgumentException 对象不能为空
notEmpty(Collection<?> collection, String message) collection != null && !collection.isEmpty() IllegalArgumentException 集合不能为空
notEmpty(Map<?,?> map, String message) map != null && !map.isEmpty() IllegalArgumentException Map不能为空
notEmpty(Object[] array, String message) array != null && array.length > 0 IllegalArgumentException 数组不能为空
hasLength(String text, String message) text != null && text.length() > 0 IllegalArgumentException 字符串有长度(允许空格)
hasText(String text, String message) text != null && text.trim().length() > 0 IllegalArgumentException 字符串有实际内容(非空格)

注意 hasLengthhasText 的区别

  • hasLength(" ") → 通过(因为长度为2)
  • hasText(" ") → 失败(因为 trim 后为空)

实战示例:

public void processUser(User user) {
    Assert.notNull(user, "用户对象不能为null");
    Assert.hasText(user.getName(), "用户名不能为空或全空格");
    Assert.notEmpty(user.getRoles(), "用户至少需要一个角色");
}
2.2 布尔条件:isTrue

当条件表达式较为复杂,无法用上述专有方法覆盖时,使用 isTrue

Assert.isTrue(age >= 18 && age <= 60, "年龄必须在18-60之间");
Assert.isTrue(list.size() <= 100, "列表元素不能超过100个");

isTrue 是最通用的断言,内部只是一句 if (!expression) throw new IllegalArgumentException(message);

2.3 状态检查:state

isTrue 功能相似,但抛出的是 IllegalStateException(非法状态异常),而非 IllegalArgumentException(非法参数异常)。语义上更适用于对象状态不符合预期的情况。

// 连接池关闭后无法获取连接
if (!pool.isOpen()) {
    throw new IllegalStateException("连接池已关闭");
}
// 使用 Assert
Assert.state(pool.isOpen(), "连接池已关闭");
2.4 类型检查:isInstanceOf / isAssignable

用于检查类型关系,抛出 IllegalArgumentException

// 确保对象是指定类型的实例
Assert.isInstanceOf(SubClass.class, obj, "obj必须是SubClass的实例");

// 确保某个类型可以赋值给另一个类型(继承或实现关系)
Assert.isAssignable(ParentClass.class, SubClass.class, "SubClass必须继承自ParentClass");

这些方法在框架代码或反射操作中非常有用。

2.5 noNullElements:集合/数组中不能包含 null 元素
List<String> list = Arrays.asList("a", null, "c");
Assert.noNullElements(list, "列表中不能包含null元素");
// 抛出 IllegalArgumentException: 列表中不能包含null元素

该方法会遍历集合,发现第一个 null 元素立即抛出异常。

2.6 其他
  • isNull:断言一个对象是{@code null}。
  • doesNotContain:断言给定文本不包含给定子串。

PS:以上所有的断言方法在Spring 5.0+ 新增了 Supplier<String> 重载方法。消息参数均支持 接收一个 Supplier<String> 函数式接口参数 来构建断言提示消息;String 版本无论断言结果如何都会立即构建消息,而 Supplier 版本仅在断言失败时才延迟生成消息,从而避免正常路径下的性能开销。

三、源码深度剖析

3.1 notNull 的实现

在这里插入图片描述
在这里插入图片描述

非常简洁,没有多余的逻辑。这种简单性正是工具类的精髓——做且仅做一件事,做好它。

3.2 hasText 的实现(稍有复杂度)

在这里插入图片描述
在这里插入图片描述

内部调用了 StringUtils.hasText(String str),其实现为:
在这里插入图片描述

3.3 isInstanceOf 的实现

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

先检查目标类型不为空,再使用 Class.isInstance() 进行判断。注意这里没有对 obj 判空——因为 null 不是任何类的实例(除了 null 本身),所以 type.isInstance(null) 返回 false,会抛出异常,符合预期。

四、与 Java 原生断言的区别和联系

许多 Java 开发者知道 assert 关键字,但 Spring Assert 与它有着本质区别。

特性 Spring Assert Java 原生 assert
启用方式 始终生效,无需 JVM 参数 需要 -ea 参数显式开启,生产环境通常关闭
抛出异常 IllegalArgumentException, IllegalStateException AssertionError(Error 而非 Exception)
语义 参数校验、状态校验(防御性编程) 调试期间的不变量检查
能否在生产代码中使用 强烈推荐 不推荐(默认关闭,不会执行)
性能影响 每次都会执行,但开销极小 关闭时零开销

结论:Java 原生 assert 主要用作开发和测试阶段的内部不变量检查,不应依赖它进行生产环境的参数校验。而 Spring Assert 正是为了解决生产环境下的参数校验需求而设计的,是线上代码的第一道防线

五、最佳实践:如何用好 Assert

5.1 在公共方法入口处进行防御性校验
public void updateUser(Long userId, UserDto dto) {
    Assert.notNull(userId, "用户ID不能为空");
    Assert.notNull(dto, "更新数据不能为空");
    Assert.hasText(dto.getName(), "用户姓名不能为空");
    // 业务逻辑...
}
5.2 结合错误码,统一异常处理

在实际项目中,通常不会直接把 IllegalArgumentException 抛给前端,而是配合全局异常处理器,将异常转换为统一的业务错误响应。

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArg(IllegalArgumentException ex) {
        return ResponseEntity.badRequest().body(new ErrorResponse(400, ex.getMessage()));
    }
}

这样,Assert 抛出的异常就能被优雅地转换成前端可以理解的错误信息。

5.3 避免过度使用

Assert 并非万能。对于复杂校验(如跨字段校验、调用数据库校验),仍然需要手写逻辑。但 isTrue 可以作为这些复杂校验结果的最终断言。

// 复杂校验
boolean isValid = validateComplexCondition(order);
Assert.isTrue(isValid, "订单状态不满足取消条件");
5.4 消息模板使用占位符

Spring Assert 的消息参数仅支持字符串拼接,不支持 SLF4J 风格的占位符。如果你需要更灵活的消息格式化,可以自行拼接或使用 String.format

Assert.notNull(user, String.format("用户[%s]不存在", userId));

六、总结与展望

Spring Assert 是一个看似简单却极其实用的工具类。它没有复杂的功能,只专注于一件事:用一行代码完成参数校验并抛出异常。这种“小而美”的设计,恰恰是 Spring 工具类的核心理念——为开发者的日常编程减负,让代码更清晰、更安全。

核心要点回顾:

  • 用途:替代手写 if (condition) throw new XxxException,用于防御性参数校验。
  • 时机:公共方法入口处、框架代码中、任何需要对输入或状态进行校验的地方。
  • 优势:代码简洁、语义清晰、异常类型统一。
  • 注意:不要与 Java 原生 assert 混淆;Assert 永远生效,原生 assert 默认关闭。

下一篇文章我们将继续探索 “Spring 常用类深度剖析(工具篇 06):StopWatch:精准计时的优雅实现”。敬请期待!


思考题:在你的项目中,除了参数校验,还有哪些场景可以用 Assert 来简化代码?尝试重构一段现有的 if-throw 代码,并对比重构前后的代码量和可读性。(欢迎在评论区分享你的重构案例)

:Spring Assert 的核心价值在于 “快速失败”(Fail-Fast)与“代码简洁” ,除了参数校验还有一些其他场景,其应用场景可总结为以下四点:

  1. 参数校验(前置条件)
    确保方法入参合法(非空、非空集合、数值范围等),明确方法契约。
    常用方法notNull, hasText, isTrue, notEmpty.
  2. 状态检查(不变量维护)
    确保对象在执行操作前处于正确的内部状态(如:未初始化不可使用、状态机流转合法)。
    常用方法state (抛出 IllegalStateException).
  3. 类型安全(防御性编程)
    在进行向下转型或反射调用前,验证对象类型兼容性,避免晦涩的 ClassCastException
    常用方法isInstanceOf, isAssignable.
  4. 结果验证(后置条件)
    在关键逻辑执行后,断言结果符合预期(如:数据库保存后必须生成 ID),便于早期发现逻辑 Bug。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐