Java常用类库39:从业务约束形成可退出的依赖决策
商品目录需要按商户分组、拒绝商户内重复商品、保留输入顺序,并让查询方无法修改发布后的容器。JDK的有序映射、列表复制与不可修改包装已经能够实现这些要求。加入Guava可以改变实现写法,却不会自动产生一个此前无法表达的业务能力。类库决策应先说明缺少什么,再说明额外依赖补上了什么。 本系列采用同一类商品数据,逐步检查空值、相等、集合视图、缓存、文本协议与资源生命周期。几种常见误用会跨越包名重复出现:默认值掩盖坏输入、惰性视图与快照混淆、完成结果被当成持久状态、解析成功被当成安全边界。类库提供的机制各不相同,调用方仍要定义输入预算、错误分类和可观察结果。 本篇形成一份适用于这份示例工程的决策记录。实验工程汇集多个候选方案以便对照,其POM不应直接成为生产服务的依赖清单。每个候选只有在承担明确业务责任、通过对应回归并满足运行基线时,才进入应用配置;没有使用理由的候选可以移除。 保留标准库的商品目录 第00篇的 Product 保存非空、非空串的商户和商品编号,以及不需要舍入的两位非负金额。它没有trim、没有SKU正则,也没有定义实体相等和哈希。Catalog 按商户建立有序索引,对商...
Java常用类库-38-依赖升级中的二进制链接与运行语义
同一份 class 文件,为什么得到两种结果 商品目录导入允许后来的记录覆盖同一商品编号的旧值。实现使用 ImmutableMap.Builder.buildKeepingLast,在开发环境编译和测试通过。部署包却携带更早的 Guava,第一次执行导入时出现 NoSuchMethodError。此时重新检查 Java 源文件没有意义:运行进程执行的是已编译的 class 文件,方法是否存在由实际加载的类决定。 本篇用一份探针字节码复现这个边界。编译依赖固定 Guava 33.5.0-jre;运行时分别只提供 33.5.0-jre 和 30.1.1-jre。正常项目的依赖不降级,旧 jar 仅进入独立子进程。两个进程都打印实际 CodeSource,并分别断言成功结果和链接错误。完整测试、复现命令附在文末资源中。 这组方向必须写清楚:新 API 编译出来的程序运行在旧库上失败,并不能推出“把旧库升级成新库必然不兼容”。源兼容、二进制兼容和运行语义是三个问题。前一篇测试方法论见 第 35 篇,本篇把断言移动到真实进程边界。 编译器接受的是哪一份契约 buildKeepingLast...
Java常用类库-37-有界商品目录导入与归档流水线
把边界放进同一条实际路径 商品目录导入不是把 CSV 转成几个对象就结束。输入可能超大或包含非法金额,详情服务可能在第二个商品上失败,取消也可能发生在报表已经写出以后。如果成功路径只展示数据流转,而失败路径依赖临时目录以后再清理,工程仍然没有完整的交付边界。 本章实现一个同步、单批次的 CatalogPipeline:有界读取严格 UTF-8 CSV,验证商品身份和价格,形成不可变查询索引,再通过 HttpClient 访问真实 localhost 服务取得 JSON 标签,最后写出 report.json 与 report.zip。调用成功返回拥有临时目录的 Export,关闭 Export 即删除结果;失败或取消则由流水线清理已创建的工作目录。 实验复用系列已有的 Product、Catalog、BoundedIo、SafePaths 与 PrivateArchive,不另建框架。固定 Guava 33.5.0-jre、Commons CSV 1.14.1、Jackson 2.20.1、HttpClient 5.6.4 和 Commons Compress 1.28.0;完整...
Java常用类库36:用JMH比较等价工作与分配成本
把 Lists.transform 的创建时间与遍历后得到完整列表的时间放在一起,通常会得到惰性转换非常快的结果。两段程序交付的东西却不同:前者返回尚未执行转换的视图,后者已经持有全部输出元素。商品报表如果必须序列化全部结果,前者的计算还会发生在后续遍历中。测量范围不同,数字再稳定也无法回答报表应采用哪种实现。 集合和缓存的比较需要先写出可观察结果,再确定计时边界。查找实验要返回同一个值;转换实验要形成元素和顺序相同的完整列表;缓存命中与未命中应分开。惰性求值的边界 与 缓存迁移契约 已说明,性能只能比较满足要求的候选方案,不能抵消语义变化。 本文固定 JMH 1.37,在 macOS aarch64、Corretto 21.0.11 执行九组真实基准。独立 benchmarks 模块以 release17 编译,依赖 Guava 33.5.0-jre、Caffeine 3.2.4、fastutil 8.5.16、Eclipse Collections 11.1.0。数字只描述本次小工作集、单线程、简单整数值负载,不构成生产环境的类库排名。 计时之前确定交付物 转换的输入有10...
Java常用类库-35-用JUnit与AssertJ验证调用方责任
测试通过之前,先确认它能够失败 目录服务承诺返回商品列表快照。正常实现调用 ImmutableList.copyOf,测试检查列表包含 A。后来实现被改成直接返回输入列表,测试仍然通过,因为它从未修改原集合,也没有观察快照在源变化之后的内容。 测试执行成功只证明已有断言满足,不证明承诺已经覆盖。这个例子缺少的不是更多测试框架,而是能够区分快照与别名的输入动作:返回之后修改源列表,再检查结果是否变化。 本篇固定 JUnit Jupiter 5.13.4、AssertJ 3.27.6、Mockito 5.20.0。基础测试使用真实 Guava 与 Commons,在 JDK 8、21 运行;外部边界测试只在 JDK 21 运行。运行说明包含正常与受控突变命令,突变失败日志单独保存。 真实资源契约的对照见 第 34 篇;本篇聚焦测试如何发现调用方自身的错误。 按契约组织输入,而不是按方法数量组织测试 基础矩阵覆盖 null、空串、普通空格和制表符。JUnit 的 NullAndEmptySource 与 ValueSource 提供四次参数化调用,每次都调用真实 StringUtils...
Java常用类库-34-HttpClient请求的超时与连接归还
第二个请求可能还没有连到服务端 目录同步客户端只允许一条连接。第一个请求收到响应头后,没有读取或关闭响应;第二个请求等待一段时间后抛超时。此时服务端可能完全没有见到第二个请求,因为它还在等待连接池提供可用连接。 把所有异常统一记录为“接口响应慢”,会把客户端资源占用与服务端处理时间混在一起。一次请求至少涉及连接租用、连接建立、响应等待、实体读取和资源归还。每段的等待条件不同,配置名称相近也不能合并为一个总超时。 本篇固定 Apache HttpClient 5.6.4,Java 8 API,在 Zulu 8.0.472 和 Corretto 21.0.11 运行。基础实验只访问本机 127.0.0.1,JDK 客户端对照只在 JDK 21 执行。运行说明记录命令与资源边界,不访问外部服务。 日志运行配置见 第 33 篇;本篇的连接实验关闭自动重试,以隔离不同失败阶段。 租用超时首先检查连接占用 实验连接池总数和每路由上限都设为一,连接租用超时为一百五十毫秒。第一条请求使用显式响应对象,响应实体尚未消费时,pool.getTotalStats().getLeased 返回一。此时发...
Java常用类库-33-SLF4J的Provider发现与日志调用边界
日志方法执行了,不代表日志已经输出 商品导入代码调用 logger.info,运行没有抛异常,控制台却只有“没有找到 provider”的诊断。另一个应用引入两套日志依赖后开始输出,但实际选择的实现与预期不同。这两种现象都发生在业务日志格式化之前,首先需要检查门面如何找到实现。 SLF4J API 提供调用接口,provider 负责连接到具体日志系统。把 API 放入依赖树只证明能够编译日志调用,不能证明运行时有可用输出实现。相反,公共类库如果强制带入一个 provider,可能与宿主应用自己的选择发生冲突。 本篇固定 SLF4J API、simple、jdk14 与 jul-to-slf4j 为 2.0.20。Java 8 基线,在 Zulu 8.0.472 和 Corretto 21.0.11 运行。完整探针每个场景启动独立 JVM,并保存 classpath、stdout 和 stderr;运行说明包含复跑方式。日志内容边界可参照 第 22 篇。 Provider 发现是运行时协议 LoggerFactory 固定源码先尝试显式指定的 provider,否则通过加载 Lo...
Java常用类库-32-Jackson映射中的缺失类型与输入预算
更新请求里,缺失和 null 是两个动作 商品更新接口接受名称和价格。请求没有 name 字段时,服务应保留旧名称;显式提交 name:null 时,可以约定清空名称。若直接绑定到一个带 String name 字段的普通对象,两种请求都会得到 name=null。映射成功,但用于选择更新动作的信息已经消失。 问题不在于 JSON 是否语法正确,而在于目标类型能否表达输入状态。文本中的字段存在性、数值表示、类型结构与对象引用,都需要在映射边界作出明确选择。Jackson 可以完成转换,却不能根据字段名称推断业务更新协议。 本篇固定 Jackson databind 与 jsr310 模块 2.20.1,使用 Java 8 API,在 Zulu 8.0.472 和 Corretto 21.0.11 运行。完整测试包含树、绑定、流式读取及失败路径,复跑说明记录依赖与命令。缺失状态的业务定义可参照 第 01 篇。 树模型保留存在性,普通字段可能合并状态 对空对象读取 name,JsonNode.get 返回 Java null;path 返回 MissingNode。对含 name:n...
Java常用类库31:从Guava迁移Caffeine的缓存契约
商品详情缓存从 Guava 换成 Caffeine,编译器通常能识别 get 的签名变化,却不会指出“商品不存在”的语义变化。Guava 加载器返回 null 会报错;Caffeine 的同步加载器可以返回 null,这个结果又不会被缓存。一个原本把异常记录为后端故障的服务,迁移后可能返回空结果,并在下次请求时再次访问后端。缓存命中率与调用方错误分类都会改变。 迁移应围绕同一份商品查询约束展开:商户和商品编号共同构成键,找不到商品与请求失败分开表达,允许多长时间的旧价格必须明确,失效之后是否接受此前开始的刷新结果也要有答案。缓存库不能替应用决定这些约束。Guava加载刷新与失效 已建立的状态时间线可以继续作为对照输入。 本文固定 Guava 33.5.0-jre 与 Caffeine 3.2.4。Caffeine 3.x 的最低运行要求是 Java 11,示例放在独立 modern 工程,以 release17 编译,实际在 Corretto 21.0.11 执行。这个实验没有在 Java 11 或 17 运行,也不适用于 Java 8 运行时;Java 8 项目应保留现有方案,...
Java常用类库-30-ZIP与TAR的隔离解包和资源预算
商品归档导入需要比“能解开”更窄的契约 商品批量导入允许上传一个 ZIP 或 TAR,里面放 CSV 与图片。归档的条目名称、条目类型和解压后大小均来自输入,应用不能因为解析库识别了格式,就直接把这些字段用于文件系统操作。一个格式合法的归档,也可以包含父路径、链接、重复目标或大量展开数据。 本篇固定 Commons Compress 1.28.0,提交 852d9c23b94127feafc1649d9c7f13d4df338845。实现只处理 ZIP 与未压缩 TAR,不推断或自动递归展开其他格式。输入先成为可信目录中的稳定本地文件;解包时在可信父目录下创建新的私有工作目录,成功才把该目录返回调用方,失败尝试清理整个工作目录。 完整实验见 Chapter30Test.java,实现见 PrivateArchive.java、SafePaths.java 和 BoundedIo.java,执行方式见 RUN.md。代码使用 Java 8 API,并在 JDK 8、JDK 21 运行同一组场景。 工作目录不允许不可信并发写者,输入文件在检查后不能被外部进程替换。计时检查不能中断卡住...

