轻量级 Java 断言框架 —— 让条件校验更优雅,告别重复的
if-throw代码
📖 English Documentation · English Changelog
在日常开发中,我们经常需要编写这样的代码:
if (req == null) {
throw new RuntimeException("参数不可为空");
}
if (req.getId() == null) {
throw new RuntimeException("id 不可为空");
}Api-Assert 将这些重复的条件判断封装为链式调用,让代码更加简洁、可读、易维护。
FunctionApiAssert apiAssert = FunctionApiAssert.create(RuntimeException::new);
apiAssert.isNull(req, "参数不可为空")
.isNull(req.getId(), "id 不可为空");将业务代码的执行流程抽象为一个
assert对象,通过流程划分保证代码质量的下限,提高可维护性。
Maven
<dependency>
<groupId>io.github.min1854</groupId>
<artifactId>apiAssert</artifactId>
<version>2.0.6</version>
</dependency>Gradle
implementation 'io.github.min1854:apiAssert:2.0.6'import io.github.min1854.apiAssert.check.FunctionApiAssert;
public class OrderService {
// 将断言声明为常量,避免重复创建
private static final FunctionApiAssert ASSERT =
FunctionApiAssert.create(IllegalArgumentException::new);
public void createOrder(CreateOrderRequest req) {
ASSERT.isNull(req, "请求参数不能为空")
.nonNull(req.getId(), "不可手动提交 ID")
.isTrue(req.getAmount() <= 0, "金额必须大于 0");
// 业务逻辑...
}
}如果任一条件成立,会立即抛出 IllegalArgumentException,并携带对应的错误信息。
| 检查器 | 异常创建方式 | 是否链式 | 是否立即抛出 | 适用场景 |
|---|---|---|---|---|
FunctionApiAssert |
函数式(由调用者提供) | ✅(多个条件校验) | ✅ | 常量定义、工具类、简单参数校验 |
OperateApiAssert |
函数式 | ✅(支持对象属性校验) | ✅ | 复杂对象校验、链式属性判断 |
EnumFunctionApiAssert |
枚举消息(函数式) | ✅ | ✅ | 国际化场景、统一错误码管理 |
EnumOperateApiAssert |
枚举消息 | ✅ | ✅ | 对象属性校验 + 国际化错误码 |
FirstApiAssert |
无(仅记录) | ✅ | ❌(需手动调用) | 收集多个校验结果,最后统一处理 |
ReflectionApiAssert |
反射 | ✅ | ✅ | 异常类型在运行时才能确定 |
IMApiAssert |
IM 接口消息 | ✅ | ✅ | 统一错误码 + 结果描述,轻量级消息接口 |
IRApiAssert |
IR 接口消息 | ✅ | ✅ | 统一错误码 + 结果描述 + 结果数据 |
- 简单参数校验 →
FunctionApiAssert - 对象属性校验(如
req.getXxx()) →OperateApiAssert - 需要国际化或错误码 →
EnumFunctionApiAssert/EnumOperateApiAssert - 需要收集所有错误后统一处理 →
FirstApiAssert - 需要统一错误码 + 结果描述 →
IMApiAssert - 需要统一错误码 + 结果描述 + 结果数据 →
IRApiAssert
FunctionApiAssert apiAssert = FunctionApiAssert.create(RuntimeException::new);
apiAssert.isNull(obj, "对象不能为空")
.nonNull(obj, "对象必须为空")
.isTrue(flag, "flag 为 true,抛出异常")
.isFalse(flag, "flag 为 false,抛出异常")
.isEmpty(collection, "集合不能为空");// 创建一个带有被测对象的校验器
OperateApiAssert<User> apiAssert = OperateApiAssert.create(user, RuntimeException::new);
// Lambda 方式获取属性,类似 MyBatis-Plus 的 LambdaWrapper
apiAssert.isNull(User::getName, "用户名不能为空")
.isFalse(User::getActive, "用户未激活")
.isEmpty(User::getOrders, "订单列表不为空");OperateApiAssert<User> userAssert = OperateApiAssert.create(user, RuntimeException::new);
// 获取用户的 ID 并继续校验
OperateApiAssert<Integer> idAssert = userAssert.then(User::getId);
idAssert.isTrue(id -> id > 0, "用户 ID 必须大于 0");OperateApiAssert<User> apiAssert = OperateApiAssert.create(user, RuntimeException::new);
apiAssert.process(() -> {
System.out.println("校验前置处理");
})
.process(userObj -> {
System.out.println("当前用户: " + userObj);
})
.process((userObj, self) -> {
self.isNull(userObj.getParent(), "父级用户不存在");
});IMApiAssert 以 IM 接口作为消息载体,支持自定义结果码和结果描述,适用于需要统一错误码管理的场景。
// 定义实现 IM 接口的枚举
public enum IMEnum implements IM {
SUCCESS(200, "success"),
FAIL(500, "%s"),
;
// ... 实现 IM 接口的 format 等方法
@Override
public IM format(Object... arguments) {
// 就算是已经转换为接口,这里的 getMessage 仍然是枚举中的 getMessage
String message = String.format(this.getMessage(), arguments);
return new IM() {
@Override
public int getCode() {
return IMEnum.this.getCode();
}
@Override
public String getMessage() {
return message;
}
@Override
public IM format(Object... arguments) {
return IMEnum.this.format(arguments);
}
@Override
public String toString() {
return "IM{" +
"code=" + getCode() +
", message='" + getMessage() + '\'' +
'}';
}
};
}
}
// 使用 IMApiAssert
IMApiAssert apiAssert = IMApiAssert.create(IMException::new);
apiAssert.isNull(obj, IMEnum.FAIL.format("对象不能为空"))
.nonNull(obj, IMEnum.FAIL.format("对象必须为空"))
.isTrue(flag, IMEnum.FAIL.format("flag 为 true,抛出异常"))
.isFalse(flag, IMEnum.FAIL.format("flag 为 false,抛出异常"))
.isEmpty(collection, IMEnum.FAIL.format("集合不能为空"));IRApiAssert 以 IR 接口作为消息载体,在 IM 的基础上额外支持结果数据,适用于需要统一错误码且携带业务数据的场景。
// 定义实现 IR 接口的枚举
public enum IREnum implements IR {
SUCCESS(200, "success"),
FAIL(500, "%s"),
;
// ... 实现 IR 接口的 format、toIR 等方法
@Override
public IR format(Object... arguments) {
String message = String.format(getMessage(), arguments);
return new IR() {
@Override
public int getCode() {
return IREnum.this.getCode();
}
@Override
public String getMessage() {
return message;
}
@Override
public IR format(Object... arguments) {
return IREnum.this.format(arguments);
}
@Override
public <T> IR toIR(T data, Object... arguments) {
return IREnum.this.toIR(data, arguments);
}
@Override
public String toString() {
return "IR{" +
"code=" + getCode() +
", message='" + getMessage() + '\'' +
", data=" + getData() +
'}';
}
};
}
@Override
public <T> IR toIR(T data, Object... arguments) {
String message = String.format(getMessage(), arguments);
return new IR() {
@Override
public int getCode() {
return IREnum.this.getCode();
}
@Override
public String getMessage() {
return message;
}
@Override
public <T> T getData() {
return (T) data;
}
@Override
public IR format(Object... arguments) {
return IREnum.this.toIR(data, arguments);
}
@Override
public <T> IR toIR(T data, Object... arguments) {
return IREnum.this.toIR(data, arguments);
}
@Override
public String toString() {
return "IR{" +
"code=" + getCode() +
", message='" + getMessage() + '\'' +
", data=" + getData() +
'}';
}
};
}
}
// 使用 IRApiAssert
IRApiAssert apiAssert = IRApiAssert.create(IRException::new);
apiAssert.isNull(obj, IREnum.FAIL.format("对象不能为空"))
.nonNull(obj, IREnum.FAIL.format("对象必须为空"))
.isTrue(flag, IREnum.FAIL.format("flag 为 true,抛出异常"))
.handler(IREnum.FAIL.format("直接抛出异常"));
// StandardApiAssert 均支持携带 cause 异常链
apiAssert.handler(IREnum.FAIL.toIR("此次异常数据", "传入了为空的对象"), new RuntimeException("真实异常"));public class DemoService {
private static final FunctionApiAssert ASSERT =
FunctionApiAssert.create(IllegalArgumentException::new);
public void processOrder(OrderReq req) {
OperateApiAssert<OrderReq> apiAssert =
OperateApiAssert.create(req, IllegalArgumentException::new);
apiAssert.isNull(OrderReq::getOrderId, "订单 ID 不能为空")
.isFalse(OrderReq::getAmount > 0, "金额必须大于 0")
.process(() -> validateInventory(req))
.then(OrderReq::getOrderId)
.isTrue(id -> id.startsWith("ORD"), "订单 ID 格式错误");
// 业务逻辑...
}
private void validateInventory(OrderReq req) {
// 库存校验逻辑
}
}2.0 版本进行了完全重构,与 1.x 不兼容:
- 包名变更(1.x 与 2.x 不同)
- 继承体系重构
- 部分 API 方法签名调整
如果你正在使用 1.x 版本,建议阅读 迁移指南(待补充)
- 新增:
IMApiAssert—— 以IM接口作为消息载体的断言检查器 - 新增:
IRApiAssert—— 以IR接口作为消息载体的断言检查器(支持结果数据)
| 版本 | 主要变更 |
|---|---|
| 2.0.5 | 所有 message 仅在条件成立后执行,避免无意义的字符串拼接开销 |
| 2.0.4 | 包名变更为 io.github.min1854 |
| 2.0.3 | 元组类重构,新增新的元组类 |
| 2.0.2 | handler 默认方法提升至 StandardApiAssert |
| 2.0.1 | OperationApiAssert 增加 handler 默认方法 |
| 2.0.0 | 完全重构,新增枚举校验器 |
完整版本日志请查看 CHANGELOG.md | English Version
FunctionApiAssert是无状态的,可以作为常量在多个线程中共享OperateApiAssert持有具体对象实例,不应跨线程共享
可以。FunctionApiAssert.create(Supplier<? extends RuntimeException>) 支持传入任何 RuntimeException 的子类。
不会。框架保证 message 只在条件成立时执行,不会产生不必要的开销。
| 特性 | Spring Assert | Api-Assert |
|---|---|---|
| 链式调用 | ❌ | ✅ |
| Lambda 属性获取 | ❌ | ✅ |
| 自定义异常 | 有限 | 完全支持 |
| 对象转换继续校验 | ❌ | ✅(then 方法) |
Apache 2.0