Apache Commons VFS 2.10.0 常用 API 与文件提供者详解
Apache Commons VFS 2.10.0 的 FileSystemManager、FileObject、选择器、复制、随机读、文件监听与各文件提供者的实测用法详解
Apache Commons VFS 2.10.0 常用 API 与文件提供者详解
目标:按对象模型系统梳理 Commons VFS 2.10.0 常用 API 与各文件提供者的真实用法,每个示例均在本地与内存文件系统上实测通过,已在 Ubuntu 24.04 实测通过。
一、环境信息
- 操作系统:Ubuntu 24.04.5 LTS
- 架构:x86_64
- JDK:OpenJDK 17.0.20.1(Temurin)
- 构建工具:Apache Maven 3.9.16
- 组件版本:Apache Commons VFS 2.10.0、commons-compress 1.27.1、commons-collections4 4.5.0-M3
- Maven 坐标:org.apache.commons:commons-vfs2:2.10.0
- 工作目录:/work/vfs-demo
实测环境确认:
java -version 2>&1 | head -1 && mvn -v | head -1
实测回显:
openjdk version "17.0.20.1" 2026-08-18
Apache Maven 3.9.16 (bbf2ae4d5949be9244c9c5cf4a904f1a27d2965a)
二、前置条件
- 已建好 /work/vfs-demo Maven 工程,依赖 commons-vfs2 2.10.0、commons-compress 1.27.1、commons-collections4 4.5.0-M3(工程 pom 见《快速入门与集成示例》)。
- 运行方式统一为:
mvn -q -B compile dependency:build-classpath -Dmdep.outputFile=/work/vfs-demo/cp.txt
java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -cp target/classes:$(cat cp.txt) 类名
- 本文示例运行时 target/vfs-demo-1.0.0.jar 需先 mvn package 生成(jar: 提供者示例用它作数据源)。
三、核心对象模型与 FileSystemManager
三个核心对象构成 VFS 的骨架:
| 对象 | 角色 |
|---|---|
| FileSystemManager | 管理器,按 URI scheme 路由到具体文件提供者,创建成本高,应用内单例复用 |
| FileObject | 统一文件句柄(目录与文件同类型),由管理器解析得到,缓存于 FilesCache |
| FileContent | 内容读写入口,由 FileObject.getContent() 获得 |
获取管理器与解析文件的常用形态:
FileSystemManager mgr = org.apache.commons.vfs2.VFS.getManager();
FileObject f1 = mgr.resolveFile("/work/vfs-demo/quick/hello.txt"); // 绝对路径
FileObject f2 = mgr.resolveFile("ram:///demo/ram-note.txt"); // 带 scheme
FileObject 的高频方法实测语义:
| 方法 | 语义 |
|---|---|
| exists() | 是否存在 |
| getType() | FileType:file、folder、imaginary(不存在) |
| getName().getURI() / getPath() / getBaseName() | 完整 URI、路径、文件名 |
| getParent() / getChildren() | 父目录、子项数组 |
| resolveFile(String) / resolveFile(String, NameScope) | 相对解析(可限定作用域) |
| createFolder() / createFile() / delete() | 建目录、建文件、删除 |
| copyFrom(FileObject, FileSelector) | 按选择器复制 |
| findFiles(FileSelector) | 按选择器遍历 |
| refresh() | 强制刷新缓存属性 |
| close() | 关闭 FileObject |
四、文件提供者清单与 URI 写法
💡 技术拓展:提供者安装方式
方式 说明 无条件注册 providers.xml 直接注册(file、ram、zip、jar、gz、res、tmp) if-available 条件注册 classpath 存在对应依赖才安装(tar、bz2、ftp、sftp、http、hdfs 等) addProvider 代码注册 运行期注册自定义 scheme 本文实测覆盖:file、ram、zip、tar、tgz、jar、res、tmp、自定义 mem2。
各提供者的 URI 写法(分号前为 scheme,分层提供者用 !/ 分隔内外层):
| 提供者 | URI 示例 |
|---|---|
| file | file:/work/vfs-demo/quick/hello.txt |
| ram | ram:///demo/ram-note.txt |
| zip | zip:file:/work/vfs-demo/archives/demo.zip!/ |
| tar | tar:file:/work/vfs-demo/archives/bundle.tar!/ |
| tgz | tgz:file:/work/vfs-demo/archives/bundle.tar.gz!/ |
| jar | jar:file:/work/vfs-demo/target/vfs-demo-1.0.0.jar!/ |
| res | res:/org/apache/commons/vfs2/impl/providers.xml |
| 自定义 | mem2:///custom/hello.txt(5.10 节注册) |
当前 classpath 下逐 scheme 解析实测(ProvidersProbe):
file:///work/vfs-demo/quick/hello.txt => 解析成功, 目标状态: file
ram:///probe.txt => 解析成功, 目标状态: 不存在
zip:file:/work/vfs-demo/archives/demo.zip!/ => 解析成功, 目标状态: folder
tar:file:/work/vfs-demo/archives/bundle.tar!/ => 解析成功, 目标状态: folder
tgz:file:/work/vfs-demo/archives/bundle.tar.gz!/ => 解析成功, 目标状态: folder
jar:file:/work/vfs-demo/target/vfs-demo-1.0.0.jar!/ => 解析成功, 目标状态: folder
res:/probe.txt => 失败: Badly formed URI "res:/probe.txt".
tmp:/probe.txt => 解析成功, 目标状态: 不存在
ftp://192.168.1.62/probe.txt => 失败: Could not find file with URI "ftp://192.168.1.62/probe.txt" because it is a relative path, and no base URI was provided.
sftp://192.168.1.62/probe.txt => 失败: Could not find file with URI "sftp://192.168.1.62/probe.txt" because it is a relative path, and no base URI was provided.
http://192.168.1.62/probe.txt => 失败: Could not find file with URI "http://192.168.1.62/probe.txt" because it is a relative path, and no base URI was provided.
https://192.168.1.62/probe.txt => 失败: Could not find file with URI "https://192.168.1.62/probe.txt" because it is a relative path, and no base URI was provided.
webdav://192.168.1.62/probe.txt => 失败: Could not find file with URI "webdav://192.168.1.62/probe.txt" because it is a relative path, and no base URI was provided.
验证结果与要点:
- ftp/sftp/http/https/webdav 的 "relative path" 报错等于「提供者未安装」:这些提供者按 if-available 条件注册,classpath 缺对应依赖(commons-net、jsch、httpclient5 等)时不安装。
- res: 只能解析真实存在的 classpath 资源,实测 res:/org/apache/commons/vfs2/impl/providers.xml 成功返回 file 类型。
- tmp: 指向系统临时目录空间,解析不报错但目标不存在。
五、本地与内存文件系统 API
file 与 ram 提供者的完整读写流程(QuickStart.java 运行实测):
[0] FileSystemManager 实现类 = org.apache.commons.vfs2.impl.StandardFileSystemManager
[1] 写入 file:///work/vfs-demo/quick/hello.txt 大小=77 字节 类型=file
[2] getInputStream 读到 Hello Apache Commons VFS 2.10.0
getString(UTF-8) 读到 统一 API 访问本地与内存文件系统
[3] 目录 /work/vfs-demo/quick 共 4 个子项:
b.txt 类型=file 大小=3
hello-from-ram.txt 类型=file 大小=22
hello.txt 类型=file 大小=77
hello-copy.txt 类型=file 大小=77
hello.txt 的父目录 = /work/vfs-demo/quick
[4] ram 文件 ram:///demo/ram-note.txt 大小=22 字节
ram 内容 = RAM 文件系统内容
[5] FileUtil.copyContent(ram -> 本地) 后大小 = 22 字节
[6] copyFrom(SELECT_ALL) 后 hello-copy.txt 大小=77 字节
[7] FileUtil.getContent 读出 77 字节
要点:
- 写入:fo.getContent().getOutputStream() 拿输出流;目录不存在时先 getParent().createFolder()。
- 读出:getInputStream(流式)、getString(Charset)(整串)、FileUtil.getContent(整字节数组)三种。
- 遍历:getChildren() 返回直接子项,getParent() 返回父目录。
- ram 文件生命周期与 JVM 进程一致,适合临时缓存、测试桩。
- FileUtil.copyContent 支持 ram 与本地之间的跨文件系统复制,本例 ram 22 字节完整落到本地。
六、归档提供者 zip、tar、tgz、jar
6.1 zip 的打包与读取
VFS 的 zip 文件系统是只读的,打包用 commons-compress 的 ZipArchiveOutputStream,读取用 zip: 提供者(ZipDemo.java 实测):
[1] commons-compress 打包完成 /work/vfs-demo/archives/demo.zip 大小=319 字节
[2] zip 根 zip:file:///work/vfs-demo/archives/demo.zip!/ 子项数=1
/docs 类型=folder
docs/readme.txt 内容 = zip 内的 readme 内容
docs/sub/data.csv 大小=14 字节
[3] zip 写入失败(提供者只读): FileSystemException - Could not write to "zip:file:///work/vfs-demo/archives/demo.zip!/new-in-zip.txt".
验证结果与要点:
- zip: URI 的 !/ 后是归档内路径,归档根类型为 folder,getChildren() 展开目录树。
- 向 zip 写入直接抛 FileSystemException,VFS 的 zip 提供者设计上只读;打包务必用 commons-compress(或 java.util.zip)先落盘。
- zip 读取只依赖 JDK 自带实现,无需 commons-compress;tar/bz2 才依赖它。
6.2 tar、tgz 的打包与读取
tar 打包注意条目必须先 setSize(ArchiveReadDemo.java 实测):
[1] tar 打包完成 /work/vfs-demo/archives/bundle.tar 大小=3072 字节
[2] tar.gz 打包完成 /work/vfs-demo/archives/bundle.tar.gz 大小=131 字节
[3] tar:// 根 tar:file:///work/vfs-demo/archives/bundle.tar!/
/conf 类型=folder 子项数=2
conf/app.properties 内容 = port=8080 | name=demo |
[4] tgz:// conf/app.properties 内容 = port=8080 | name=demo |
验证结果与要点:
- TarArchiveEntry 只给名字不给尺寸时默认 0,写完数据后 closeArchiveEntry 会报错,先 entry.setSize(data.length) 再 putArchiveEntry。
- tgz: 是 gz 与 tar 两层提供者的叠加(providers.xml 中 tgz 由 TarFileProvider 注册,条件是 gz 与 tar 均可用),URI 写法与 tar 相同。
- tar: 根目录同样解析为 folder,遍历与内容读取 API 与本地文件一致。
6.3 jar 的读取
jar: 提供者直接以任意 jar 为文件系统根(本例把工程自身构建产物当数据源):
[5] jar:// 根 jar:file:///work/vfs-demo/target/vfs-demo-1.0.0.jar!/ 子项数=11
/META-INF 类型=folder
/ArchiveReadDemo.class 类型=file
/RandomAccessDemo.class 类型=file
/CacheProbe.class 类型=file
/QuickStart.class 类型=file
/MonitorDemo$1.class 类型=file
/MonitorDemo.class 类型=file
/ZipDemo.class 类型=file
/SelectorDemo.class 类型=file
/ProvidersProbe.class 类型=file
/CustomSchemeDemo.class 类型=file
META-INF/MANIFEST.MF 前 3 行:
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.0
Java-Version: 17
验证结果与要点:jar 提供者同时注册了 sar、ear、par、ejb3、war 五个 scheme,Java EE 归档同样可读;MANIFEST.MF 等任意条目按 FileObject 读取。
七、遍历与选择器 API
选择器决定 findFiles/copyFrom 的处理范围。常用选择器对比:
| 选择器 | 过滤维度 | 构造示例 |
|---|---|---|
| FileTypeSelector | 文件类型 | new FileTypeSelector(FileType.FILE) |
| FileExtensionSelector | 后缀 | new FileExtensionSelector("log") |
| FileDepthSelector | 深度区间 | new FileDepthSelector(0, 0) |
| FileFilterSelector | java.io.FileFilter | new FileFilterSelector(filter) |
| AndFileFilter(filter 包) | 组合 FileFilter | new AndFileFilter(a, b) |
NameScope 限定相对解析的作用域(配合 FileObject.resolveFile):
| 取值 | 语义 |
|---|---|
| CHILD | 仅允许直接子项 |
| DESCENDENT | 允许任意后代 |
| DESCENDENT_OR_SELF | 允许后代与自身 |
| FILE_SYSTEM | 允许跨出当前文件系统根 |
实测(SelectorDemo.java,树:root 下 a.log b.log c.txt sub/d.log sub/deep/e.log sub/deep/f.txt):
[1] FileTypeSelector(FILE) 命中 6 个文件:
/work/vfs-demo/selector/b.log
/work/vfs-demo/selector/sub/d.log
/work/vfs-demo/selector/sub/deep/f.txt
/work/vfs-demo/selector/sub/deep/e.log
/work/vfs-demo/selector/a.log
/work/vfs-demo/selector/c.txt
[2] FileExtensionSelector(log) 命中 4 个:
/work/vfs-demo/selector/b.log
/work/vfs-demo/selector/sub/d.log
/work/vfs-demo/selector/sub/deep/e.log
/work/vfs-demo/selector/a.log
[3] FileDepthSelector(0, 0) 命中 1 个:
/work/vfs-demo/selector 类型=folder
[4] FileFilterSelector(首字母 a) 命中 1 个:
/work/vfs-demo/selector/a.log
[5] sub.resolveFile(d.log, CHILD) = /work/vfs-demo/selector/sub/d.log
sub.resolveFile(deep/e.log, CHILD) 被拒绝: Invalid descendent file name "deep/e.log".
[6] FileFilterSelector(文件 且 *.txt) 命中 1 个:
/work/vfs-demo/selector/c.txt
验证结果与要点:
- FileTypeSelector(FILE) 默认递归整棵树;FileDepthSelector(0, 0) 只命中根自身(folder),因为深度 0 是起点目录,只取第一层内容应遍历根的 getChildren()。
- NameScope.CHILD 对孙级路径直接抛 FileSystemException(Invalid descendent file name),比静默越权解析安全。
- 组合过滤用 filter 包的 AndFileFilter 叠加 FileFileFilter.FILE 与 SuffixFileFilter(".txt"),二者均为 java.io.FileFilter,由 FileFilterSelector 适配成 VFS 选择器。
- findFiles 返回 FileObject[],遍历顺序不保证有序,需展示时自行按名字排序。
八、内容读写与复制 API
内容读写与复制的推荐入口:
| API | 用途 | 备注 |
|---|---|---|
| fo.getContent().getOutputStream() | 写内容 | 需 try-with-resources |
| fo.getContent().getInputStream() | 流式读 | 同上 |
| fo.getContent().getString(Charset) | 整串读 | 一次性载入内存 |
| FileUtil.getContent(fo) | 整字节数组读 | 静态方法,内部处理关闭 |
| FileUtil.copyContent(src, dst) | 内容复制 | 跨文件系统可用 |
| dst.copyFrom(src, selector) | 结构复制 | 支持目录树与选择器 |
随机读(RandomAccessDemo.java,文件内容 0123456789ABCDEFGHIJ):
RandomAccessContent rac = fo.getContent().getRandomAccessContent(org.apache.commons.vfs2.util.RandomAccessMode.READ);
[1] 打开随机读: 内容长度=21 字节, 初始指针=0
[2] seek(5) + readFully(5) = 56789, 指针移动到 10
[3] seek(0) + readFully(全部) = 0123456789ABCDEFGHIJ
[3a] readLine() 在 2.10.0 已废弃, 直接抛出: Deprecated
[4] seek(15) + read() = F (ASCII 70)
[5] 在文件末尾继续读抛出 EOFException
[6] RandomAccessContent 已关闭
验证结果与要点:
- RandomAccessContent 扩展 DataInput/DataOutput,seek/readFully/readUnsignedByte/length/getFilePointer 均可用。
- readLine() 在 2.10.0 已废弃且直接抛 UnsupportedOperationException,勿再使用。
- RandomAccessMode.READ 打开后写入方法被禁用,防止误写。
九、文件监听 DefaultFileMonitor
DefaultFileMonitor 以后台线程轮询感知文件变化,事件回调 FileListener 的三个方法。实测(MonitorDemo.java,delay=300ms 递归监控目录):
[0] 监控参数 delay=300ms recursive=true
[1] DefaultFileMonitor 已启动, 监控目录 /work/vfs-demo/monitor
[2] 启动观察期结束(上方若无事件输出, 说明启动时对已存在文件 pre.txt 不误报)
[监听] 创建: /work/vfs-demo/monitor/notes.txt
[监听] 修改: /work/vfs-demo/monitor/notes.txt
[监听] 删除: /work/vfs-demo/monitor/notes.txt
[3] DefaultFileMonitor 已停止
验证结果与要点:
- 创建、修改、删除三类事件在一次运行内全部触发,间隔 300ms 感知迅速。
- 启动时目录里已存在的 pre.txt 未产生任何事件,即 addFile 时已存在的对象不触发创建事件。
- 建议监听目录而非单文件:新建文件能被稳定捕获;单文件模式下 addFile 前文件已存在则只有修改与删除事件。
- monitor.stop() 结束后台线程;回调中不要做耗时操作。
十、自定义 scheme 注册 addProvider
StandardFileSystemManager.addProvider(scheme, FileProvider) 可注册自定义前缀。实测复用 RamFileProvider 注册 mem2(CustomSchemeDemo.java):
[1] mem2 文件 mem2:///custom/hello.txt 大小=39 字节
内容 = 自定义 scheme mem2 复用 RAM 实现
[2] 再次解析 mem2:///custom/hello.txt 大小=39 字节
[3] 默认 VFS.getManager() 不识别 mem2: Could not find file with URI "mem2:///x.txt" because it is a relative path, and no base URI was provided.
验证结果与要点:
- addProvider 在 init() 前调用;注册后 mem2:/// 与 ram:/// 完全同效,可重复解析。
- 注册只对当前实例生效,VFS.getManager() 单例不受污染,可按业务隔离出不同 scheme 语义。
- 除注册新前缀,还可 addProvider(String[] schemes, FileProvider) 批量注册、setDefaultProvider(FileProvider) 更换兜底。
十一、参考链接
- Apache Commons VFS 官网:https://commons.apache.org/proper/commons-vfs/
- VFS Javadoc(FileObject):https://commons.apache.org/proper/commons-vfs/apidocs/org/apache/commons/vfs2/FileObject.html
- VFS Javadoc(FileUtil):https://commons.apache.org/proper/commons-vfs/apidocs/org/apache/commons/vfs2/FileUtil.html
- VFS 用户指南:https://commons.apache.org/proper/commons-vfs/userguide.html
- Maven Central 构件页:https://repo1.maven.org/maven2/org/apache/commons/commons-vfs2/