ValidX vs Google Guava Preconditions:验证 vs 断言

ValidX vs Google Guava Preconditions:验证 vs 断言

📋 目录


概述

在 Java 世界里,"先检查、再执行"几乎是每个方法的第一行。这件事有两个主流工具:

  • Google Guava Preconditionscom.google.common.base.Preconditions,提供 checkArgumentcheckNotNullcheckState 等静态方法,条件不满足立刻抛异常
  • ValidX :一个面向中国业务场景的开源验证库,既提供 @NotBlank 这类注解,也提供 ValidX.init() 链式 API,条件不满足收集错误,由业务决定如何处理。

两者都做"前置检查",但哲学截然不同------这正是本文标题"验证 vs 断言 "要讲清楚的事:Guava 是断言,ValidX 是验证。断言管的是"我的代码假设错了"(开发者违约),验证管的是"用户给的数据不合法"(输入不合格)。搞混它们,代码会变得要么过度防御、要么对用户输入毫无招架。


快速对比表

维度 Guava Preconditions ValidX 链式 API
定位 编程契约 / 前置条件断言 业务数据 / 用户输入验证
失败方式 抛异常(fail-fast) 收集错误,返回 List<String>
异常类型 IllegalArgumentException / NPE / IllegalStateException / IndexOutOfBoundsException 不抛(可由调用方自行 throw ValidationException
消息受众 开发者(调试定位) 最终用户(可直接展示)
消息模板 %s 占位符 8 种语言 + 字段标签
空值语义 checkNotNull:null 即失败 默认放行,notNull()/notEmpty() 按需收紧
批量校验 一次只能断一个条件 一条链校验多个字段,错误累积
可读性 连续 if + 多行 checkArgument 声明式链式调用
适用层 库内部 / 方法边界防御 Controller / Service 入口、批量导入
依赖 整个 Guava(含大量无关类) ValidX 单一依赖

一、核心哲学:断言 vs 验证

1.1 断言(Guava):假设错了,程序就别跑了

Preconditions.checkArgument 的隐含语义是:这个条件必须为真,如果为假,说明调用方违反了约定,即程序有 bug。所以它选择 fail-fast------立刻抛异常、立刻停止,把问题暴露在最靠近源头的地方。

java 复制代码
// 库的内部实现:假设调用方传了合法参数
public BigDecimal calculateInterest(BigDecimal principal, double rate) {
    checkArgument(principal != null && principal.signum() >= 0, "本金不能为负: %s", principal);
    checkArgument(rate >= 0 && rate <= 1, "利率必须在 [0,1] 区间: %s", rate);
    // ... 从这里开始,假设前提都成立
}

"断言失败" ≈ "我的代码有 bug"------这是开发者之间的契约。

1.2 验证(ValidX):数据不对,提示用户改

ValidX 的隐含语义是:这个输入来自外部(用户、上游系统、文件导入),它不合法是常态,需要被当成业务事件处理------要么返回错误提示让用户重填,要么记录日志、跳过这一行继续处理。

java 复制代码
// 业务入口:验证用户提交的数据
ValidX v = ValidX.init()
    .field("手机号").notEmpty().isChinesePhone(phone)
    .field("邮箱").notEmpty().isEmail(email)
    .field("QQ(可选)").allowNull().isQQ(qq);

if (!v.passed()) {
    // 错误累积成列表,交给 Controller 返回给前端
    throw new ValidationException(v.getErrors());
}

"验证失败" ≈ "用户输入不合格"------这是应用与用户之间的契约。

1.3 一张图看懂区别

断言(Guava) 验证(ValidX)
问的问题 "我的代码假设错了吗?" "用户给的数据合法吗?"
失败的含义 程序 bug 业务事件
应该怎么处理 崩溃、尽早暴露 提示、重试、降级、跳过
消息写给谁 开发者 最终用户
典型场景 库参数防御、状态机校验 表单提交、API 入参、Excel 导入

一句话记忆:断言失败是你的问题,验证失败是用户的问题。


二、API 形态对比

2.1 Guava:四个静态方法打天下

java 复制代码
import static com.google.common.base.Preconditions.*;

// 条件必须为真,否则 IllegalArgumentException
checkArgument(user.getAge() >= 18, "年龄必须大于等于 18:%s", user.getAge());

// 引用不能为 null,否则 NullPointerException(返回原对象便于链式)
User u = checkNotNull(user, "user 不能为 null");

// 状态必须为真,否则 IllegalStateException
checkState(connection.isOpen(), "连接未打开");

// 下标必须在 [0, size) 内,否则 IndexOutOfBoundsException
checkElementIndex(index, list.size(), "index");

2.2 ValidX:一条链搞定多个字段

java 复制代码
import io.github.vipxieliang.validx.chain.ValidX;

ValidX v = ValidX.init()
    .field("手机号").notEmpty().isChinesePhone(phone)
    .field("身份证").notEmpty().isChineseIdCard(idCard)
    .field("金额").notNull().isIn(amount, new String[]{"USD", "CNY"})
    .field("QQ(可选)").allowNull().isQQ(qq);

if (!v.passed()) {
    System.out.println(v.getErrorMessage());  // "手机号: ..., 身份证: ..."
}

核心差异:Guava 是"一次一个断言",N 个条件要 N 行 + N 个异常分支;ValidX 是"一条链声明全部规则",错误自然累积,无需逐个 catch。


三、失败方式:异常中断 vs 错误收集

这是两者最直观的差异,直接影响代码控制流。

3.1 Guava:抛异常 = 中断执行

java 复制代码
public void transfer(Account from, Account to, BigDecimal amount) {
    checkArgument(amount != null && amount.signum() > 0, "转账金额必须为正: %s", amount);
    checkState(from.getBalance().compareTo(amount) >= 0, "余额不足");

    // 走到这里,所有前提都成立,不需要再判断
    from.debit(amount);
    to.credit(amount);
}

好处:后续代码无需任何 if ,因为前提已在入口断言;坏处:第一个失败即终止,拿不到全部问题

3.2 ValidX:收集错误 = 一次告诉你所有问题

java 复制代码
ValidX v = ValidX.init()
    .field("用户名").notEmpty().isAlphaNumber(username)
    .field("邮箱").notEmpty().isEmail(email)
    .field("密码").notEmpty().isPassword(password, 8);

if (!v.passed()) {
    // 三个字段的问题一次全拿到
    List<String> errors = v.getErrors();
    // ["用户名: 值不能为空", "邮箱: 邮箱格式不正确", "密码: 长度必须在8到20之间"]
}

好处:一次交互反馈全部问题 (对用户体验尤其重要------没人想改完邮箱又被告知密码也错了);坏处:需要显式判断 passed()

3.3 什么时候异常反而更好?

当"校验失败 = 程序 bug"时,异常(断言)就是正确的选择。一个典型反例是把用户输入校验写成 checkArgument------一旦抛错,Spring 默认返回 500 而不是 400,前端拿到的是"服务器内部错误"而不是"邮箱格式不对"。错误的数据流向错误的处理通道,是断言/验证混用的最大坑。


四、空值语义对比

场景 Guava ValidX
不允许 null checkNotNull(x, "msg") .notNull().isXxx(x) 或全局 GLOBAL_NOT_NULL
不允许 null 和空串 需自己再写 checkArgument(!x.isEmpty()) .notEmpty().isXxx(x)
允许 null(可选字段) if (x != null) checkArgument(...) .allowNull().isXxx(x)(默认行为)
java 复制代码
// Guava:可选字段要手动 if 包裹
if (qq != null) {
    checkArgument(qq.matches("^[1-9]\\d{4,10}$"), "QQ 格式不正确");
}

// ValidX:allowNull() 声明式表达"可选"
ValidX.init().field("QQ").allowNull().isQQ(qq);

关键认知:ValidX 链式校验默认允许 null/空 (因为 Map/JSON 中字段缺失返回 null 是常态),必须用 notNull()/notEmpty() 显式收紧;Guava 则是"默认严格",checkNotNull 是最高频方法。两者方向相反,迁移时最容易踩坑。


五、消息与国际化

Guava ValidX
模板 "余额不足: %s"%s 占位) 内置 8 种语言消息
字段名 手写进模板 .field("手机号") 自动拼进错误
语言切换 手动 withLocale(Locale.SIMPLIFIED_CHINESE) 一行切换
java 复制代码
// Guava:消息靠手写,面向调试
checkArgument(age >= 18, "age 不合法: %s", age);

// ValidX:字段标签 + 语言切换,面向用户
ValidX v = ValidX.init()
    .withLocale(Locale.ENGLISH)
    .field("Age").notEmpty().isAge(age, 0, 120);   // 错误消息自动为英文

对出海应用来说,ValidX 的"一套校验规则、多语言错误消息"能省掉大量硬编码文案。


六、性能与依赖

  • Guava Preconditions :本身是纯静态方法调用,开销极小;但为了用它引入整个 Guava(数 MB、几十个模块),对只想要"几个检查方法"的项目是杀鸡用牛刀。轻量替代:JDK 自带 Objects.requireNonNull + Objects.checkIndex(Java 9+)。
  • ValidX 链式 API:一次实例一个校验器,无反射(注解方式才涉及反射),链式场景开销可忽略;单一依赖、约 300KB,且内置 100+ 业务验证器(身份证、手机号、银行卡......)------这是 Guava 给不了的。

性能建议 :两者在"判断一个布尔值"上的开销都可忽略,真正的性能差异来自引入了多少你根本用不上的代码


七、实战对照:同一个注册接口

用同一个"用户注册"场景,看两种思路的完整落地:

7.1 Guava 写法:断言风格

java 复制代码
public void register(UserDTO dto) {
    // 每个条件一个断言,第一个失败即抛异常
    checkArgument(dto != null, "dto 不能为 null");
    checkArgument(dto.getUsername() != null && !dto.getUsername().isBlank(),
            "用户名不能为空");
    checkArgument(dto.getEmail() != null && EMAIL_PATTERN.matcher(dto.getEmail()).matches(),
            "邮箱格式不正确: %s", dto.getEmail());
    checkArgument(dto.getPhone() == null || PHONE_PATTERN.matcher(dto.getPhone()).matches(),
            "手机号格式不正确: %s", dto.getPhone());

    userService.create(dto);
    // 问题:用户改完用户名,又被告知邮箱也错了(一次只能报一个错)
}

7.2 ValidX 写法:验证风格

java 复制代码
public void register(UserDTO dto) {
    ValidX v = ValidX.init()
            .field("用户名").notEmpty().isAlphaNumber(dto.getUsername())
            .field("邮箱").notEmpty().isEmail(dto.getEmail())
            .field("手机号").allowNull().isChinesePhone(dto.getPhone());  // 可选字段

    if (!v.passed()) {
        // 一次返回全部错误,直接拼进响应体
        throw new ValidationException(v.getErrors());
    }

    userService.create(dto);
}

直观差异

  • Guava 版每多一个字段就要多写一行正则 + 一个异常;ValidX 版是字段标签 + 现成验证器;
  • 手机号/身份证这类中国业务格式 ,Guava 没有内置,必须手写正则;ValidX 直接 isChinesePhone/isChineseIdCard
  • Guava 一次只能报一个错,ValidX 一次报全部。

八、协作:什么时候用谁

正确的姿势不是二选一,而是各司其职

层级 用谁 原因
库 / 工具类内部参数防御 Guava checkArgument / checkNotNull 调用方是开发者,违约即 bug,应 fail-fast
状态机 / 不可逆操作前置 Guava checkState "状态不对"说明程序流转出错,是 bug
Controller / Service 入参 ValidX 链式 / 注解 输入来自用户,需要友好、聚合的错误提示
批量导入(Excel/CSV) ValidX 链式 需要"这行有问题跳过,继续处理下一行"

混合使用示例:业务入口用 ValidX 验证用户输入,通过后进入内部方法用 Guava 断言内部契约。

java 复制代码
public void submitOrder(OrderRequest req) {
    // 第一层:用户输入验证(ValidX,错误返回给前端)
    ValidX v = ValidX.init()
            .field("商品ID").notEmpty().isAlphaNumber(req.getProductId())
            .field("数量").notNull().isIn(req.getQuantity(), new String[]{"1", "2", "3", "4", "5"});
    if (!v.passed()) {
        throw new ValidationException(v.getErrors());
    }

    // 第二层:内部契约断言(Guava,违约即 bug)
    checkState(inventory.isLocked(req.getProductId()), "库存未锁定,流程状态异常");
    checkArgument(orderService.calcPrice(req).signum() > 0, "计算出的价格非法");

    orderService.submit(req);
}

使用场景建议

你的场景 推荐 一句话理由
写公共库 / SDK,防御非法参数 Guava checkArgument 调用方违约即 bug,fail-fast 最合适
Controller 接收用户表单/JSON ValidX 链式或注解 错误要聚合、要友好、要可多语言
校验中国手机号/身份证/银行卡 ValidX 内置验证器,不用手写正则
Excel 批量导入,行级容错 ValidX 链式 收集全部问题行,跳过继续
只需要"非空 + 不越界" JDK Objects.requireNonNull 零依赖即可,不必引入任何库
状态机 / 操作前置条件 Guava checkState "状态不对"是程序 bug 信号
框架内部 DTO 自动校验 ValidX 注解(@NotBlank 等) 与 Bean Validation 无缝集成

总结

  • Guava Preconditions 是断言:面向开发者契约,失败即抛异常(fail-fast),消息是给调试看的。它的价值在于把"前提条件"压缩到方法入口,让后续代码免于判断。
  • ValidX 是验证:面向用户输入,失败收集错误(可聚合、可多语言、可带字段标签),消息是给用户看的。它的价值在于用声明式链式 API 覆盖中国业务场景的海量格式规则。
  • 别混用 :把用户输入校验写成 checkArgument,会把"输入不合法"误报成"服务器错误";把内部契约写成业务验证,又会让 bug 被"友好提示"掩盖。
  • 最佳实践是分层协作 :入口用 ValidX 验证数据,内部用 Guava 断言契约------验证管用户,断言管自己

关联阅读:本系列对比文章《ValidX vs Apache Commons Validator:功能与性能对比》《ValidX vs Hutool Validator:专业框架 vs 工具库》,帮你把 ValidX 放进整个 Java 验证生态中选型。

相关推荐
吴声子夜歌1 小时前
ApacheCommons——commons-configuration2(多数据源配置统一管理与热加载)
java·开发语言·算法·apache
星星落进兜里1 小时前
Java虚拟机面试题-补充
java·开发语言
吴声子夜歌4 小时前
ApacheCommons——commons-pool2(高性能通用对象池框架)
java·开发语言·apache
mldong8 小时前
C# 开发者也有自己的轻量工作流引擎了:NuGet 装包,5 分钟跑通一条审批流
后端·c#·.net
xcl092510 小时前
幼儿托育系统开发实战:从需求分析到上线全流程指南
java·大数据·需求分析
吴声子夜歌10 小时前
ApacheCommons——commons-cli(命令行参数解析)
java·开发语言·apache
小小猪的春天10 小时前
Java 手写第一个 MCP Server:Spring AI MCP 半小时跑通
java·人工智能·spring boot·ai编程
find1star10 小时前
LeetCode 141:环形链表
java·算法·leetcode·链表
爱学堂IT分享10 小时前
鹤翔老师pmp项目管理培训认证考试课 (直播录播题库送学时证明)【共58课时】-51CTO学堂
后端