BizAssert:一个轻量级、生产就绪的 Java 业务断言工具类

灵感来源于 Spring 的 org.springframework.util.AssertBizAssert 专为业务逻辑校验 而设计。断言失败时抛出携带错误码的业务异常(而非 IllegalArgumentException),非常适合企业应用中 Service 层的参数与状态校验。


🔗 项目地址


  • 如果在使用过程中遇到问题,欢迎随时提交 Issue;也非常欢迎通过 PR 参与改进。
  • 如果这个项目对你有所帮助,欢迎在 GitHub 上点个 ⭐ Star 支持一下。
  • 你的支持是开源作者持续维护和迭代项目的重要动力!

✨ 特性

  • 🎯 面向业务 --- 默认抛出带错误码的 BizException,而非 IllegalArgumentException
  • 🔌 可插拔异常工厂 --- 支持全局替换默认异常类型,也支持单次调用指定异常
  • 🏷️ Label 机制 --- 避免重复编写完整错误消息(notNullAs(userId, "userId")
  • 📝 占位符消息 --- 支持 {}, {} 风格的消息格式化
  • 📋 错误码枚举 --- 一等支持 IErrorCode 错误码枚举
  • ↩️ Pass-through 返回值 --- notNullnotEmptynotBlank、数值断言直接返回校验后的值
  • 🧩 丰富的断言方法 --- 覆盖布尔、空值、字符串、集合、Map、数组、数值、正则、相等性等
  • 零依赖 --- 纯 Java 实现,无任何第三方依赖

📦 安装

Maven

xml 复制代码
<dependency>
    <groupId>io.github.vennarshulytz</groupId>
    <artifactId>biz-assert</artifactId>
    <version>1.0.0</version>
</dependency>

Gradle

groovy 复制代码
implementation 'io.github.vennarshulytz:biz-assert:1.0.0'

要求 Java 8+


🚀 快速开始

java 复制代码
import io.github.vennarshulytz.common.assert_.BizAssert;

// 基本用法 --- 为 null 时抛出 BizException
User user = BizAssert.notNull(userRepository.findById(id), "用户不存在");

// 指定错误码
BizAssert.notNull(userId, 10001, "userId 不能为空");

// 错误枚举
BizAssert.notNull(userId, BizError.USER_NOT_NULL);

// Label 机制 --- 自动生成消息:"userId must not be null"
BizAssert.notNullAs(userId, "userId");

// 占位符
BizAssert.notNull(userId, "{} 不能为空", "userId");

📖 详细使用说明

1. 全局异常工厂配置

默认情况下,所有断言失败都抛出 BizException。你可以在应用启动时替换一次

java 复制代码
// 默认行为 --- 抛出 BizException
BizAssert.notNull(null, "oops");
// → BizException{code=-1, message=oops}

// 全局替换(仅调用一次,例如在 main() 或 @PostConstruct 中)
BizAssert.setDefaultExceptionFactory(PaymentException::new);

// 现在所有断言都抛出 PaymentException
BizAssert.notNull(null, 10001, "oops");
// → PaymentException(10001, "oops")

⚠️ setDefaultExceptionFactory 只能调用一次 ,重复调用会抛出 IllegalStateException

对于只接受 (String message) 构造函数的异常:

java 复制代码
BizAssert.setDefaultExceptionFactory(
    ExceptionFactory.ofMessage(IllegalStateException::new)
);

2. 消息风格

每个断言方法都支持 7 种消息风格

java 复制代码
Object userId = null;

// ① 无消息 --- 使用默认消息
BizAssert.notNull(userId);
// → "parameter must not be null"

// ② 显式消息
BizAssert.notNull(userId, "userId 不能为空");
// → "userId 不能为空"

// ③ 错误码 + 消息
BizAssert.notNull(userId, 10001, "userId 不能为空");
// → code=10001, "userId 不能为空"

// ④ 占位符消息({}, {}, ...)
BizAssert.notNull(userId, "{} 不能为空", "userId");
// → "userId 不能为空"

// ⑤ 延迟消息(Supplier,仅在失败时求值)
BizAssert.notNull(userId, () -> entity.getName() + " 的 ID 为空");

// ⑥ 错误枚举
BizAssert.notNull(userId, BizError.USER_NOT_NULL);
// → code=10001, "user must not be null"

// ⑦ 错误枚举 + 占位符参数
BizAssert.notNull(userId, BizError.PARAM_INVALID, "userId");
// → code=10003, "userId is invalid"

Label 机制

对于常见的 "xxx must not be null" 模式,使用 xxxAs 简写:

java 复制代码
// 传统写法:
BizAssert.notNull(userId, "{} must not be null", "userId");

// Label 简写:
BizAssert.notNullAs(userId, "userId");
// → "userId must not be null"

// 同样支持:
BizAssert.notEmptyAs(name, "name");    // → "name must not be empty"
BizAssert.notBlankAs(email, "email");  // → "email must not be blank"
BizAssert.isPositiveAs(age, "age");    // → "age must be positive"

null 参数展示

占位符参数为 null 时,展示为 <null>

java 复制代码
BizAssert.isTrue(false, "{} is invalid", (Object) null);
// → "<null> is invalid"

3. 单次调用指定自定义异常

风格一:ExceptionFactory 参数

由框架构造异常。要求异常类有 (int, String) 构造函数:

java 复制代码
BizAssert.isTrue(order.isPaid(), "订单未支付", PaymentException::new);

BizAssert.isTrue(order.isPaid(), 20001, "订单未支付", PaymentException::new);

// 对于只有 (String) 构造函数的异常:
BizAssert.notNull(user, "未找到",
    ExceptionFactory.ofMessage(NotFoundException::new));

风格二:直接传入异常实例(xxxOrThrow

由调用方完全控制异常类型和内容,最大灵活性:

java 复制代码
BizAssert.isTrueOrThrow(order.isPaid(),
    () -> new PaymentException("订单未支付,ID=" + order.getId()));

User user = BizAssert.notNullOrThrow(userRepo.findById(id),
    () -> new NotFoundException("用户 " + id + " 不存在"));

String name = BizAssert.notBlankOrThrow(input,
    () -> new ValidationException("名称不能为空"));

4. Pass-through 返回值

校验值的断言方法直接返回该值,省去额外的变量赋值:

java 复制代码
// 传统写法
User user = userRepository.findById(id);
BizAssert.notNull(user, "用户不存在");
// 使用 user...

// Pass-through 写法 ✅
User user = BizAssert.notNull(userRepository.findById(id), "用户不存在");

// 可以直接串联使用
String name = BizAssert.notBlank(request.getName(), "名称不能为空");
List<Item> items = BizAssert.notEmpty(order.getItems(), "订单无商品");
int age = BizAssert.isPositive(request.getAge(), "年龄必须为正数");

返回值规则:

返回原值 返回 void
notNullT isTrue
notEmptyString / Collection / Map / T[] isFalse
notBlankString isEqual / notEqual
isPositiveint / long state
isNonNegativeint / long
isBetweenint / long
matchesString
failT(永不返回)

5. 布尔断言

java 复制代码
BizAssert.isTrue(order.isPaid(), "发货前必须完成支付");

BizAssert.isFalse(account.isLocked(), "账号已被锁定");

BizAssert.isTrue(amount > 0, 20001, "金额必须大于零");

BizAssert.isTrue(user.isActive(), BizError.USER_NOT_ACTIVE);

BizAssert.isTrueOrThrow(order.isPaid(),
    () -> new PaymentException("未支付订单:" + order.getId()));

6. 空值断言

java 复制代码
// notNull --- 返回校验后的值
User user = BizAssert.notNull(userRepo.findById(id), "用户不存在");

// notNullAs --- Label 机制
Long userId = BizAssert.notNullAs(request.getUserId(), "userId");

// isNull
BizAssert.isNull(existingUser, "用户已存在");

7. 字符串断言

java 复制代码
// notEmpty --- null 或 "" 不通过
String name = BizAssert.notEmpty(request.getName(), "名称不能为空");
// 注意:"   "(纯空格)可以通过 notEmpty

// notBlank --- null、""、纯空白字符都不通过
String email = BizAssert.notBlank(request.getEmail(), "邮箱不能为空");
// 返回原始值,不做 trim

// Label 变体
BizAssert.notEmptyAs(name, "name");   // → "name must not be empty"
BizAssert.notBlankAs(email, "email"); // → "email must not be blank"

8. 集合断言

java 复制代码
List<User> users = BizAssert.notEmpty(userRepo.findAll(), "未找到用户");

BizAssert.notEmpty(order.getItems(), BizError.ORDER_ITEMS_EMPTY);

List<String> tags = BizAssert.notEmptyAs(post.getTags(), "tags");
// → "tags must not be empty"

Set<Role> roles = BizAssert.notEmptyOrThrow(user.getRoles(),
    () -> new AuthException("用户无角色"));

9. Map 断言

java 复制代码
Map<String, Object> config = BizAssert.notEmpty(loadConfig(), "配置为空");

BizAssert.notEmptyAs(headers, "headers");

10. 数组断言

java 复制代码
String[] args = BizAssert.notEmpty(commandArgs, "未提供参数");

BizAssert.notEmptyAs(permissions, "permissions");

11. 相等性断言

java 复制代码
BizAssert.isEqual(actual.getStatus(), "ACTIVE", "状态必须为 ACTIVE");

BizAssert.isEqual(a, b, BizError.VALUE_MISMATCH, "a", "b");

BizAssert.notEqual(newPassword, oldPassword, "新密码不能与旧密码相同");

// null 安全:isEqual(null, null) 通过

12. 数值断言

所有数值断言都支持 Pass-throughint/long 两种类型:

java 复制代码
int quantity = BizAssert.isPositive(request.getQuantity(), "数量必须为正数");

long balance = BizAssert.isNonNegative(account.getBalance(), "余额不足");

int age = BizAssert.isBetween(request.getAge(), 1, 150, "年龄不合法");

// isBetween 的默认消息:
BizAssert.isBetween(age, 18, 65);
// → "value must be between 18 and 65, but was 16"

// Label 变体
BizAssert.isPositiveAs(amount, "amount");       // → "amount must be positive"
BizAssert.isNonNegativeAs(balance, "balance");  // → "balance must be non-negative"
BizAssert.isBetweenAs(age, 18, 65, "age");      // → "age must be between 18 and 65, but was 16"

// 错误枚举
BizAssert.isPositive(price, BizError.PRICE_MUST_POSITIVE);

13. 正则匹配

java 复制代码
String phone = BizAssert.matches(input, "^1[3-9]\\d{9}$", "手机号格式不正确");

BizAssert.matches(email, "^[\\w.]+@[\\w.]+$", BizError.EMAIL_FORMAT_INVALID);

14. 字符串包含/前缀/后缀

java 复制代码
BizAssert.contains(url, "://", "URL 必须包含协议");

BizAssert.doesNotContain(password, username, "密码不能包含用户名");

BizAssert.startsWith(orderNo, "ORD-", "订单号格式不正确");

BizAssert.endsWith(filename, ".pdf", "仅接受 PDF 文件");

15. 无 null 元素

java 复制代码
List<String> names = BizAssert.noNullElements(nameList, "名称列表包含 null");

String[] ids = BizAssert.noNullElements(idArray, "ID 数组包含 null");

16. 状态断言

语义上与 isTrue 相同,但表达的是对象/系统状态校验的意图:

java 复制代码
BizAssert.state(connection.isOpen(), "连接已关闭");

BizAssert.state(order.getStatus() == PENDING, BizError.ORDER_STATE_INVALID);

BizAssert.stateOrThrow(!engine.isShutdown(),
    () -> new EngineException("引擎已关闭"));

17. Fail(不可达分支)

fail 始终抛出异常,声明返回类型为 T,因此可在表达式和 switch 分支中使用:

java 复制代码
// 在 switch 中
switch (status) {
    case ACTIVE:  return handleActive();
    case CLOSED:  return handleClosed();
    default:      return BizAssert.fail("未知状态:" + status);
}

// 带错误码
return BizAssert.fail(50001, "内部处理错误");

// 带错误枚举
return BizAssert.fail(BizError.UNKNOWN_TYPE);

// 带自定义工厂
return BizAssert.fail("致命错误", ExceptionFactory.ofMessage(FatalException::new));

18. 错误码枚举

通过实现 IErrorCode 接口定义业务错误码:

java 复制代码
public enum BizError implements IErrorCode {

    USER_NOT_NULL(10001, "user must not be null"),
    USER_IS_NULL(10002, "{} is null"),
    PARAM_INVALID(10003, "{} is invalid"),
    ORDER_NOT_PAID(20001, "订单 {} 未支付"),
    ;

    private final int code;
    private final String message;

    BizError(int code, String message) {
        this.code = code;
        this.message = message;
    }

    @Override
    public int getCode() { return code; }

    @Override
    public String getMessage() { return message; }
}

使用方式:

java 复制代码
// 直接使用
BizAssert.notNull(user, BizError.USER_NOT_NULL);
// → code=10001, message="user must not be null"

// 带占位符参数
BizAssert.isTrue(order.isPaid(), BizError.ORDER_NOT_PAID, order.getId());
// → code=20001, message="订单 ORD-12345 未支付"

19. 完整方法速查表

类别 方法 返回值 说明
布尔 isTrue(...) void 断言表达式为 true
isTrueOrThrow(...) void 断言为 true,抛自定义异常
isFalse(...) void 断言表达式为 false
isFalseOrThrow(...) void 断言为 false,抛自定义异常
空值 notNull(...) T 断言非 null,返回原值
notNullAs(obj, label) T Label 简写
notNullOrThrow(...) T 断言非 null,抛自定义异常
isNull(...) void 断言为 null
字符串 notEmpty(String, ...) String 断言非 null 且非空
notEmptyAs(str, label) String Label 简写
notEmptyOrThrow(...) String 抛自定义异常
notBlank(...) String 断言非 null/空/空白
notBlankAs(str, label) String Label 简写
notBlankOrThrow(...) String 抛自定义异常
集合 notEmpty(Collection, ...) T 断言非 null 且非空
notEmptyAs(col, label) T Label 简写
notEmptyOrThrow(...) T 抛自定义异常
Map notEmpty(Map, ...) T 断言非 null 且非空
notEmptyAs(map, label) T Label 简写
notEmptyOrThrow(...) T 抛自定义异常
数组 notEmpty(T[], ...) T[] 断言非 null 且非空
notEmptyAs(arr, label) T[] Label 简写
notEmptyOrThrow(...) T[] 抛自定义异常
相等 isEqual(actual, expected, ...) void 断言相等(null 安全)
notEqual(actual, unexpected, ...) void 断言不相等
数值 isPositive(int/long, ...) int/long 断言 > 0
isPositiveAs(val, label) int/long Label 简写
isNonNegative(int/long, ...) int/long 断言 >= 0
isNonNegativeAs(val, label) int/long Label 简写
isBetween(val, min, max, ...) int/long 断言在 [min, max] 内
isBetweenAs(val, min, max, label) int/long Label 简写
正则 matches(str, regex, ...) String 断言匹配正则
字符串操作 contains(str, sub, ...) String 断言包含子串
doesNotContain(str, sub, ...) String 断言不包含子串
startsWith(str, prefix, ...) String 断言以前缀开头
endsWith(str, suffix, ...) String 断言以后缀结尾
元素 noNullElements(Collection/T[], ...) 原类型 断言无 null 元素
状态 state(...) void 断言状态条件
stateOrThrow(...) void 抛自定义异常
失败 fail(...) T(永不返回) 直接抛异常,用于不可达代码

🏗️ 架构概览

vbnet 复制代码
┌─────────────────────────────────────────────────────┐
│                    BizAssert                        │
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │ 全局 ExceptionFactory(AtomicReference)       │  │
│  │ 默认: BizException::new                       │  │
│  │ 仅允许配置一次                                 │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  消息风格                                           │
│  ├── 默认消息(无参)                               │
│  ├── String message                                 │
│  ├── int code + String message                      │
│  ├── String pattern + Object... args({} 占位符)  │
│  ├── Supplier<String>(延迟求值)                   │
│  ├── IErrorCode(错误枚举)                         │
│  ├── IErrorCode + Object... args                    │
│  └── Label 机制(xxxAs 方法)                       │
│                                                     │
│  异常风格                                           │
│  ├── 全局默认工厂                                   │
│  ├── 单次调用指定 ExceptionFactory                  │
│  └── xxxOrThrow(Supplier<RuntimeException>)         │
│                                                     │
│  返回值                                             │
│  ├── void: isTrue, isFalse, isEqual, state          │
│  └── T:    notNull, notEmpty, notBlank, 数值断言    │
└─────────────────────────────────────────────────────┘

📄 开源协议

Apache License 2.0


🤝 参与贡献

欢迎提交 Issue 和 Pull Request!

相关推荐
贫民窟的勇敢爷们2 小时前
SpringBoot整合AOP切面编程实战,实现日志统一记录+接口权限校验
java·spring boot·spring
candyTong2 小时前
Claude Code Agent Teams:多 Agent 协作的生命周期与实现机制
后端·架构
AC赳赳老秦3 小时前
供应链专员提效:OpenClaw自动跟踪物流信息、更新库存数据,异常自动提醒
java·大数据·服务器·数据库·人工智能·自动化·openclaw
迈巴赫车主3 小时前
Java基础:list、set、map一遍过
java·开发语言
灵犀学长4 小时前
基于 Spring ThreadPoolTaskScheduler + CronTrigger 实现的动态定时任务调度系统
java·数据库·spring
好家伙VCC5 小时前
【无标题】
java
小碗羊肉6 小时前
【JavaWeb | 第十一篇】文件上传(本地&阿里云OSS)
java·阿里云·servlet
吾疾唯君医6 小时前
Java SpringBoot集成积木报表实操记录
java·spring boot·spring·导出excel·积木报表·数据文件下载
Byron Loong6 小时前
【c++】为什么有了dll和.h,还需要包含lib
java·开发语言·c++
hexu_blog7 小时前
vue+java实现图片批量压缩
java·前端·vue.js