Apache Commons VFS 2.10.0 常用 API 与文件提供者详解

Apache Commons VFS 2.10.0 的 FileSystemManager、FileObject、选择器、复制、随机读、文件监听与各文件提供者的实测用法详解

Apache Commons VFSApache Commons VFS2026/10/15

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)

二、前置条件

  1. 已建好 /work/vfs-demo Maven 工程,依赖 commons-vfs2 2.10.0、commons-compress 1.27.1、commons-collections4 4.5.0-M3(工程 pom 见《快速入门与集成示例》)。
  2. 运行方式统一为:
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) 类名
  1. 本文示例运行时 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 示例
filefile:/work/vfs-demo/quick/hello.txt
ramram:///demo/ram-note.txt
zipzip:file:/work/vfs-demo/archives/demo.zip!/
tartar:file:/work/vfs-demo/archives/bundle.tar!/
tgztgz:file:/work/vfs-demo/archives/bundle.tar.gz!/
jarjar:file:/work/vfs-demo/target/vfs-demo-1.0.0.jar!/
resres:/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)
FileFilterSelectorjava.io.FileFilternew FileFilterSelector(filter)
AndFileFilter(filter 包)组合 FileFilternew 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) 更换兜底。

十一、参考链接