欢迎你来读这篇博客,这篇博客主要是关于Guava IO 基础:Files。 其中包括 Guava Files 基础 API、文件读写、文件复制、文件移动、路径处理、按行读取、文件编码处理、文件哈希、文件比较、临时目录、文件遍历、文件扩展名处理,以及 Files 工具类背后的源码设计思想。
序言 Java IO 是后端开发绕不开的一块。
你可能会在这些场景里频繁处理文件:
上传文件落盘;
导入 CSV;
导出报表;
读取配置文件;
复制临时文件;
移动文件到归档目录;
计算文件哈希;
判断两个文件内容是否一致;
遍历目录;
提取文件扩展名;
创建临时目录;
处理文本编码。
JDK 自身已经提供了很多 IO API。
比如早期的:
1 2 3 4 5 java.io.File java.io.InputStream java.io.OutputStream java.io.Reader java.io.Writer
以及 Java 7 之后更推荐的:
1 2 3 java.nio.file.Files java.nio.file.Path java.nio.charset.StandardCharsets
那 Guava 的 Files 还有什么学习价值?
答案是:非常有价值,尤其适合作为 IO 模块入门。
Guava 的 com.google.common.io.Files 对常见文件操作做了很多封装,让代码更容易读,也更容易理解“文件工具类应该怎么设计”。
这一章对应课程结构:
1 2 3 4 第 4 章:Guava IO 基础:Files 4.1 Files 第一部分 4.2 Files 第二部分 4.3 Files 第三部分
对应原课程:
1 2 3 08 Guava 之 Files 讲解第一部分 09 Guava 之 Files 讲解第二部分 10 Guava 之 Files 讲解第三部分
注意一个容易混淆的点:
1 com.google.common.io.Files
和:
不是同一个类。
前者是 Guava 的 Files。
后者是 JDK 的 Files。
在同一个类里同时使用时,建议不要同时 import 两个 Files,否则名字冲突。
可以 import 一个,另一个写全限定名。
正文 chapter 1:Guava Files 是什么 Guava 的 Files 位于:
1 com.google.common.io.Files
它主要提供围绕 java.io.File 的常见工具方法。
常见能力包括:
文件读写;
创建 Reader / Writer;
创建 ByteSource / CharSource;
创建 ByteSink / CharSink;
文件复制;
文件移动;
创建父目录;
touch 文件;
按行读取;
文件哈希;
文件内容比较;
临时目录;
文件遍历;
文件扩展名;
文件名无扩展名;
路径简化。
它的定位可以理解为:
在 JDK 原生 File API 之上,提供更好用、更清晰的文件工具方法。
不过现在新项目中,如果只是普通文件操作,JDK 的 java.nio.file.Files 已经非常强大。
所以实践建议是:
老项目或已有 Guava 依赖,可以使用 Guava Files;
新项目优先考虑 JDK java.nio.file.Files;
学习 Guava Files 可以帮助理解 IO 工具类设计思想;
Guava 的 ByteSource、CharSource、ByteSink、CharSink 很值得深入学习。
chapter 2:Guava Files 和 JDK Files 对比
对比项
Guava com.google.common.io.Files
JDK java.nio.file.Files
核心对象
java.io.File
java.nio.file.Path
出现定位
补充早期 JDK IO 不便之处
Java 标准库 NIO 文件 API
文件读写
简洁封装
标准能力完整
目录遍历
提供 fileTraverser 等
walk、find、list
临时文件/目录
有工具方法
标准方法更推荐
推荐程度
老项目常见,学习价值高
新项目优先推荐
设计思想
Source/Sink 抽象非常值得学
标准化、平台能力更完整
一句话建议:
业务新代码优先 JDK NIO,理解 IO 抽象和维护老项目时必须会 Guava Files。
chapter 3:Files 第一部分:基础 API 总览 Guava Files 常用方法可以大致分为几组。
1. Source / Sink 相关 1 2 3 4 Files.asByteSource(file) Files.asCharSource(file, charset) Files.asByteSink(file) Files.asCharSink(file, charset)
2. 读写相关 1 2 3 4 5 6 Files.toByteArray(file) Files.readLines(file, charset) Files.readFirstLine(file, charset) Files.write(bytes, file) Files.write(charSequence, file, charset) Files.append(charSequence, file, charset)
3. 复制移动相关 1 2 Files.copy(from, to) Files.move(from, to)
4. 目录相关 1 2 Files.createParentDirs(file) Files.createTempDir()
5. 文件属性和名称相关 1 2 3 Files.getFileExtension(fileName) Files.getNameWithoutExtension(fileName) Files.simplifyPath(path)
6. 文件比较和哈希 1 2 Files.equal(file1, file2) Files.hash(file, hashFunction)
7. 遍历相关
不同 Guava 版本中,一些旧方法可能被标记为不推荐使用,官方更鼓励使用 ByteSource、CharSource、ByteSink、CharSink 这一套抽象。
所以本文会同时讲“直接方法”和“Source/Sink 思想”。
chapter 4:创建测试文件 后面示例会用一个临时目录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.io.File;import java.io.IOException;public class FileDemoSupport { public static File createDemoDir () throws IOException { File dir = new File ("guava-files-demo" ); if (!dir.exists() && !dir.mkdirs()) { throw new IOException ("create demo dir failed: " + dir.getAbsolutePath()); } return dir; } }
真实项目中,不建议把临时文件随便写到当前工作目录。
可以使用:
1 java.nio.file.Files.createTempDirectory(...)
或者在 Spring Boot 配置里指定上传目录、导入目录、归档目录。
chapter 5:文件写入:写入字符串 Guava Files 可以写入字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class FilesWriteStringDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/hello.txt" ); Files.createParentDirs(file); Files.write("hello guava files" , file, StandardCharsets.UTF_8); System.out.println("写入完成:" + file.getAbsolutePath()); } }
这里:
1 Files.createParentDirs(file)
用于创建父目录。
用于写入文本内容。
注意指定编码:
不要依赖系统默认编码。
chapter 6:文件写入:写入字节数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class FilesWriteBytesDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/bytes.txt" ); Files.createParentDirs(file); byte [] data = "hello bytes" .getBytes(StandardCharsets.UTF_8); Files.write(data, file); System.out.println("字节写入完成" ); } }
字节写入适合:
图片;
文件上传内容;
二进制协议;
加密后数据;
压缩后数据。
文本优先使用字符集写入。
二进制优先使用 byte 数组或流。
chapter 7:追加文件内容 追加内容可以使用:
示例:
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.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class FilesAppendDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/log.txt" ); Files.createParentDirs(file); Files.write("line1\n" , file, StandardCharsets.UTF_8); Files.append("line2\n" , file, StandardCharsets.UTF_8); Files.append("line3\n" , file, StandardCharsets.UTF_8); System.out.println("追加写入完成" ); } }
不过在新代码里,更推荐使用 CharSink:
1 2 Files.asCharSink(file, StandardCharsets.UTF_8, FileWriteMode.APPEND) .write("line" );
这个写法更体现 Guava IO 的设计思想。
chapter 8:使用 CharSink 写文件 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 CharSinkDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/charsink.txt" ); Files.createParentDirs(file); CharSink charSink = Files.asCharSink(file, StandardCharsets.UTF_8); charSink.write("hello CharSink" ); System.out.println("CharSink 写入完成" ); } }
如果要追加:
1 2 3 4 5 6 7 import com.google.common.io.FileWriteMode;CharSink charSink = Files.asCharSink( file, StandardCharsets.UTF_8, FileWriteMode.APPEND );
CharSink 的好处是:
写入目标被抽象成对象,后续可以统一处理文件、内存、网络等不同输出目标。
chapter 9:使用 ByteSink 写文件 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 ByteSinkDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/bytesink.txt" ); Files.createParentDirs(file); ByteSink byteSink = Files.asByteSink(file); byteSink.write("hello ByteSink" .getBytes(StandardCharsets.UTF_8)); System.out.println("ByteSink 写入完成" ); } }
什么时候用 ByteSink?
写二进制文件;
写图片;
写压缩文件;
写加密数据;
写上传文件内容。
文本优先 CharSink。
二进制优先 ByteSink。
chapter 10:文件读取:读取全部字节 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class FilesReadBytesDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/hello.txt" ); byte [] bytes = Files.toByteArray(file); String text = new String (bytes, StandardCharsets.UTF_8); System.out.println(text); } }
这种方式会把整个文件读入内存。
适合小文件。
不适合大文件。
如果文件很大,比如几百 MB 或 GB,不能直接 toByteArray。
应该使用流式处理、按行处理或分块处理。
chapter 11:文件读取:读取文本内容 Guava 早期有直接读取字符串的方法。
但更推荐使用 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 com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class CharSourceReadDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/hello.txt" ); CharSource charSource = Files.asCharSource(file, StandardCharsets.UTF_8); String content = charSource.read(); System.out.println(content); } }
CharSource 表示字符输入来源。
可以理解为:
一个可以读取字符数据的来源。
它比“直接读文件”更抽象。
这为后面学习 Guava IO 的 CharSource、ByteSource 打基础。
chapter 12:使用 ByteSource 读取文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.google.common.io.ByteSource;import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class ByteSourceReadDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/hello.txt" ); ByteSource byteSource = Files.asByteSource(file); byte [] bytes = byteSource.read(); System.out.println(new String (bytes, StandardCharsets.UTF_8)); } }
ByteSource 表示字节输入来源。
适合:
读取二进制文件;
文件哈希;
文件复制;
文件内容比较;
把来源抽象出来。
chapter 13:文件复制 Guava Files 复制文件:
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.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class FilesCopyDemo { public static void main (String[] args) throws Exception { File source = new File ("guava-files-demo/source.txt" ); File target = new File ("guava-files-demo/backup/source-copy.txt" ); Files.createParentDirs(source); Files.write("copy me" , source, StandardCharsets.UTF_8); Files.createParentDirs(target); Files.copy(source, target); System.out.println("复制完成:" + target.getAbsolutePath()); } }
注意:
1 Files.copy(source, target)
如果目标文件已存在,可能会覆盖内容。
真实业务中要根据场景处理:
是否允许覆盖;
是否备份旧文件;
是否校验文件大小;
是否计算哈希;
是否记录日志;
是否异常回滚。
chapter 14:文件移动 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.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class FilesMoveDemo { public static void main (String[] args) throws Exception { File source = new File ("guava-files-demo/move-source.txt" ); File target = new File ("guava-files-demo/archive/move-target.txt" ); Files.createParentDirs(source); Files.write("move me" , source, StandardCharsets.UTF_8); Files.createParentDirs(target); Files.move(source, target); System.out.println("移动完成:" + target.getAbsolutePath()); } }
移动文件常见于:
上传成功后移动到正式目录;
导入完成后移动到归档目录;
处理失败后移动到 error 目录;
临时文件转正式文件;
日志文件归档。
移动文件时要考虑:
目标文件是否存在;
跨磁盘移动是否成功;
移动失败如何处理;
是否需要原子移动;
是否需要保留源文件。
如果要求原子移动,JDK NIO 的:
1 java.nio.file.Files.move(...)
配合:
1 StandardCopyOption.ATOMIC_MOVE
更适合。
chapter 15:创建父目录 很多文件写入失败,不是因为文件内容有问题,而是因为父目录不存在。
Guava 提供:
1 Files.createParentDirs(file)
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class CreateParentDirsDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/a/b/c/data.txt" ); Files.createParentDirs(file); Files.write("hello" , file, StandardCharsets.UTF_8); System.out.println("写入完成" ); } }
这个方法会创建:
但不会创建文件本身。
它只创建父目录。
chapter 16:touch 文件 touch 的含义类似 Linux 中的 touch 命令:
如果文件不存在,就创建文件;
如果文件存在,就更新最后修改时间。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import com.google.common.io.Files;import java.io.File;public class FilesTouchDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/touch.txt" ); Files.createParentDirs(file); Files.touch(file); System.out.println("touch 完成:" + file.exists()); } }
常见用途:
创建空文件;
标记文件已处理;
更新文件时间戳;
生成占位文件。
chapter 17:路径处理:simplifyPath Guava 提供:
1 Files.simplifyPath(path)
用于简化路径字符串。
示例:
1 2 3 4 5 6 7 8 9 import com.google.common.io.Files;public class SimplifyPathDemo { public static void main (String[] args) { System.out.println(Files.simplifyPath("/a/b/../c/./d" )); System.out.println(Files.simplifyPath("a//b/./c" )); } }
输出类似:
它处理的是字符串路径,不等价于真正访问文件系统。
不要把它当成安全路径校验。
如果涉及用户上传文件路径,一定要防止路径穿越攻击:
真实项目中要结合 canonical path、根目录校验等方式处理。
chapter 18:Files 第二部分:按行读取 读取文本文件时,按行读取非常常见。
Guava 提供:
1 Files.readLines(file, charset)
示例:
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.Files;import java.io.File;import java.nio.charset.StandardCharsets;import java.util.List;public class FilesReadLinesDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/users.csv" ); Files.createParentDirs(file); Files.write("1,Mario\n2,Luigi\n3,Peach" , file, StandardCharsets.UTF_8); List<String> lines = Files.readLines(file, StandardCharsets.UTF_8); for (String line : lines) { System.out.println(line); } } }
输出:
这种方式会把所有行读入内存。
适合小文件。
大文件建议使用 LineProcessor 或 JDK 流式 API。
chapter 19:读取第一行 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class FilesReadFirstLineDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/users.csv" ); String firstLine = Files.readFirstLine(file, StandardCharsets.UTF_8); System.out.println(firstLine); } }
适合:
读取 CSV 表头;
判断文件格式;
读取配置第一行;
读取版本标识。
如果文件为空,结果可能为 null,要做好处理。
chapter 20:使用 LineProcessor 处理大文件 LineProcessor 可以逐行处理文件,而不是一次性把所有行加载到内存。
示例:统计行数。
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 30 31 32 import com.google.common.io.Files;import com.google.common.io.LineProcessor;import java.io.File;import java.io.IOException;import java.nio.charset.StandardCharsets;public class LineProcessorDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/users.csv" ); Integer count = Files.readLines(file, StandardCharsets.UTF_8, new LineProcessor <Integer>() { private int lineCount = 0 ; @Override public boolean processLine (String line) throws IOException { lineCount++; return true ; } @Override public Integer getResult () { return lineCount; } }); System.out.println("行数:" + count); } }
processLine 返回 boolean。
返回 true:继续处理下一行;
返回 false:停止处理。
chapter 21:LineProcessor 实战:查找第一条匹配行 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 30 31 32 33 34 35 36 import com.google.common.io.Files;import com.google.common.io.LineProcessor;import java.io.File;import java.io.IOException;import java.nio.charset.StandardCharsets;public class FindLineProcessorDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/users.csv" ); String result = Files.readLines(file, StandardCharsets.UTF_8, new LineProcessor <String>() { private String matched; @Override public boolean processLine (String line) throws IOException { if (line.contains("Luigi" )) { matched = line; return false ; } return true ; } @Override public String getResult () { return matched; } }); System.out.println("匹配结果:" + result); } }
这种写法适合:
大文件搜索;
日志扫描;
CSV 校验;
找到目标后立即停止;
避免一次性加载全部内容。
chapter 22:文件编码处理 读取文本文件时,一定要指定字符集。
推荐:
示例:
1 String content = Files.asCharSource(file, StandardCharsets.UTF_8).read();
不要这样:
因为 FileReader 使用平台默认字符集,容易产生乱码。
错误的编码处理会导致:
中文乱码;
CSV 导入失败;
签名不一致;
文件哈希与预期不同;
跨环境行为不一致。
在业务系统中,文件编码最好有统一规范:
如果必须支持 GBK、GB18030 等编码,要在配置或导入参数里明确指定。
chapter 23:文件哈希 文件哈希常用于:
判断文件内容是否变化;
文件去重;
秒传;
校验下载完整性;
生成文件指纹;
校验上传结果。
Guava 可以使用:
1 Files.hash(file, Hashing.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.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class FilesHashDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-files-demo/hash.txt" ); Files.createParentDirs(file); Files.write("hello hash" , file, StandardCharsets.UTF_8); HashCode hashCode = Files.hash(file, Hashing.sha256()); System.out.println(hashCode.toString()); } }
注意:某些 Guava 版本中,一些便捷 hash 方法可能更推荐通过 ByteSource 使用。
例如:
1 HashCode hashCode = Files.asByteSource(file).hash(Hashing.sha256());
这种写法更符合 Guava IO 的 Source 设计。
chapter 24:常见哈希算法选择 常见算法:
1 2 3 4 Hashing.md5() Hashing.sha1() Hashing.sha256() Hashing.sha512()
实践建议:
文件完整性校验:SHA-256;
安全相关:不要用 MD5;
普通非安全去重:可以考虑更快的非加密哈希,但要理解风险;
密码存储:不要用这些普通哈希,应该用 bcrypt、Argon2、PBKDF2。
MD5 现在不适合安全场景。
如果只是判断文件是否相同,MD5 仍然可能在老系统中看到,但新设计建议优先 SHA-256。
chapter 25:文件比较 Guava 提供:
1 Files.equal(file1, file2)
用于判断两个文件内容是否相同。
示例:
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 FilesEqualDemo { public static void main (String[] args) throws Exception { File file1 = new File ("guava-files-demo/a.txt" ); File file2 = new File ("guava-files-demo/b.txt" ); Files.createParentDirs(file1); Files.write("same content" , file1, StandardCharsets.UTF_8); Files.write("same content" , file2, StandardCharsets.UTF_8); boolean same = Files.equal(file1, file2); System.out.println("文件是否相同:" + same); } }
内部通常会先比较文件长度。
如果长度不同,直接返回 false。
长度相同,再比较内容。
这比你手写流比较更可靠。
chapter 26:文件比较的业务场景 文件比较适合:
上传文件重复判断;
导入文件是否和上次一致;
备份文件校验;
文件同步校验;
生成文件后验证;
本地缓存文件检查。
不过大文件比较会消耗 IO。
如果频繁比较大文件,可以考虑提前保存文件哈希。
例如:
1 2 3 4 5 file_id file_name file_size sha256 storage_path
判断是否重复时先比较:
chapter 27:Files 第三部分:临时目录 Guava 提供:
可以创建临时目录。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 import com.google.common.io.Files;import java.io.File;public class FilesCreateTempDirDemo { public static void main (String[] args) { File tempDir = Files.createTempDir(); System.out.println(tempDir.getAbsolutePath()); } }
不过在新项目中,更推荐 JDK NIO:
1 java.nio.file.Files.createTempDirectory("prefix-" )
示例:
1 2 3 4 5 6 7 8 9 10 import java.nio.file.Path;public class JdkTempDirectoryDemo { public static void main (String[] args) throws Exception { Path tempDir = java.nio.file.Files.createTempDirectory("guava-demo-" ); System.out.println(tempDir); } }
临时目录适合:
文件上传临时处理;
解压缩临时目录;
导入文件预处理;
生成报表中间文件;
测试文件操作。
chapter 28:临时目录使用注意事项 临时目录不是创建完就不管了。
要注意:
1. 用完要清理 否则临时文件越来越多。
2. 不要保存业务重要文件 临时目录可能被系统清理。
3. 不要把用户文件名直接拼到路径里 防止路径穿越和非法字符。
4. 注意并发 多个任务不能写同一个临时文件名。
5. 注意磁盘空间 大文件导入、解压、转码都可能快速吃满磁盘。
真实项目中建议:
临时目录单独配置;
定期清理;
限制文件大小;
使用唯一任务 ID 作为目录名;
处理结束后 finally 清理。
chapter 29:文件扩展名 Guava 提供:
1 Files.getFileExtension(fileName)
示例:
1 2 3 4 5 6 7 8 9 10 import com.google.common.io.Files;public class FileExtensionDemo { public static void main (String[] args) { System.out.println(Files.getFileExtension("report.xlsx" )); System.out.println(Files.getFileExtension("archive.tar.gz" )); System.out.println(Files.getFileExtension("README" )); } }
输出:
注意:
返回的是:
不是:
如果业务上需要识别复合扩展名,要自己处理。
chapter 30:获取无扩展名文件名 1 2 3 4 5 6 7 8 9 10 import com.google.common.io.Files;public class NameWithoutExtensionDemo { public static void main (String[] args) { System.out.println(Files.getNameWithoutExtension("report.xlsx" )); System.out.println(Files.getNameWithoutExtension("archive.tar.gz" )); System.out.println(Files.getNameWithoutExtension("README" )); } }
输出:
1 2 3 report archive.tar README
常见用途:
上传文件重命名;
生成导出文件名;
保留原文件名;
文件类型判断;
日志展示。
chapter 31:文件名处理注意事项 不要只根据扩展名判断文件安全性。
例如用户上传:
可能不是真图片。
真实上传校验至少考虑:
扩展名白名单;
MIME Type;
文件魔数;
文件大小;
图片解码验证;
病毒扫描;
存储路径隔离;
禁止直接执行上传文件;
文件名随机化。
扩展名只是辅助信息,不是安全保证。
chapter 32:文件遍历:fileTraverser Guava 提供文件遍历能力。
较新的写法是:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import com.google.common.io.Files;import com.google.common.graph.Traverser;import java.io.File;public class FileTraverserDemo { public static void main (String[] args) { File root = new File ("guava-files-demo" ); Traverser<File> traverser = Files.fileTraverser(); for (File file : traverser.depthFirstPreOrder(root)) { System.out.println(file.getPath()); } } }
常见遍历方式:
1 2 3 depthFirstPreOrder(root) depthFirstPostOrder(root) breadthFirst(root)
1. 深度优先前序 先访问目录本身,再访问子节点。
2. 深度优先后序 先访问子节点,再访问目录本身。
适合删除目录时使用。
3. 广度优先 一层一层访问。
适合层级扫描。
chapter 33:文件遍历实战:统计目录下所有文件大小 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.Files;import java.io.File;public class DirectorySizeDemo { public static void main (String[] args) { File root = new File ("guava-files-demo" ); long totalSize = 0L ; for (File file : Files.fileTraverser().depthFirstPreOrder(root)) { if (file.isFile()) { totalSize += file.length(); } } System.out.println("目录文件总大小:" + totalSize + " bytes" ); } }
文件遍历常见用途:
统计目录大小;
查找特定扩展名文件;
清理临时文件;
扫描导入目录;
归档历史文件;
删除过期文件。
chapter 34:文件遍历实战:查找所有 CSV 文件 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.Files;import java.io.File;import java.util.ArrayList;import java.util.List;public class FindCsvFilesDemo { public static void main (String[] args) { File root = new File ("guava-files-demo" ); List<File> csvFiles = new ArrayList <>(); for (File file : Files.fileTraverser().depthFirstPreOrder(root)) { if (file.isFile() && "csv" .equalsIgnoreCase(Files.getFileExtension(file.getName()))) { csvFiles.add(file); } } System.out.println("CSV 文件数量:" + csvFiles.size()); System.out.println(csvFiles); } }
这就是一个简单的目录扫描器。
真实项目中要注意:
权限异常;
符号链接;
目录过深;
文件数量过多;
扫描耗时;
并发任务冲突。
chapter 35:JDK 文件遍历对比 JDK NIO 也提供文件遍历:
1 java.nio.file.Files.walk(path)
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.nio.file.Path;import java.util.stream.Stream;public class JdkFilesWalkDemo { public static void main (String[] args) throws Exception { Path root = Path.of("guava-files-demo" ); try (Stream<Path> stream = java.nio.file.Files.walk(root)) { stream.filter(java.nio.file.Files::isRegularFile) .forEach(System.out::println); } } }
JDK Files.walk 返回 Stream。
要注意关闭 Stream:
1 2 try (Stream<Path> stream = Files.walk(root)) { }
新项目中目录遍历优先考虑 JDK NIO。
Guava 的 fileTraverser 更适合理解遍历思想和维护旧项目。
chapter 36:Files 工具类源码设计思想 Guava Files 的源码设计有几个值得学习的点。
1. 工具类不可实例化 Files 是工具类。
它通常会有私有构造方法:
表示不允许实例化。
这是一种工具类常见设计。
2. 静态方法封装常用操作 例如:
1 2 3 4 5 6 copy move equal hash readLines createParentDirs
让调用方不用每次手写样板代码。
3. 基于 Source / Sink 抽象 这是 Guava IO 设计中非常重要的一点。
1 2 3 4 ByteSource CharSource ByteSink CharSink
它们把“从哪里读”和“写到哪里”抽象出来。
文件只是其中一种来源或目标。
这种设计比单纯工具方法更灵活。
4. 显式字符集 很多文本相关方法都要求传入 Charset。
这避免了默认字符集带来的乱码问题。
5. 尽早失败 比如参数为 null 时尽早抛错。
这符合 Guava 一贯风格。
6. 组合复用 很多 Files 方法内部会委托给 ByteSource、CharSource、ByteSink、CharSink。
这体现了良好的分层。
chapter 37:Source / Sink 思想为什么重要 传统写法关注具体类型:
1 2 3 4 FileInputStream FileOutputStream InputStreamReader BufferedReader
调用方容易被细节淹没。
Guava 的 Source / Sink 思想是:
1 2 Source:数据从哪里来 Sink:数据写到哪里去
例如:
1 2 3 ByteSource source = Files.asByteSource(file);ByteSink sink = Files.asByteSink(target); source.copyTo(sink);
这段代码表达的是:
它不关心底层细节。
这种抽象在后面学习:
1 2 3 4 5 CharSource CharSink ByteSource ByteSink Closer
时会非常重要。
chapter 38:Guava Files 和 Spring Boot 文件上传 在 Spring Boot 文件上传中,经常会处理 MultipartFile。
例如:
1 2 3 @PostMapping("/upload") public String upload (@RequestParam MultipartFile file) { }
如果要保存到本地,可以使用 JDK NIO:
1 2 3 Path target = Path.of(uploadDir, file.getOriginalFilename()); java.nio.file.Files.copy(file.getInputStream(), target);
如果项目已有 Guava,也可以结合 ByteSink:
1 2 3 4 5 File targetFile = new File (uploadDir, safeFileName); Files.createParentDirs(targetFile); Files.asByteSink(targetFile).writeFrom(file.getInputStream());
不过真实上传不能直接使用原始文件名。
需要做:
文件名清洗;
扩展名白名单;
文件大小限制;
MIME 校验;
路径穿越防护;
随机文件名;
分目录存储;
上传日志记录。
chapter 39:实战:本地文件归档工具 下面写一个简单归档工具。
需求:
输入源文件;
创建归档目录;
计算文件 SHA-256;
复制到归档目录;
比较复制前后文件是否一致;
返回归档结果。
归档结果 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class ArchiveResult { private final String sourcePath; private final String targetPath; private final String sha256; private final boolean same; public ArchiveResult (String sourcePath, String targetPath, String sha256, boolean same) { this .sourcePath = sourcePath; this .targetPath = targetPath; this .sha256 = sha256; this .same = same; } public String getSourcePath () { return sourcePath; } public String getTargetPath () { return targetPath; } public String getSha256 () { return sha256; } public boolean isSame () { return same; } @Override public String toString () { return "ArchiveResult{" + "sourcePath='" + sourcePath + '\'' + ", targetPath='" + targetPath + '\'' + ", sha256='" + sha256 + '\'' + ", same=" + same + '}' ; } }
归档服务 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import com.google.common.hash.HashCode;import com.google.common.hash.Hashing;import com.google.common.io.Files;import java.io.File;public class FileArchiveService { public ArchiveResult archive (File sourceFile, File archiveDir) throws Exception { if (sourceFile == null || !sourceFile.isFile()) { throw new IllegalArgumentException ("sourceFile must be a file" ); } if (archiveDir == null ) { throw new IllegalArgumentException ("archiveDir can not be null" ); } if (!archiveDir.exists() && !archiveDir.mkdirs()) { throw new IllegalStateException ("create archive dir failed: " + archiveDir.getAbsolutePath()); } HashCode hashCode = Files.asByteSource(sourceFile).hash(Hashing.sha256()); String extension = Files.getFileExtension(sourceFile.getName()); String nameWithoutExtension = Files.getNameWithoutExtension(sourceFile.getName()); String targetFileName = extension.isBlank() ? nameWithoutExtension + "-" + hashCode : nameWithoutExtension + "-" + hashCode + "." + extension; File targetFile = new File (archiveDir, targetFileName); Files.copy(sourceFile, targetFile); boolean same = Files.equal(sourceFile, targetFile); return new ArchiveResult ( sourceFile.getAbsolutePath(), targetFile.getAbsolutePath(), hashCode.toString(), same ); } }
客户端 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.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class FileArchiveDemo { public static void main (String[] args) throws Exception { File source = new File ("guava-files-demo/source/report.txt" ); Files.createParentDirs(source); Files.write("hello archive" , source, StandardCharsets.UTF_8); File archiveDir = new File ("guava-files-demo/archive" ); FileArchiveService archiveService = new FileArchiveService (); ArchiveResult result = archiveService.archive(source, archiveDir); System.out.println(result); } }
这个实战把本章多个知识点串起来了:
创建父目录;
写文件;
获取扩展名;
获取无扩展名文件名;
计算哈希;
复制文件;
比较文件内容。
chapter 40:Guava Files 使用建议 1. 文本读写必须指定字符集 推荐:
2. 小文件可以一次性读取 例如:
1 Files.asCharSource(file, UTF_8).read()
大文件不要一次性读入内存。
3. 大文件按行或流式处理 可以使用:
或者 JDK:
4. 新项目优先 JDK NIO 尤其是:
1 2 Path java.nio.file.Files
5. 老项目 Guava Files 要能维护 很多老项目仍然使用:
1 com.google.common.io.Files
6. 文件上传不要直接信任原文件名 要防止:
7. 临时文件要及时清理 尤其是导入、导出、压缩、转码任务。
8. 文件比较和哈希会消耗 IO 大文件频繁比较要谨慎。
9. Source / Sink 思想要重点理解 这是后续学习 Guava IO 更高级内容的基础。
chapter 41:Files 常用 API 速查
功能
Guava Files API
创建父目录
Files.createParentDirs(file)
创建空文件或更新时间
Files.touch(file)
写字节
Files.write(byte[], file)
写文本
Files.write(CharSequence, file, charset)
追加文本
Files.append(text, file, charset)
读字节
Files.toByteArray(file)
按行读取
Files.readLines(file, charset)
读取第一行
Files.readFirstLine(file, charset)
文件复制
Files.copy(from, to)
文件移动
Files.move(from, to)
文件比较
Files.equal(file1, file2)
文件哈希
Files.asByteSource(file).hash(...)
字符来源
Files.asCharSource(file, charset)
字节来源
Files.asByteSource(file)
字符输出
Files.asCharSink(file, charset)
字节输出
Files.asByteSink(file)
扩展名
Files.getFileExtension(name)
无扩展名文件名
Files.getNameWithoutExtension(name)
路径简化
Files.simplifyPath(path)
文件遍历
Files.fileTraverser()
chapter 42:一句话总结 这一章的核心是:
Guava Files 用简单清晰的 API 封装了常见文件操作,同时通过 ByteSource、CharSource、ByteSink、CharSink 展示了优秀的 IO 抽象设计。
Files 第一部分重点是:
文件读写;
文件复制;
文件移动;
父目录创建;
路径处理。
Files 第二部分重点是:
文件内容读取;
按行读取;
编码处理;
文件哈希;
文件比较。
Files 第三部分重点是:
临时目录;
文件遍历;
文件扩展名;
文件名处理;
源码设计思想。
在现代 Java 项目中,JDK NIO 的 java.nio.file.Files 很多时候是首选。
但 Guava Files 仍然非常值得学,因为它让我们看到一个好工具类应该如何封装常见操作,又如何通过 Source/Sink 抽象把 IO 设计得更清晰。
好代码不是把所有 IO 细节都堆到业务方法里。
好代码应该让人一眼看懂:
1 2 3 4 5 这里在读文件 这里在写文件 这里在复制文件 这里在比较文件 这里在遍历目录
IO 代码越基础,越要稳。
因为它一旦出错,常常不是一个小 bug,而是文件丢失、导入失败、数据损坏、磁盘爆满。
参考资料
Google Guava API: com.google.common.io.Files.
Google Guava API: ByteSource.
Google Guava API: CharSource.
Google Guava API: ByteSink.
Google Guava API: CharSink.
Google Guava API: LineProcessor.
Google Guava API: Hashing.
Java Documentation: java.io.File.
Java Documentation: java.nio.file.Files.
Java Documentation: java.nio.file.Path.
Java Documentation: StandardCharsets.
启示录 富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。