Guava IO 进阶:Source、Sink 与 Streams

欢迎你来读这篇博客,这篇博客主要是关于Guava IO 进阶:Source、Sink 与 Streams
其中包括 CharSource、CharSink、ByteSource、ByteSink、CharStreams、ByteStreams 的核心思想、源码设计、模板方法模式、流复制底层实现,以及在 Java 后端文件处理中的实战用法。

序言

上一章我们讲了 Guava 的 Files 工具类。

Files 很适合做 IO 入门,因为它直接面向文件操作:

1
2
3
4
5
6
Files.write(...)
Files.copy(...)
Files.move(...)
Files.readLines(...)
Files.equal(...)
Files.getFileExtension(...)

这些 API 很直观。

但如果只停留在 Files,还不算真正理解 Guava IO 的设计思想。

Guava IO 真正值得学习的是它对 IO 的抽象:

1
2
Source:数据从哪里来
Sink:数据写到哪里去

再细分为:

1
2
3
4
5
ByteSource:字节输入来源
ByteSink:字节输出目标

CharSource:字符输入来源
CharSink:字符输出目标

有了这套抽象之后,文件不再是唯一的数据来源。

数据可以来自:

  • 文件;
  • 内存;
  • URL;
  • classpath 资源;
  • 网络输入流;
  • 字节数组;
  • 字符串;
  • 其他自定义来源。

输出目标也可以是:

  • 文件;
  • 内存;
  • 字节流;
  • 字符流;
  • 黑洞输出流;
  • 其他自定义目标。

这一章对应课程结构:

1
2
3
4
第 5 章:Guava IO 进阶:Source、Sink 与 Streams
5.1 CharSource 和 CharSink 源码剖析
5.2 ByteSource 和 ByteSink 源码剖析
5.3 CharStreams 和 ByteStreams 源码剖析

对应原课程:

1
2
3
11 Guava 之 CharSource 和 CharSink 源码剖析
12 Guava 之 ByteSource 和 ByteSink 源码剖析
13 Guava 之 CharStreams 和 ByteStreams 源码剖析

这篇文章的目标不是让你背 API,而是理解 Guava IO 背后的设计:

把 IO 操作从具体文件、具体流中抽象出来,用 Source 表示输入,用 Sink 表示输出,再用 Streams 工具类完成底层复制和读取。

这个思想在后端工程中非常重要。

因为业务代码最怕被各种 IO 细节淹没。

好的 IO 代码应该表达清楚:

1
2
3
4
5
6
从哪里读
写到哪里
用什么编码
是否按字符还是按字节
是否一次性读取
是否流式复制

正文

chapter 1:从 Files 到 Source / Sink

上一章我们使用过:

1
2
3
4
Files.asCharSource(file, StandardCharsets.UTF_8)
Files.asCharSink(file, StandardCharsets.UTF_8)
Files.asByteSource(file)
Files.asByteSink(file)

这些方法表面上属于 Files 工具类。

但它们返回的对象已经不是简单工具方法,而是 Guava IO 的核心抽象:

1
2
3
4
CharSource
CharSink
ByteSource
ByteSink

例如:

1
2
CharSource source = Files.asCharSource(file, StandardCharsets.UTF_8);
String content = source.read();

这里的重点不再是:

1
读某个 File

而是:

1
从一个 CharSource 读取字符内容

同理:

1
2
CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8);
sink.write("hello");

重点变成:

1
向一个 CharSink 写入字符内容

这就是抽象的力量。

文件只是 Source / Sink 的一种实现。

chapter 2:Source / Sink 思想

Guava IO 把 IO 抽象成四个基本概念:

抽象 含义 面向数据类型
ByteSource 字节输入来源 byte
ByteSink 字节输出目标 byte
CharSource 字符输入来源 char / String
CharSink 字符输出目标 char / String

可以这样理解:

1
2
3
4
Source = 我能从这里读
Sink = 我能往这里写
Byte = 二进制
Char = 文本字符

比如:

1
2
3
4
文件       -> 可以是 ByteSource,也可以是 CharSource
字符串 -> 可以是 CharSource
byte[] -> 可以是 ByteSource
文件输出 -> 可以是 ByteSink,也可以是 CharSink

这种抽象可以让业务代码摆脱具体 IO 类型。

例如一个方法只关心“读取文本”:

1
2
3
public String loadConfig(CharSource source) throws IOException {
return source.read();
}

它可以读取:

  • 文件;
  • 字符串;
  • classpath 资源;
  • URL 资源;
  • 测试中的内存数据。

这比写死:

1
public String loadConfig(File file)

更灵活。

chapter 3:为什么 Source / Sink 比直接使用 File 更灵活

如果方法参数是 File

1
2
public void importUsers(File file) {
}

那么调用方必须提供文件。

如果测试时不想创建真实文件,就不方便。

但如果参数是 CharSource

1
2
public void importUsers(CharSource source) {
}

测试时可以直接使用内存字符串:

1
CharSource source = CharSource.wrap("1,Mario\n2,Luigi");

生产中可以使用文件:

1
CharSource source = Files.asCharSource(file, StandardCharsets.UTF_8);

这样业务逻辑不关心数据来自哪里。

这就是 Source 抽象的价值。

同理,如果输出参数是 CharSink

1
2
public void exportUsers(CharSink sink) {
}

生产中可以写文件。

测试中可以写到内存或自定义 Sink。

这种设计比到处传 FileInputStreamOutputStream 更有表达力。

chapter 4:CharSource 是什么

CharSource 表示字符输入来源。

它的核心职责是:

打开一个 Reader,并基于这个 Reader 提供高级读取方法。

你可以把它理解为:

1
CharSource = 一个可重复打开的字符数据来源

常见创建方式:

1. 从文件创建

1
CharSource source = Files.asCharSource(file, StandardCharsets.UTF_8);

2. 从字符串创建

1
CharSource source = CharSource.wrap("hello guava");

3. 空字符来源

1
CharSource source = CharSource.empty();

CharSource 的常见方法包括:

1
2
3
4
5
6
7
8
openStream()
openBufferedStream()
read()
readFirstLine()
readLines()
copyTo(...)
isEmpty()
length()

不同版本 Guava 中部分方法可能有差异,但核心思想一致。

chapter 5:CharSource 基本用法

从字符串创建 CharSource

1
2
3
4
5
6
7
8
9
10
11
12
import com.google.common.io.CharSource;

public class CharSourceWrapDemo {

public static void main(String[] args) throws Exception {
CharSource source = CharSource.wrap("hello\nworld");

String content = source.read();

System.out.println(content);
}
}

输出:

1
2
hello
world

读取第一行:

1
String firstLine = source.readFirstLine();

读取所有行:

1
List<String> lines = source.readLines();

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.google.common.io.CharSource;

import java.util.List;

public class CharSourceReadLinesDemo {

public static void main(String[] args) throws Exception {
CharSource source = CharSource.wrap("Mario\nLuigi\nPeach");

List<String> lines = source.readLines();

System.out.println(lines);
}
}

输出:

1
[Mario, Luigi, Peach]

chapter 6:从文件创建 CharSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.io.CharSource;
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class FileCharSourceDemo {

public static void main(String[] args) throws Exception {
File file = new File("guava-io-demo/users.txt");

Files.createParentDirs(file);
Files.asCharSink(file, StandardCharsets.UTF_8)
.write("1,Mario\n2,Luigi\n3,Peach");

CharSource source = Files.asCharSource(file, StandardCharsets.UTF_8);

System.out.println(source.read());
}
}

这里:

1
Files.asCharSource(file, StandardCharsets.UTF_8)

把一个文件包装成字符来源。

读取时,CharSource 内部会打开 Reader。

使用者不需要手动管理 FileInputStreamInputStreamReaderBufferedReader 这些细节。

chapter 7:CharSource 的模板方法设计

CharSource 的源码设计很典型。

它通常会把最底层的打开流方法设计成抽象方法:

1
public abstract Reader openStream() throws IOException;

然后在父类中基于 openStream() 实现高级方法:

1
2
3
4
5
public String read() throws IOException {
// 打开 Reader
// 调用 CharStreams.toString(reader)
// 关闭 Reader
}
1
2
3
4
5
public List<String> readLines() throws IOException {
// 打开 Reader
// 按行读取
// 关闭 Reader
}

这就是典型的模板方法模式:

子类负责提供底层 Reader,父类负责固定读取流程。

例如:

  • 文件 CharSource 的 openStream() 打开文件 Reader;
  • 字符串 CharSource 的 openStream() 打开 StringReader;
  • 资源 CharSource 的 openStream() 打开资源 Reader。

父类不关心具体来源。

它只要能拿到 Reader,就能实现读取、按行读取、复制等高级能力。

chapter 8:CharSource 自定义实现

我们可以自己实现一个简单 CharSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.google.common.io.CharSource;

import java.io.Reader;
import java.io.StringReader;

public class CustomStringCharSource extends CharSource {

private final String content;

public CustomStringCharSource(String content) {
this.content = content;
}

@Override
public Reader openStream() {
return new StringReader(content);
}
}

使用:

1
2
3
4
5
6
7
8
public class CustomCharSourceDemo {

public static void main(String[] args) throws Exception {
CharSource source = new CustomStringCharSource("hello custom source");

System.out.println(source.read());
}
}

这个例子说明:

只要你实现了 openStream,CharSource 父类就能提供一系列通用读取能力。

这就是源码设计中模板方法的价值。

chapter 9:CharSource 复制到 Appendable

CharSource 可以把内容复制到 Appendable

例如 StringBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.google.common.io.CharSource;

public class CharSourceCopyToAppendableDemo {

public static void main(String[] args) throws Exception {
CharSource source = CharSource.wrap("hello char source");

StringBuilder builder = new StringBuilder();

source.copyTo(builder);

System.out.println(builder);
}
}

输出:

1
hello char source

这里 StringBuilder 实现了 Appendable

这体现了 Guava 设计里的“面向抽象”思想。

chapter 10:CharSource 复制到 CharSink

CharSource 可以复制到 CharSink

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.google.common.io.CharSink;
import com.google.common.io.CharSource;
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class CharSourceCopyToSinkDemo {

public static void main(String[] args) throws Exception {
CharSource source = CharSource.wrap("hello copy to sink");

File targetFile = new File("guava-io-demo/copy-char.txt");

Files.createParentDirs(targetFile);

CharSink sink = Files.asCharSink(targetFile, StandardCharsets.UTF_8);

source.copyTo(sink);

System.out.println("复制完成");
}
}

这段代码表达非常清楚:

1
从一个字符来源,复制到一个字符目标。

不需要关心 Reader / Writer 的细节。

chapter 11:CharSink 是什么

CharSink 表示字符输出目标。

它的核心职责是:

打开一个 Writer,并基于这个 Writer 提供高级写入方法。

你可以理解为:

1
CharSink = 一个可以写入字符数据的目的地

常见创建方式:

1
CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8);

常用方法:

1
2
3
4
5
openStream()
openBufferedStream()
write(CharSequence)
writeLines(Iterable)
writeFrom(Readable)

CharSinkCharSource 是一对。

  • CharSource 负责读;
  • CharSink 负责写。

chapter 12:CharSink 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.io.CharSink;
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class CharSinkBasicDemo {

public static void main(String[] args) throws Exception {
File file = new File("guava-io-demo/charsink-basic.txt");

Files.createParentDirs(file);

CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8);

sink.write("hello CharSink");

System.out.println("写入完成");
}
}

写多行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.google.common.io.CharSink;
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class CharSinkWriteLinesDemo {

public static void main(String[] args) throws Exception {
File file = new File("guava-io-demo/charsink-lines.txt");

Files.createParentDirs(file);

CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8);

sink.writeLines(List.of("Mario", "Luigi", "Peach"));

System.out.println("多行写入完成");
}
}

chapter 13:CharSink 追加写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.google.common.io.CharSink;
import com.google.common.io.FileWriteMode;
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class CharSinkAppendDemo {

public static void main(String[] args) throws Exception {
File file = new File("guava-io-demo/append.txt");

Files.createParentDirs(file);

CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8);

sink.write("line1\n");

CharSink appendSink = Files.asCharSink(
file,
StandardCharsets.UTF_8,
FileWriteMode.APPEND
);

appendSink.write("line2\n");

System.out.println("追加写入完成");
}
}

FileWriteMode.APPEND 表示追加。

如果不指定,默认会覆盖文件内容。

chapter 14:CharSink 的模板方法设计

CharSink 的源码结构也很典型。

底层抽象方法类似:

1
public abstract Writer openStream() throws IOException;

父类基于 openStream() 实现高级写入:

1
2
3
4
5
public void write(CharSequence charSequence) throws IOException {
// 打开 Writer
// 写入字符
// 关闭 Writer
}
1
2
3
4
5
public void writeLines(Iterable<? extends CharSequence> lines) throws IOException {
// 打开 Writer
// 循环写入每一行
// 关闭 Writer
}

这同样是模板方法思想:

子类负责打开 Writer,父类负责统一写入流程。

如果写入目标是文件,openStream() 打开文件 Writer。

如果写入目标是内存,openStream() 可以打开内存 Writer。

业务代码只依赖 CharSink,不依赖具体目标。

chapter 15:CharSource / CharSink 实战:文本转换器

需求:

  1. 从一个 CharSource 读取文本;
  2. 转成大写;
  3. 写入一个 CharSink

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.google.common.io.CharSink;
import com.google.common.io.CharSource;

import java.io.IOException;
import java.util.Locale;

public class TextUppercaseConverter {

public void convert(CharSource source, CharSink sink) throws IOException {
String content = source.read();

String upper = content.toUpperCase(Locale.ROOT);

sink.write(upper);
}
}

使用文件作为输入输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class TextUppercaseConverterDemo {

public static void main(String[] args) throws Exception {
File sourceFile = new File("guava-io-demo/input.txt");
File targetFile = new File("guava-io-demo/output.txt");

Files.createParentDirs(sourceFile);

Files.asCharSink(sourceFile, StandardCharsets.UTF_8)
.write("hello guava io");

TextUppercaseConverter converter = new TextUppercaseConverter();

converter.convert(
Files.asCharSource(sourceFile, StandardCharsets.UTF_8),
Files.asCharSink(targetFile, StandardCharsets.UTF_8)
);

System.out.println(Files.asCharSource(targetFile, StandardCharsets.UTF_8).read());
}
}

测试时不一定要用真实文件:

1
CharSource source = CharSource.wrap("hello test");

这就是 Source/Sink 抽象带来的测试便利。

chapter 16:ByteSource 是什么

ByteSource 表示字节输入来源。

它的核心职责是:

打开一个 InputStream,并基于这个 InputStream 提供高级读取方法。

常见创建方式:

1
2
ByteSource source = Files.asByteSource(file);
ByteSource source = ByteSource.wrap(byteArray);

常见方法:

1
2
3
4
5
6
7
8
9
10
openStream()
openBufferedStream()
read()
copyTo(...)
size()
isEmpty()
hash(...)
contentEquals(...)
slice(...)
asCharSource(charset)

ByteSource 用于二进制数据。

例如:

  • 图片;
  • PDF;
  • 压缩包;
  • 上传文件;
  • 加密数据;
  • 任意 byte 数组。

chapter 17:ByteSource 基本用法

从 byte 数组创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.google.common.io.ByteSource;

import java.nio.charset.StandardCharsets;

public class ByteSourceWrapDemo {

public static void main(String[] args) throws Exception {
byte[] data = "hello byte source".getBytes(StandardCharsets.UTF_8);

ByteSource source = ByteSource.wrap(data);

byte[] read = source.read();

System.out.println(new String(read, StandardCharsets.UTF_8));
}
}

从文件创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.google.common.io.ByteSource;
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class FileByteSourceDemo {

public static void main(String[] args) throws Exception {
File file = new File("guava-io-demo/bytesource.txt");

Files.createParentDirs(file);
Files.write("hello file byte source".getBytes(StandardCharsets.UTF_8), file);

ByteSource source = Files.asByteSource(file);

System.out.println(new String(source.read(), StandardCharsets.UTF_8));
}
}

chapter 18:ByteSource 转 CharSource

字节数据加上字符集,就可以成为字符数据。

1
CharSource charSource = byteSource.asCharSource(StandardCharsets.UTF_8);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;

import java.nio.charset.StandardCharsets;

public class ByteSourceAsCharSourceDemo {

public static void main(String[] args) throws Exception {
ByteSource byteSource = ByteSource.wrap("hello 中文".getBytes(StandardCharsets.UTF_8));

CharSource charSource = byteSource.asCharSource(StandardCharsets.UTF_8);

System.out.println(charSource.read());
}
}

这体现了 ByteSource 和 CharSource 的关系:

1
ByteSource + Charset = CharSource

也就是说:

  • ByteSource 是原始字节;
  • CharSource 是按某种编码解释后的字符。

chapter 19:ByteSource 计算哈希

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteSource;

import java.nio.charset.StandardCharsets;

public class ByteSourceHashDemo {

public static void main(String[] args) throws Exception {
ByteSource source = ByteSource.wrap("hello hash".getBytes(StandardCharsets.UTF_8));

HashCode hashCode = source.hash(Hashing.sha256());

System.out.println(hashCode);
}
}

这比直接面对 InputStream 更清晰。

业务上常用于:

  • 文件去重;
  • 上传文件校验;
  • 下载完整性校验;
  • 生成文件指纹。

chapter 20:ByteSource 内容比较

ByteSource 可以判断内容是否一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.google.common.io.ByteSource;

import java.nio.charset.StandardCharsets;

public class ByteSourceContentEqualsDemo {

public static void main(String[] args) throws Exception {
ByteSource source1 = ByteSource.wrap("hello".getBytes(StandardCharsets.UTF_8));
ByteSource source2 = ByteSource.wrap("hello".getBytes(StandardCharsets.UTF_8));
ByteSource source3 = ByteSource.wrap("world".getBytes(StandardCharsets.UTF_8));

System.out.println(source1.contentEquals(source2));
System.out.println(source1.contentEquals(source3));
}
}

输出:

1
2
true
false

如果来源是文件,也可以比较文件内容。

chapter 21:ByteSource 切片 slice

ByteSource 支持切片:

1
ByteSource slice = source.slice(offset, length);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.google.common.io.ByteSource;

import java.nio.charset.StandardCharsets;

public class ByteSourceSliceDemo {

public static void main(String[] args) throws Exception {
ByteSource source = ByteSource.wrap("hello guava".getBytes(StandardCharsets.UTF_8));

ByteSource slice = source.slice(6, 5);

System.out.println(new String(slice.read(), StandardCharsets.UTF_8));
}
}

输出:

1
guava

切片适合:

  • 读取文件头;
  • 读取魔数;
  • 分段处理;
  • 大文件部分读取;
  • 简单协议解析。

chapter 22:ByteSink 是什么

ByteSink 表示字节输出目标。

它的核心职责是:

打开一个 OutputStream,并基于这个 OutputStream 提供高级写入方法。

常见创建方式:

1
ByteSink sink = Files.asByteSink(file);

常见方法:

1
2
3
4
5
openStream()
openBufferedStream()
write(byte[])
writeFrom(InputStream)
asCharSink(charset)

适合写入二进制数据。

chapter 23:ByteSink 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.io.ByteSink;
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class ByteSinkBasicDemo {

public static void main(String[] args) throws Exception {
File file = new File("guava-io-demo/bytesink.txt");

Files.createParentDirs(file);

ByteSink sink = Files.asByteSink(file);

sink.write("hello byte sink".getBytes(StandardCharsets.UTF_8));

System.out.println("ByteSink 写入完成");
}
}

从输入流写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import com.google.common.io.ByteSink;
import com.google.common.io.Files;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.charset.StandardCharsets;

public class ByteSinkWriteFromDemo {

public static void main(String[] args) throws Exception {
File file = new File("guava-io-demo/write-from.txt");

Files.createParentDirs(file);

ByteSink sink = Files.asByteSink(file);

ByteArrayInputStream inputStream = new ByteArrayInputStream(
"hello writeFrom".getBytes(StandardCharsets.UTF_8)
);

sink.writeFrom(inputStream);

System.out.println("writeFrom 完成");
}
}

chapter 24:ByteSink 转 CharSink

字节输出目标加上字符集,可以变成字符输出目标。

1
CharSink charSink = byteSink.asCharSink(StandardCharsets.UTF_8);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.google.common.io.ByteSink;
import com.google.common.io.CharSink;
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class ByteSinkAsCharSinkDemo {

public static void main(String[] args) throws Exception {
File file = new File("guava-io-demo/byte-to-char-sink.txt");

Files.createParentDirs(file);

ByteSink byteSink = Files.asByteSink(file);

CharSink charSink = byteSink.asCharSink(StandardCharsets.UTF_8);

charSink.write("hello from char sink");

System.out.println("写入完成");
}
}

这体现了 ByteSink 和 CharSink 的关系:

1
ByteSink + Charset = CharSink

chapter 25:ByteSource / ByteSink 统一文件、内存、URL 资源

ByteSource 的价值在于统一输入来源。

1. 文件来源

1
ByteSource source = Files.asByteSource(file);

2. 内存来源

1
ByteSource source = ByteSource.wrap(bytes);

3. URL 或 classpath 资源

Guava 的 Resources 可以把资源抽象为 ByteSource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.google.common.io.ByteSource;
import com.google.common.io.Resources;

import java.net.URL;

public class ResourceByteSourceDemo {

public static void main(String[] args) throws Exception {
URL url = Resources.getResource("demo.txt");

ByteSource source = Resources.asByteSource(url);

System.out.println(source.size());
}
}

这样业务逻辑可以统一接收:

1
ByteSource source

而不用关心数据来自文件、内存还是资源路径。

chapter 26:ByteSource / ByteSink 实战:二进制复制器

1
2
3
4
5
6
7
8
9
10
11
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;

import java.io.IOException;

public class BinaryCopyService {

public long copy(ByteSource source, ByteSink sink) throws IOException {
return source.copyTo(sink);
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class BinaryCopyServiceDemo {

public static void main(String[] args) throws Exception {
File sourceFile = new File("guava-io-demo/source.bin");
File targetFile = new File("guava-io-demo/target.bin");

Files.createParentDirs(sourceFile);

Files.asByteSink(sourceFile)
.write("binary content".getBytes(StandardCharsets.UTF_8));

BinaryCopyService service = new BinaryCopyService();

long copied = service.copy(
Files.asByteSource(sourceFile),
Files.asByteSink(targetFile)
);

System.out.println("复制字节数:" + copied);
}
}

这个服务不依赖 File。

它只依赖:

1
2
ByteSource
ByteSink

因此更容易复用和测试。

chapter 27:CharSource / CharSink 与 ByteSource / ByteSink 的关系

可以这样理解:

1
2
3
4
5
ByteSource:读原始字节
ByteSink:写原始字节

CharSource:按字符集读文本
CharSink:按字符集写文本

二者的桥梁是:

1
Charset

关系如下:

1
2
ByteSource + Charset -> CharSource
ByteSink + Charset -> CharSink

所以:

  • 如果你处理的是文本,用 CharSource / CharSink;
  • 如果你处理的是二进制,用 ByteSource / ByteSink;
  • 如果你从字节转字符,必须明确 Charset;
  • 如果你从字符转字节,也必须明确 Charset。

不要依赖默认字符集。

chapter 28:CharStreams 是什么

CharStreams 是 Guava 提供的字符流工具类。

包名:

1
com.google.common.io.CharStreams

它主要围绕:

1
2
3
4
Reader
Writer
Readable
Appendable

提供工具方法。

常见方法包括:

1
2
3
4
5
6
copy(Readable from, Appendable to)
toString(Readable readable)
readLines(Readable readable)
skipFully(Reader reader, long n)
nullWriter()
asWriter(Appendable target)

CharSourceCharSink 的很多高级方法,底层会用到 CharStreams

可以理解为:

CharSource / CharSink 是高层抽象,CharStreams 是底层字符流工具。

chapter 29:CharStreams.copy

字符流复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.google.common.io.CharStreams;

import java.io.StringReader;
import java.io.StringWriter;

public class CharStreamsCopyDemo {

public static void main(String[] args) throws Exception {
StringReader reader = new StringReader("hello char streams");

StringWriter writer = new StringWriter();

long copied = CharStreams.copy(reader, writer);

System.out.println("复制字符数:" + copied);
System.out.println(writer.toString());
}
}

输出:

1
2
复制字符数:18
hello char streams

底层本质是:

1
2
3
4
创建 char buffer
循环 reader.read(buffer)
writer.append/write(buffer)
直到读到 -1

chapter 30:CharStreams.toString

Readable 读取成字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.google.common.io.CharStreams;

import java.io.StringReader;

public class CharStreamsToStringDemo {

public static void main(String[] args) throws Exception {
StringReader reader = new StringReader("hello toString");

String content = CharStreams.toString(reader);

System.out.println(content);
}
}

注意:

toString 会把全部内容读入内存。

它适合小文本。

不适合大文件。

chapter 31:CharStreams.readLines

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.google.common.io.CharStreams;

import java.io.StringReader;
import java.util.List;

public class CharStreamsReadLinesDemo {

public static void main(String[] args) throws Exception {
StringReader reader = new StringReader("a\nb\nc");

List<String> lines = CharStreams.readLines(reader);

System.out.println(lines);
}
}

输出:

1
[a, b, c]

如果要处理大文件,还是建议使用流式处理或 LineProcessor

chapter 32:CharStreams.nullWriter

nullWriter() 返回一个黑洞 Writer。

写进去的数据会被丢弃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.google.common.io.CharStreams;

import java.io.Writer;

public class NullWriterDemo {

public static void main(String[] args) throws Exception {
Writer writer = CharStreams.nullWriter();

writer.write("this content will be ignored");

writer.flush();

System.out.println("写入 nullWriter 完成");
}
}

常见用途:

  • 测试;
  • 统计写入流程耗时;
  • 丢弃不需要的输出;
  • 适配必须传 Writer 的接口。

它类似 Linux 里的:

1
/dev/null

chapter 33:ByteStreams 是什么

ByteStreams 是 Guava 提供的字节流工具类。

包名:

1
com.google.common.io.ByteStreams

它围绕:

1
2
3
4
InputStream
OutputStream
ReadableByteChannel
WritableByteChannel

提供常见工具方法。

常见方法包括:

1
2
3
4
5
6
7
copy(InputStream from, OutputStream to)
toByteArray(InputStream in)
limit(InputStream in, long limit)
skipFully(InputStream in, long n)
readFully(InputStream in, byte[] b)
exhaust(InputStream in)
nullOutputStream()

ByteSourceByteSink 的很多高级方法,底层会用到 ByteStreams

可以理解为:

ByteSource / ByteSink 是高层抽象,ByteStreams 是底层字节流工具。

chapter 34:ByteStreams.copy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.google.common.io.ByteStreams;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;

public class ByteStreamsCopyDemo {

public static void main(String[] args) throws Exception {
ByteArrayInputStream inputStream = new ByteArrayInputStream(
"hello byte streams".getBytes(StandardCharsets.UTF_8)
);

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

long copied = ByteStreams.copy(inputStream, outputStream);

System.out.println("复制字节数:" + copied);
System.out.println(outputStream.toString(StandardCharsets.UTF_8));
}
}

输出:

1
2
复制字节数:18
hello byte streams

底层本质是:

1
2
3
4
创建 byte buffer
循环 inputStream.read(buffer)
outputStream.write(buffer)
直到读到 -1

chapter 35:ByteStreams.toByteArray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.google.common.io.ByteStreams;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;

public class ByteStreamsToByteArrayDemo {

public static void main(String[] args) throws Exception {
ByteArrayInputStream inputStream = new ByteArrayInputStream(
"hello toByteArray".getBytes(StandardCharsets.UTF_8)
);

byte[] bytes = ByteStreams.toByteArray(inputStream);

System.out.println(new String(bytes, StandardCharsets.UTF_8));
}
}

注意:

toByteArray 会把全部字节读入内存。

适合小文件、小响应体。

不适合大文件。

大文件应该使用流式复制:

1
ByteStreams.copy(inputStream, outputStream)

chapter 36:ByteStreams.nullOutputStream

nullOutputStream() 返回一个黑洞 OutputStream。

写进去的数据会被丢弃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.google.common.io.ByteStreams;

import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

public class NullOutputStreamDemo {

public static void main(String[] args) throws Exception {
OutputStream outputStream = ByteStreams.nullOutputStream();

outputStream.write("ignored bytes".getBytes(StandardCharsets.UTF_8));

outputStream.flush();

System.out.println("写入 nullOutputStream 完成");
}
}

常见用途:

  • 丢弃输出;
  • 测试复制性能;
  • 只想消费输入流;
  • 适配必须传 OutputStream 的 API。

chapter 37:ByteStreams.limit

ByteStreams.limit 可以限制最多读取多少字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.io.ByteStreams;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

public class ByteStreamsLimitDemo {

public static void main(String[] args) throws Exception {
ByteArrayInputStream inputStream = new ByteArrayInputStream(
"hello guava limit".getBytes(StandardCharsets.UTF_8)
);

InputStream limited = ByteStreams.limit(inputStream, 5);

byte[] bytes = ByteStreams.toByteArray(limited);

System.out.println(new String(bytes, StandardCharsets.UTF_8));
}
}

输出:

1
hello

这种能力适合:

  • 限制读取大小;
  • 防止超大输入;
  • 读取文件头;
  • 简单安全保护;
  • 协议解析。

但不要把它当成完整的上传文件大小限制。

上传限制应该在网关、Servlet 容器、应用层一起做。

chapter 38:ByteStreams.exhaust

exhaust 用于把输入流读完并丢弃内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.google.common.io.ByteStreams;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;

public class ByteStreamsExhaustDemo {

public static void main(String[] args) throws Exception {
ByteArrayInputStream inputStream = new ByteArrayInputStream(
"some content".getBytes(StandardCharsets.UTF_8)
);

long count = ByteStreams.exhaust(inputStream);

System.out.println("消耗字节数:" + count);
}
}

适合:

  • 只想消费流;
  • 丢弃响应体;
  • 测试读取速度;
  • 清空剩余输入。

chapter 39:流复制的底层实现思想

无论是 CharStreams.copy 还是 ByteStreams.copy,底层思想都非常朴素:

1
2
3
4
1. 准备一个缓冲区
2. 从输入流读取一批数据
3. 写入输出流
4. 重复直到读取结束

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
byte[] buffer = new byte[BUFFER_SIZE];

long total = 0;

while (true) {
int read = input.read(buffer);

if (read == -1) {
break;
}

output.write(buffer, 0, read);

total += read;
}

字符流类似:

1
2
3
4
5
char[] buffer = new char[BUFFER_SIZE];

while ((read = reader.read(buffer)) != -1) {
writer.write(buffer, 0, read);
}

这里有几个关键点:

1. 不要一个字节一个字节复制

这样性能很差。

2. 使用缓冲区批量读写

缓冲区可以显著减少系统调用次数。

3. 注意返回值 read

最后一次读取可能不足 buffer 大小。

所以写入时必须使用:

1
output.write(buffer, 0, read);

不能写:

1
output.write(buffer);

否则可能写入脏数据。

4. 注意关闭资源

工具类通常不一定负责关闭你传入的流。

谁打开,谁关闭。

Source / Sink 的高级方法通常会帮你打开并关闭内部流。

chapter 40:Streams 工具类和 Source / Sink 的关系

可以这样理解:

1
2
Source / Sink:更高层 IO 抽象
Streams:更底层流工具

例如:

1
ByteSource.copyTo(ByteSink)

内部大概会:

1
2
3
4
打开 InputStream
打开 OutputStream
调用 ByteStreams.copy
关闭流

再例如:

1
CharSource.read()

内部大概会:

1
2
3
打开 Reader
调用 CharStreams.toString
关闭 Reader

所以:

  • 业务代码更推荐用 Source / Sink;
  • 底层流操作可以用 CharStreams / ByteStreams;
  • 如果你已经有 InputStream / OutputStream,就用 ByteStreams;
  • 如果你已经有 Reader / Writer,就用 CharStreams;
  • 如果你还没打开流,只是有文件、URL、字节数组,优先抽象成 Source / Sink。

chapter 41:实战:统一文本导入服务

需求:

  1. 输入可以来自文件,也可以来自内存字符串;
  2. 统一使用 CharSource
  3. 按行读取;
  4. 过滤空行;
  5. 返回有效行列表。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.google.common.io.CharSource;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class TextImportService {

public List<String> importLines(CharSource source) throws IOException {
List<String> lines = source.readLines();

List<String> result = new ArrayList<>();

for (String line : lines) {
if (line != null && !line.isBlank()) {
result.add(line.trim());
}
}

return result;
}
}

使用内存字符串测试:

1
2
3
4
5
6
7
8
9
10
11
12
import com.google.common.io.CharSource;

public class TextImportMemoryDemo {

public static void main(String[] args) throws Exception {
CharSource source = CharSource.wrap("Mario\n\n Luigi \nPeach");

TextImportService service = new TextImportService();

System.out.println(service.importLines(source));
}
}

使用文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class TextImportFileDemo {

public static void main(String[] args) throws Exception {
File file = new File("guava-io-demo/import-users.txt");

Files.createParentDirs(file);
Files.asCharSink(file, StandardCharsets.UTF_8)
.write("Mario\n\nLuigi\nPeach");

TextImportService service = new TextImportService();

System.out.println(service.importLines(
Files.asCharSource(file, StandardCharsets.UTF_8)
));
}
}

同一个业务服务可以支持不同数据来源。

这就是 CharSource 的价值。

chapter 42:实战:统一二进制存储服务

需求:

  1. 输入是任意 ByteSource
  2. 保存到任意 ByteSink
  3. 计算 SHA-256;
  4. 返回复制字节数和哈希。

结果对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class BinaryStoreResult {

private final long copiedBytes;

private final String sha256;

public BinaryStoreResult(long copiedBytes, String sha256) {
this.copiedBytes = copiedBytes;
this.sha256 = sha256;
}

public long getCopiedBytes() {
return copiedBytes;
}

public String getSha256() {
return sha256;
}

@Override
public String toString() {
return "BinaryStoreResult{"
+ "copiedBytes=" + copiedBytes
+ ", sha256='" + sha256 + '\''
+ '}';
}
}

服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;

import java.io.IOException;

public class BinaryStoreService {

public BinaryStoreResult store(ByteSource source, ByteSink sink) throws IOException {
HashCode hashCode = source.hash(Hashing.sha256());

long copiedBytes = source.copyTo(sink);

return new BinaryStoreResult(
copiedBytes,
hashCode.toString()
);
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.google.common.io.ByteSource;
import com.google.common.io.Files;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class BinaryStoreDemo {

public static void main(String[] args) throws Exception {
ByteSource source = ByteSource.wrap(
"hello binary store".getBytes(StandardCharsets.UTF_8)
);

File targetFile = new File("guava-io-demo/store.bin");

Files.createParentDirs(targetFile);

BinaryStoreService service = new BinaryStoreService();

BinaryStoreResult result = service.store(
source,
Files.asByteSink(targetFile)
);

System.out.println(result);
}
}

这个服务不关心输入来自哪里,也不关心输出写到哪里。

这就是 ByteSource / ByteSink 的威力。

chapter 43:Source / Sink 的设计优势总结

1. 把 IO 来源和目标抽象化

业务方法不再写死 File

2. 测试更简单

测试可以用:

1
2
CharSource.wrap(...)
ByteSource.wrap(...)

不一定创建真实文件。

3. 资源管理更清晰

Source / Sink 的高级方法会打开并关闭内部流。

4. 代码表达更清楚

1
source.copyTo(sink)

比手写一堆流复制更表达意图。

5. 统一文件、内存、资源等来源

文件只是来源之一。

6. 和模板方法模式结合得很好

子类只需要提供 openStream()

父类提供通用读取/写入能力。

chapter 44:使用建议

1. 处理文本优先 CharSource / CharSink

1
2
CharSource source = Files.asCharSource(file, UTF_8);
CharSink sink = Files.asCharSink(file, UTF_8);

2. 处理二进制优先 ByteSource / ByteSink

1
2
ByteSource source = Files.asByteSource(file);
ByteSink sink = Files.asByteSink(file);

3. 已经有流时使用 Streams 工具类

1
2
ByteStreams.copy(inputStream, outputStream);
CharStreams.copy(reader, writer);

4. 小文件可以 read / toByteArray

大文件不要一次性读入内存。

5. 谁打开流,谁负责关闭

如果你自己传入 InputStream,就要自己管理关闭。

6. 不要依赖默认字符集

字符和字节转换必须指定:

1
StandardCharsets.UTF_8

7. Spring Boot 上传下载中可以借鉴 Source / Sink 思想

即使不用 Guava,也可以把输入和输出抽象出来。

8. 不要为了抽象而抽象

简单文件读写用 JDK Files 就可以。

当你需要统一多种来源、多种目标时,Source / Sink 的价值更明显。

chapter 45:一句话总结

这一章的核心是:

Guava IO 的精髓不是 Files 工具方法,而是 Source / Sink 抽象:用 Source 表示数据来源,用 Sink 表示数据目标,再通过 Streams 工具类完成底层流复制。

CharSource 表示字符输入来源。

CharSink 表示字符输出目标。

ByteSource 表示字节输入来源。

ByteSink 表示字节输出目标。

CharStreams 处理 Reader / Writer / Readable / Appendable。

ByteStreams 处理 InputStream / OutputStream。

它们之间的关系可以总结为:

1
2
3
4
5
6
7
文件 / 内存 / URL / 资源

Source

Streams 底层读取复制

Sink

好的 IO 设计不是到处传 FileInputStreamFileOutputStream

好的 IO 设计应该让业务代码表达:

1
2
3
4
从这个来源读
写到那个目标
文本用这个编码
二进制按字节处理

Source / Sink 这套思想非常值得学习。

即使你在新项目中更多使用 JDK NIO,也可以把这种抽象思路带到自己的代码设计中。

参考资料

  • Google Guava API: CharSource.
  • Google Guava API: CharSink.
  • Google Guava API: ByteSource.
  • Google Guava API: ByteSink.
  • Google Guava API: CharStreams.
  • Google Guava API: ByteStreams.
  • Google Guava API: Files.
  • Google Guava API: Resources.
  • Java Documentation: Reader.
  • Java Documentation: Writer.
  • Java Documentation: InputStream.
  • Java Documentation: OutputStream.
  • Java Documentation: StandardCharsets.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


Guava IO 进阶:Source、Sink 与 Streams
https://allendericdalexander.github.io/2026/04/01/java/utils/guava/05guava-source-sink-streams-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议