Java常用类库39:从业务约束形成可退出的依赖决策
商品目录需要按商户分组、拒绝商户内重复商品、保留输入顺序,并让查询方无法修改发布后的容器。JDK的有序映射、列表复制与不可修改包装已经能够实现这些要求。加入Guava可以改变实现写法,却不会自动产生一个此前无法表达的业务能力。类库决策应先说明缺少什么,再说明额外依赖补上了什么。
本系列采用同一类商品数据,逐步检查空值、相等、集合视图、缓存、文本协议与资源生命周期。几种常见误用会跨越包名重复出现:默认值掩盖坏输入、惰性视图与快照混淆、完成结果被当成持久状态、解析成功被当成安全边界。类库提供的机制各不相同,调用方仍要定义输入预算、错误分类和可观察结果。
本篇形成一份适用于这份示例工程的决策记录。实验工程汇集多个候选方案以便对照,其POM不应直接成为生产服务的依赖清单。每个候选只有在承担明确业务责任、通过对应回归并满足运行基线时,才进入应用配置;没有使用理由的候选可以移除。
保留标准库的商品目录
第00篇的 Product 保存非空、非空串的商户和商品编号,以及不需要舍入的两位非负金额。它没有trim、没有SKU正则,也没有定义实体相等和哈希。Catalog 按商户建立有序索引,对商户内相同商品编号拒绝导入,完成后复制并包装返回的容器。跨商户相同SKU合法,键身份应包括商户维度。
这些边界不能被工具名称替代。StringUtils.isBlank 会加入空白分类,严格SKU语法会排除更多编号,反射相等Builder会纳入字段,大小写转换又可能合并原本不同的标识符。它们都需要单独的业务理由。商品目录只拒绝null和空串时,直接使用JDK检查有更小的语义范围;需要更严格格式时,应增加具名验证规则和输入回归。
不可修改容器与深度不可变也需要分别判断。目录返回的映射与列表不能通过正常修改API写入,输入列表随后清空不会影响目录。列表中的Product仍是相同对象;此处Product字段final、金额和字符串也不提供原地修改接口,所以浅复制符合当前发布要求。换成包含可变库存对象的实体后,原来的容器包装不再足以隔离元素状态。
以下完整运行类复用原始实现,检查商户作用域、重复拒绝与容器快照,不需要额外类库:
1 | |
该类实际在Zulu8u472与Corretto21.0.11执行,输出相同。它证明当前三条目录规则,没有证明SKU语法、线程发布协议或对象深复制。完整 决策基线源码、复跑说明、全系列实验工程 与 下载包校验值 可以与各篇反例一起核查。
每项额外依赖对应一项责任
对当前商品流程,可以按操作判断依赖,而不是按项目知名度排列候选。下面是示例的选择范围,版本均定位在系列冻结表;表中的保留条件属于本项目约束,不是通用排名。
| 操作与规则 | 当前选择 | 保留理由与退出条件 |
|---|---|---|
| 商户内唯一、遇见顺序与容器快照 | JDK集合 | 规则已有短小实现;复杂多值操作才另外考虑专用结构 |
| 多值映射、频次与区间模型 | Guava | 多个调用需要同一结构协议时保留;单次局部分组可用JDK |
| CSV引号、分隔符与多行字段 | Commons CSV | 避免自行拼接协议;只处理真正无转义的受限文本时重新评估 |
| 明确类型与预算的JSON边界 | Jackson | 需要树、流或类型映射;Gson可在同一契约回归通过后作为替代 |
| 借用流复制与目录操作 | JDK加有界辅助代码,按需Commons IO | 简便API不能省去预算与所有权;没有实际API需求就移除工具依赖 |
| ZIP/TAR输入解析 | Commons Compress加私有解包边界 | 多格式支持由类库承担,路径与资源策略仍属应用;只生成固定ZIP可用JDK |
| 可复用HTTP连接与请求生命周期 | HttpClient,另对照JDK HttpClient | 由运行版本、连接管理和异常要求选择,不同时装两个生产客户端 |
| 容量与刷新缓存 | Java8示例Guava,现代对照Caffeine | 迁移先确认缺失、刷新失效与取消;无需跨请求重用时删除缓存 |
| 日志协议 | SLF4J加一个provider | 应用确定唯一provider;复现多provider的测试配置不能照搬 |
这里的“退出”应有可观察条件。缓存只有一次查询时,加载器和失效策略增加了维护成本,没有可重用结果;若缓存连续访问收益抵不过旧值风险,可以删除。多值结构若只服务一个小循环,JDK局部集合足以表达规则;但若十处代码都重复维护计数与总数,就应重新评估Multiset这样的具名协议。
退出并不要求先包一个只有单实现的接口。业务边界已经清楚时,可以把库类型限制在导入、后端调用或索引实现内部,对外返回项目已有的结果类型。真正出现第二个实现或独立替换需求时,再引入适合的接口。过早把每个静态工具包装一层,会增加文件而没有减少实际迁移责任。
错误规则比方法对应表更稳定
类库替换的回归单位应是失败分类。商品不存在、编号非法、导入重复、后端暂时失败与预算耗尽分别有不同处理。把它们全部变成empty或false,会让简洁的调用代码持续发布错误数据。异常类型变化还可能绕过原有catch分支,固定cause的比较需要和调用方的处理策略一起检查。
缓存迁移已经给出两个独立状态:刷新future可以返回迟到的新值,而映射仍处于失效状态。对应回归应同时检查观察结果与缓存内容,不能只断言 join 返回了新值。异步取消也要分为共享future是否取消、工作线程是否中断、HTTP请求是否停止和副作用是否已经发生;某一个状态改变不能证明后面三个状态。
文本协议的错误则应落在输入边界。缺失JSON字段与显式null、CSV不一致列数与空字段、Base64严格模式与忽略非字母字符,都不能仅凭API命名推断。本系列的实际对照保留了默认差异;生产配置可以选择其中一种,但配置之后需要同一份样本验证接受集合和拒绝集合。
反例也要能持续执行。预期的 NoSuchMethodError 在独立fork中断言,故意放入的重复商品由正常测试断言拒绝;主回归仍应成功。真正的突变测试先产生独立红条,再恢复正确源码确认全绿。长期保留一份无法区分预期与偶发错误的失败脚本,会降低后续升级时证据的可信度。
预算与提交边界决定失败是否可控
导入路径至少需要限制输入字节、记录数、单字段或结构深度、后端响应字节和导出总量。一个按记录迭代的CSV解析器仍可能被非常大的单字段占用内存;readTree 即使限制了字节,也仍需考虑嵌套深度。预算要靠实际读取字节和已处理记录计数,不能只接受网络头或归档头中声明的大小。
解包进一步需要限定路径、文件类型和解压后大小。字符串规范化可以拒绝 ..,却不能消除文件系统中的符号链接及并发替换。示例只在可信父目录下创建私有暂存目录,不允许恶意并发写入;这个前提必须写进部署约束。完成后对暂存产物统一交付,失败则清理暂存区,不能把已产生一半的文件暴露给下游。
时间限制同样要注明检查位置。循环内读数检查能限制检查点之间持续工作的总量,不能保证一个已经阻塞的read或解析调用会在截止时刻返回。HTTP响应超时、连接池获取超时与连接建立超时又各自作用于不同阶段;不能把拒绝连接当成连接超时的证据。任务取消也不能撤销远端已经写入的记录,真正写入业务仍需幂等或补偿协议。
隔离解包 与37综合案例把这些预算放进真实执行路径。选择类库只解决解析和传输的部分责任;何时发布结果、清理失败如何保留原始异常、调用者是否拥有流,仍需要项目代码明确执行。版本升级的三层回归
依赖升级首先检查解析和二进制链接:实际加载的是哪个jar、编译依赖和运行依赖是否一致、provider或处理器是否被错误引入。第38篇的新源码调用 buildKeepingLast 在旧Guava运行时产生 NoSuchMethodError,说明源码编译成功只证明编译时类路径存在该方法。运行jar的CodeSource与字节码方法签名能定位这个差异。
第二层检查行为契约。相同的输入、输出所有权、异常分类与边界时序应保持项目所需结果。源码兼容和二进制兼容不能保证新的默认值、字符分类或刷新规则完全相同。Java8与21的字符差异也提醒运行时本身是契约条件,不能只记录依赖坐标。
第三层才是资源与性能。JMH实验比较完整等价工作,并保存两个fork与原始各迭代数据。平均耗时、分配率与布局估计是不同指标;缓存未命中区间重叠也不形成稳定排名。即使小基准更快,仍要确认生产工作集、并发、后端等待和旧值预算,不能用一次单线程结果覆盖完整迁移回归。
安全修复另有明确的公告、受影响版本和触发面,不能从一个ImmutableMap链接实验推断所有IO路径安全。版本冻结方便复现;公告变化后需要重新核查采用版本和实际调用面。引入旧jar复现二进制问题时,旧jar应只进入测试fork,不成为正常运行依赖。
回归工程与未覆盖范围
全系列工程分为Java8兼容主实验、modern、processors 与 benchmarks 四个独立入口。主实验在Java8和21运行;现代缓存、Resilience4j、Mockito与HTTP对照采用更高目标且在21执行;处理器模块实际在两套JDK生成源码和检查字节码;JMH采用固定21环境。模块隔离使较高要求的依赖不会被误认为可在Java8运行。
原始证据按章放在 evidence/,记录官方文档、固定源码、实际实验及 NOT_RUN。最终整合记录与正文页面、附件检查分别保存:Maven成功证明对应测试运行,Hexo生成证明正文和标签可构建,本机HTTP字节核验确认交付文件可读取。这些检查没有覆盖所有浏览器的视觉效果,也没有执行生产服务部署。
本次没有在JDK11、17、25或Windows/Linux运行,没有模拟恶意并发目录写入,没有证明远端幂等,也没有测量大规模共享缓存的尾延迟。这些是适用范围,不是计划中尚未写完的章节;遇到对应上线条件时,应建立新的实验,而不是从已有绿色结果推断通过。
| 可迁移做法 | 项目中的交付形式 |
|---|---|
| 为依赖记录具体收益和退出条件 | 关联业务操作、回归样本、运行基线与删除条件,避免直接复制实验POM |
| 分开验证链接、行为和资源成本 | 固定jar与源码,运行失败反例,再解释性能与预算证据 |
商品目录的最终规则仍然落在领域数据和输入输出边界上。项目可以更换具体的JSON、缓存或集合实现,只要商户作用域、钱值、所有权与失败发布规则保持可验证;这些规则应比某个库的方法清单更长久。
