☰
Java 通用 I/O API 设计:用四个接口把数据搬移从零散代码重构为可伸缩、可扩展的抽象
2026/10/8 8:16:14 网站建设 项目流程
  • 文档
  • 教程
  • 知识库

【免费下载链接】translations

🐼 Chinese translations for classic software development resources

项目地址:https://gitcode.com/gh_mirrors/tr/translations
点击查看免费下载

generic-io-api-in-java-and-api-design/README.md是经典技术文章《A generic input/output API in Java》的中文译本,它完整展示了一次从"普通简单实现"逐步整理为"正交分解、可复用、可扩展、高性能、无错误"的 Java I/O API 设计全过程。读者读完本文,将掌握如何用Input/Output/Sender/Receiver四个接口把数据搬运(读取、转换、写出)拆解为可独立复用的构件,以及如何用filter、map等标准修饰器在不改动传输核心的前提下拦截和增强传输过程。

从痛点出发:数据搬移为什么这么难写

原作者的切入点是真实而普遍的:上周处理了大量数据搬移,有原始byte形式的、有String形式的,还有SPI和领域级对象形式的。把这些数据从一处搬到另一处,还要做到可伸缩、高性能、正确处理错误,难度相当高。于是作者产生了一个想法:一定存在一个通用模式来处理这些事,可以抽取出来放进库中——"从文本文件中读出文本行"这样的事应该只写一遍,然后在各个场景中复用。

先看一个"读一个文件、写入另一个文件"的典型场景,尝试从中拆出组成部分:

// 1. 客户代码:初始化传输,需要知道输入与输出的源 File source = new File(getClass().getResource("/iotest.txt").getFile()); File destination = File.createTempFile("test", ".txt"); destination.deleteOnExit(); // 2. 从输入中读的代码 BufferedReader reader = new BufferedReader(new FileReader(source)); long count = 0; try { // 4. 接收数据、写数据的代码 BufferedWriter writer = new BufferedWriter(new FileWriter(destination)); try { String line = null; while ((line = reader.readLine()) != null) { count++; // 3. 辅助代码:跟踪整个过程(计数) writer.append(line).append('\n'); } writer.close(); } catch (IOException e) { writer.close(); destination.delete(); } } finally { reader.close(); } System.out.println(count);

这段代码里混着 4 类不同职责:

  1. 客户代码:初始化传输,要知道输入和输出的源。
  2. 从输入中读的代码:负责从输入侧取数据。
  3. 辅助代码:用于跟踪整个过程(如计数),与具体的传输类型无关,最希望被复用。
  4. 接收数据、写数据的代码:负责接收并写出数据,可以改成批量读写(一次处理多行)。

问题在于这 4 类逻辑交织在一起:读的逻辑、写的逻辑、辅助逻辑(计数)、客户初始化代码全部耦合在一段try/catch/finally中,资源清理、错误处理(写失败时关闭 writer 并删除目标文件)和业务逻辑无法分离,也就谈不上复用与扩展。

四个接口:一次正交分解的 API 设计

一旦明确上述 4 个部分,剩下的事就清晰了:为每个部分整理成一个接口,并保证在各种场景下能方便使用。结果是发送方与接收方各两个接口,形成完全对称的正交结构。

Input:传输的发起者

public interface Input<T, SenderThrowableType extends Throwable> { <ReceiverThrowableType extends Throwable> void transferTo( Output<T,ReceiverThrowableType> output ) throws SenderThrowableType, ReceiverThrowableType; }

Input如同Iterables,可以被多次使用,用于初始化"一处到另一处"的传输。数据类型被泛化为类型参数T,因此可以是任何类型——byte[]、String、EntityState、MyDomainObject都可以。为了让发送者和接收者能各自抛出自己的异常,接口把双方的异常声明为类型参数:例如出错时Input抛出的可以是SQLException,而Output抛出的是IOException。异常是强类型的,且传输双方在出错时都必须知晓,这使双方能够做合适的恢复操作,关闭它们打开的资源。

Output:传输的接收端

public interface Output<T, ReceiverThrowableType extends Throwable> { <SenderThrowableType extends Throwable> void receiveFrom(Sender<T, SenderThrowableType> sender) throws ReceiverThrowableType, SenderThrowableType; }

当receiveFrom方法被Input调用时(由Input.transferTo触发),Output应当已经打开好它所需要的资源,然后等待数据从Sender发送过来。Input与Output必须持有相同的类型T,双方对传输内容达成一致;至于不一致的情况如何处理(映射),下文"拦截传输过程"一节会给出方案。

Sender:数据的推送者

public interface Sender<T, SenderThrowableType extends Throwable> { <ReceiverThrowableType extends Throwable> void sendTo(Receiver<T, ReceiverThrowableType> receiver) throws ReceiverThrowableType, SenderThrowableType; }

Output调用sendTo方法并传入一个Receiver,Sender借助这个Receiver逐条发送数据。Sender此时发起传输,把类型T的数据一次一个地传给Receiver。

Receiver:数据的接收者

public interface Receiver<T, ReceiverThrowableType extends Throwable> { void receive(T item) throws ReceiverThrowableType; }

当Receiver从Sender收到数据时,既可以立即写入底层资源,也可以分批写入(攒批)。Receiver知道传输何时结束——sendTo方法返回即代表结束——因此它可以在正确时机写掉剩余的分批数据、关闭持有的资源。

这个简单模式在发送方和接收方各有 2 个接口,并保持了以可伸缩、高性能和容错方式传输数据的潜能。它体现的设计要点值得细品:

  • 职责正交:发起(Input)、承接(Output)、推送(Sender)、接收(Receiver)四个角色各司其职,互不越界;
  • 双向异常传递:通过泛型化的SenderThrowableType/ReceiverThrowableType,异常在调用链两端保持强类型可见,任何一方出错都能让对方感知并做针对性恢复;
  • 类型通用:T泛化了数据形态,同一套 API 既能搬运byte[]也能搬运领域对象。

标准化 I/O:一行代码完成文件拷贝

上述 API 定义了数据发送与接收的契约,接下来可以制定若干输入输出的标准实现。以"从文本文件读出文本行、再写入文本文件"为例,这个操作可以封装进静态方法中方便复用。于是拷贝文本文件最终写成:

File source = ...; File destination = ...; Inputs.text(source).transferTo(Outputs.text(destination));

一行代码就处理完了读文件、写文件、资源清理以及其它零零碎碎的操作。transferTo方法会抛出IOException,需要向用户展示Error时可以catch这个异常;但实际处理这些Error的动作——关闭文件、删除没写成功的文件——Input、Output的实现已经代为处理好了。调用方再也不需要关心文件读写的细节。这正是把第 2、4 部分(读、写)连同资源生命周期一起收进标准实现后获得的可复用价值。

拦截传输过程:用标准修饰器扩展传输

基础 I/O 传输就绪后,实际场景中常常还需要附加行为:计数传输了多少数据、过滤掉一部分数据、每 1000 条做一次日志、或者观察当前正在进行的操作。既然输入输出已经分离,这些附加逻辑就变成了在输入输出的协调代码中简单地插入,而不是改写传输核心。大部分协调代码功能类似,可以放进标准工具方法中,方便复用。

过滤器:基于 Specification

第一个标准修饰器是过滤器,实现时用到了Specification(规格模式):

public static <T,ReceiverThrowableType extends Throwable> Output<T, ReceiverThrowableType> filter(final Specification<T> specification, final Output<T, ReceiverThrowableType> output) { // 创建这样一个 Output:根据 Specification<T> 过滤条目 }

Specification的定义是:

interface Specification<T> { boolean test(T item); }

有了这个简单部件,就能在传输时轻松过滤掉那些不希望出现在接收者端的数据。下面的例子删除文件中的空行:

File source = ...; File destination = ...; Inputs.text(source).transferTo( Transforms.filter(new Specification<String>() { public boolean test(String string) { return string.length() != 0; } }, Outputs.text(destination)));

filter返回的是一个Output,它内部持有原Output并按Specification决定放行或丢弃——装饰器模式的标准形态,外层接口签名与内层完全一致。

映射:处理数据类型不一致

第二个常见操作是把数据从一个类型映射到另一个类型,用于处理Input与Output数据类型不同的情况。下面把String映射成JSONObject:

public static <From,To,ReceiverThrowableType extends Throwable> Output<From, ReceiverThrowableType> map(final Function<From,To> function, final Output<To, ReceiverThrowableType> output)

Function的定义是:

interface Function<From, To> { To map(From from); }

通过它,可以把String的Input连接到JSONObject的Output:

Input<String,IOException> input = ...; Output<JSONObject,RuntimeException> output = ...; input.transferTo(Transforms.map(new String2JSON(), output));

其中String2JSON实现了Function接口,其map方法负责把String转换为JSONObject。注意这里Input与Output的类型参数可以不同——映射正是"处理两端类型不一致"的通用手段,这正是上一节所说的"不一致情况"的解法。

计数:把辅助逻辑做成通用映射

回到最初的"数据计数"需求——它可以实现成一个通用的映射:转换前后的类型不变,只是维护一个计数,在每次调用map方法时更新:

File source = ...; File destination = ...; Counter<String> counter = new Counter<String>(); Inputs.text(source).transferTo(Transforms.map(counter, Outputs.text(destination))); System.out.println("Nr of lines:" + counter.getCount());

对比开头的原始代码:计数逻辑从散落在大段try/catch中的count++,变成了一个可独立复用的Counter修饰器,任何传输场景都可以直接挂接。过滤、映射、计数三者都遵循同一模式——包装Output并委托原实现——这就是"拦截传输过程"的统一心智模型。

从设计过程中提炼的方法论

这篇文章的价值不止于给出 API 本身,更在于完整展示了设计步骤与过程,让 API 设计"有些条理":

  • 先拆职责,再定接口:从一段真实的、耦合的搬运代码中识别出 4 个稳定部分(客户初始化、读取、辅助跟踪、接收写出),再为每部分抽象出接口;
  • 正交分解:4 个接口各自只解决一个问题,Input管发起、Output管承接、Sender管推送、Receiver管消费,组合起来覆盖完整传输链路;
  • 装饰器而非改写:过滤、映射、计数等附加能力通过包装Output实现,传输核心零改动即可获得新功能,天然支持无限叠加;
  • 资源与异常内聚:资源打开/关闭、失败清理(如删除未写成功的文件)收进标准实现,调用方获得"无错误"的体验——错误处理不再是调用方的负担。

设计偏向是艺术,赏心悦目的 API 设计在旁人看来多是妙手偶得;但若能给出方法以减少艺术工作中的艺术性工作量,则是更有价值的事——本文展示的正是一次"有章可循"的设计示范。

结论与延伸

软件开发中,把一个输入搬到另一个输出、中间还可能做转换,是非常常见的任务。通常的做法是用零散代码(scratch)拼凑,结果往往是代码错误和使用不当的模式。通过引入通用 I/O API,恰当封装与隔离之后,这个任务可以以可伸缩、高性能、无错误的方式完成,并且能在需要额外功能时通过修饰器扩展实现。作者指出,这篇文章仅仅勾勒了这种使用方式,API 与辅助类可以在Qi4j Core 1.3-SNAPSHOT中获得;理想状态是,整个 Qi4j 中任何使用 I/O 的地方一开始就按这种方式来。

关于Usage in the Qi4j SPI一节(讲述该 API 在 Qi4j SPI 中的具体应用),译者已注明略过。需要说明的背景是:Qi4j 后来更名为 polygene 并迁至 Apache 基金会托管,但本文所讲的四个接口设计思想与修饰器组合模式,与具体框架的生命周期无关,至今仍有直接的借鉴意义。

作为补充阅读路径,译者指出:原文只给出了设计的发展思路、关键接口与典型使用方式,没有给出实现细节,读起来可能比较费力——细致的分解后的设计往往比较抽象,不容易快速理解。为此译者另行实现了完整工程的 Demo 代码,并撰写了一篇关于该设计练习的简单分析文档,可作为将本文抽象接口落地为可运行代码的参考。译文本身收录于本仓库 generic-io-api-in-java-and-api-design/README.md,其中的全部接口定义、标准 I/O 用法与修饰器示例均可直接用于自己的设计推演。

-EOF-

  • 文档
  • 教程
  • 知识库

【免费下载链接】translations

🐼 Chinese translations for classic software development resources

项目地址:https://gitcode.com/gh_mirrors/tr/translations
点击查看免费下载

相关推荐

上一篇:VeighNa Elite Trader 市场深度交易模块实战指南:盘口监控、深度下单与多账户批量交易
下一篇:PixiJS v8 Container 场景图节点完全指南:分组、变换、排序与生命周期管理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询