Apache Commons VFS 2.10.0 快速入门与集成示例
Apache Commons VFS 2.10.0 的 10 分钟最小可用工程、Maven 集成常见坑、与 vfs1 同 classpath 共存实测及选型建议
Apache Commons VFS 2.10.0 快速入门与集成示例
目标:10 分钟完成 Commons VFS 2.10.0 的最小可用工程,跑通本地文件、ram 内存文件系统与跨文件系统复制,并掌握 Maven 集成常见坑,已在 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
实测环境确认:
head -2 /etc/os-release && java -version 2>&1 | head -1 && mvn -v | head -1
实测回显:
PRETTY_NAME="Ubuntu 24.04.5 LTS"
NAME="Ubuntu"
openjdk version "17.0.20.1" 2026-08-18
Apache Maven 3.9.16 (bbf2ae4d5949be9244c9c5cf4a904f1a27d2965a)
二、10 分钟最小可用工程
2.1 引入依赖
Commons VFS 的定位是虚拟文件系统门面:用同一套 FileSystemManager 与 FileObject API 访问本地磁盘、内存文件系统与各类归档。创建 /work/vfs-demo/pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>vfs-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-vfs2</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.27.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.5.0-M3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
</plugins>
</build>
</project>
依赖说明:
- commons-vfs2 必需,自动带入 commons-logging 1.3.4、commons-lang3 3.17.0、commons-io 2.18.0。
- commons-compress 用于 tar/bz2 提供者与 zip 打包(commons-vfs2 2.10.0 的 tar 提供者按 if-available 条件注册)。
- commons-collections4 仅在使用 LRUFilesCache 缓存实现时需要(见 3.3 节)。
- 固定 maven-compiler-plugin 3.11.0,避免默认插件按 Java 5 语法级别编译。
💡 技术拓展:VFS 的三个核心对象
对象 角色 常用入口 FileSystemManager 虚拟文件系统管理器,负责按 URI scheme 路由到具体提供者 VFS.getManager() FileObject 统一文件句柄,目录与文件都是它 mgr.resolveFile("ram:///a.txt") FileContent 文件内容读写入口 fo.getContent().getInputStream() 本文全程使用这三个对象;schema 前缀决定后端,例如 file:(本地)、ram:(内存)、zip:(归档)。
2.2 最小验证程序
创建 /work/vfs-demo/src/main/java/QuickStart.java:
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.VFS;
import org.apache.commons.vfs2.FileUtil;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class QuickStart {
public static void main(String[] args) throws Exception {
FileSystemManager mgr = VFS.getManager();
System.out.println("[0] FileSystemManager 实现类 = " + mgr.getClass().getName());
// 1. resolveFile 定位本地文件并写入内容
FileObject readme = mgr.resolveFile("/work/vfs-demo/quick/hello.txt");
readme.getParent().createFolder();
try (OutputStream out = readme.getContent().getOutputStream()) {
out.write("Hello Apache Commons VFS 2.10.0\n统一 API 访问本地与内存文件系统\n".getBytes(StandardCharsets.UTF_8));
}
System.out.println("[1] 写入 " + readme.getName().getURI() + " 大小=" + readme.getContent().getSize() + " 字节 类型=" + readme.getType());
// 2. 两种读法:getInputStream 与 getString
String viaStream;
try (java.io.InputStream in = readme.getContent().getInputStream()) {
viaStream = new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
System.out.println("[2] getInputStream 读到 " + viaStream.split("\n")[0]);
System.out.println(" getString(UTF-8) 读到 " + readme.getContent().getString(StandardCharsets.UTF_8).split("\n")[1]);
// 3. 层级遍历 getChildren / getParent
FileObject dir = readme.getParent();
FileObject b = dir.resolveFile("b.txt");
try (OutputStream out = b.getContent().getOutputStream()) {
out.write("BBB".getBytes(StandardCharsets.UTF_8));
}
System.out.println("[3] 目录 " + dir.getName().getPath() + " 共 " + dir.getChildren().length + " 个子项:");
for (FileObject child : dir.getChildren()) {
String size = child.getType().hasContent() ? String.valueOf(child.getContent().getSize()) : "-";
System.out.println(" " + child.getName().getBaseName() + " 类型=" + child.getType() + " 大小=" + size);
}
System.out.println(" hello.txt 的父目录 = " + readme.getParent().getName().getPath());
// 4. ram:// 内存文件系统读写
FileObject ram = mgr.resolveFile("ram:///demo/ram-note.txt");
ram.getParent().createFolder();
try (OutputStream out = ram.getContent().getOutputStream()) {
out.write("RAM 文件系统内容".getBytes(StandardCharsets.UTF_8));
}
System.out.println("[4] ram 文件 " + ram.getName().getURI() + " 大小=" + ram.getContent().getSize() + " 字节");
System.out.println(" ram 内容 = " + ram.getContent().getString(StandardCharsets.UTF_8));
// 5. 跨文件系统复制:ram -> 本地
FileObject localCopy = mgr.resolveFile("/work/vfs-demo/quick/hello-from-ram.txt");
FileUtil.copyContent(ram, localCopy);
System.out.println("[5] FileUtil.copyContent(ram -> 本地) 后大小 = " + localCopy.getContent().getSize() + " 字节");
// 6. 同文件系统复制:copyFrom + 全选选择器
FileObject localCopy2 = mgr.resolveFile("/work/vfs-demo/quick/hello-copy.txt");
localCopy2.copyFrom(readme, org.apache.commons.vfs2.Selectors.SELECT_ALL);
System.out.println("[6] copyFrom(SELECT_ALL) 后 " + localCopy2.getName().getBaseName() + " 大小=" + localCopy2.getContent().getSize() + " 字节");
// 7. FileUtil.getContent 一步读出字节数组
byte[] raw = FileUtil.getContent(readme);
System.out.println("[7] FileUtil.getContent 读出 " + raw.length + " 字节");
}
}
2.3 编译与运行
cd /work/vfs-demo
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) QuickStart
实测回显:
[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 字节
验证结果:本地文件写入 77 字节、两种方式读回内容一致;目录遍历列出 4 个子项(本次为重复运行,hello-from-ram.txt 与 hello-copy.txt 是前次运行留下的产物,首次运行此处为 2 个子项);ram 内存文件与跨文件系统复制全部成功。
2.4 要点提示
- resolveFile 接受绝对 URI,也接受相对路径(相对管理器的 base file)。
- 目录与文件都是 FileObject,用 getType() 区分(file/folder/imaginary)。
- 写入通过 fo.getContent().getOutputStream(),读出可选 getInputStream、getString、FileUtil.getContent 三种方式。
- copyFrom(源, 选择器) 是 2.x 的复制 API,选择器决定复制哪些子项,Selectors.SELECT_ALL 表示全量。
三、Maven 工程常见坑
3.1 缺 commons-compress 时 tar 提供者未安装
VFS 的 tar、bz2、tbz2、ftp、sftp、http 等提供者在内置配置文件 providers.xml 中以 if-available 条件注册:classpath 上有对应依赖才安装。缺少 commons-compress 时解析 tar:
VFS.getManager().resolveFile("tar:file:/work/vfs-demo/archives/bundle.tar!/");
实测回显:
tar 解析失败: Could not find file with URI "tar:file:/work/vfs-demo/archives/bundle.tar!/" because it is a relative path, and no base URI was provided.
原因:提供者未安装时,管理器把 tar: 前缀当作相对路径处理,报错信息不直观。引入 org.apache.commons:commons-compress 后恢复。
3.2 zip 读取与写入的依赖要求不同
zip 提供者基于纯 JDK 实现注册(providers.xml 中无 if-available 条件),仅读 zip 不需要 commons-compress。实测在只有 vfs2、commons-logging、commons-lang3、commons-io 四个 jar 的 classpath 上解析 zip:
zip 解析成功
但用 VFS API 直接打包/写 zip 时会用到 commons-compress 的类;自己引入 ZipArchiveOutputStream 打包(VFS 的 zip 文件系统为只读,写入会失败)时更必须引入。实测向已有 zip 写入:
[3] zip 写入失败(提供者只读): FileSystemException - Could not write to "zip:file:///work/vfs-demo/archives/demo.zip!/new-in-zip.txt".
3.3 LRUFilesCache 缺 commons-collections4 直接崩溃
使用 org.apache.commons.vfs2.cache.LRUFilesCache 作为 FilesCache 时,若 classpath 缺 commons-collections4:
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/collections4/MapIterator
at org.apache.commons.vfs2.cache.LRUFilesCache.<init>(LRUFilesCache.java:69)
at CacheProbe.main(CacheProbe.java:45)
原因:LRUFilesCache 基于 commons-collections4 的 MultiMap 实现,而 commons-collections4 是 commons-vfs2 的 optional 依赖,Maven 不会自动带入。引入 commons-collections4 4.5.0-M3 后实测正常:
[4] LRUFilesCache(64) + ON_CALL 读取 = LRU-DEMO 大小=8 字节
3.4 2.x 真实 API 命名纠错
网上旧示例常带 1.x 习惯或想象中的 API。以下写法在 2.10.0 编译失败(实测):
import org.apache.commons.vfs2.filter.FileFilterSelector;
fo.copyTo(fo);
fo.resolveFile("x", NameScope.CHILDREN);
new FileFilterSelector(null);
BadApi.java:3: error: cannot find symbol
import org.apache.commons.vfs2.filter.FileFilterSelector;
^
symbol: class FileFilterSelector
location: package org.apache.commons.vfs2.filter
BadApi.java:7: error: cannot find symbol
fo.copyTo(fo);
^
symbol: method copyTo(FileObject)
location: variable fo of type FileObject
BadApi.java:8: error: cannot find symbol
fo.resolveFile("x", NameScope.CHILDREN);
^
symbol: variable CHILDREN
location: class NameScope
BadApi.java:9: error: cannot find symbol
new FileFilterSelector(null);
^
symbol: class FileFilterSelector
location: class BadApi
4 errors
对照的正确写法:
| 错误写法 | 2.10.0 正确写法 |
|---|---|
| fo.copyTo(dest) | dest.copyFrom(src, Selectors.SELECT_ALL) |
| NameScope.CHILDREN | NameScope.CHILD(其余取值 DESCENDENT、DESCENDENT_OR_SELF、FILE_SYSTEM) |
| org.apache.commons.vfs2.filter.FileFilterSelector | org.apache.commons.vfs2.FileFilterSelector(位于根包) |
| info.getName()(FileSelectInfo) | info.getFile().getName() |
3.5 中文输出乱码
POSIX locale 服务器(mvn -version 显示 platform encoding: ANSI_X3.4-1968)下,中文 System.out 输出会变问号。实测某 Ubuntu 22.04 环境不带编码参数运行:
????: file:///root/vfs-work/check.txt ??=32 ??
????: ram:///check.txt ??=RAM OK
解决:运行参数追加 -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8;JDK 18 起 System.out 编码独立于 file.encoding,两者都带上最稳。
四、vfs1 与 vfs2 同 classpath 共存实测
历史遗留工程可能同时引用两代构件。两者 groupId/artifactId/包名完全不同(commons-vfs:commons-vfs 的包是 org.apache.commons.vfs;org.apache.commons:commons-vfs2 的包是 org.apache.commons.vfs2),类加载互不冲突。下载 vfs 1.0 后实测:
curl -sO https://repo1.maven.org/maven2/commons-vfs/commons-vfs/1.0/commons-vfs-1.0.jar
创建 /work/vfs-demo/src/main/java/CoexistDemo.java:
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.VFS;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class CoexistDemo {
public static void main(String[] args) throws Exception {
FileSystemManager m2 = VFS.getManager();
org.apache.commons.vfs.FileSystemManager m1 = org.apache.commons.vfs.VFS.getManager();
System.out.println("[1] vfs1 管理器 = " + m1.getClass().getName());
System.out.println(" vfs2 管理器 = " + m2.getClass().getName());
// 同一个本地文件,两代 API 各读一次
FileObject f2 = m2.resolveFile("/work/vfs-demo/quick/hello.txt");
org.apache.commons.vfs.FileObject f1 = m1.resolveFile("/work/vfs-demo/quick/hello.txt");
System.out.println("[2] vfs1 读 " + f1.getName().getURI() + " 大小=" + f1.getContent().getSize() + " 字节");
System.out.println(" vfs2 读 " + f2.getName().getURI() + " 大小=" + f2.getContent().getSize() + " 字节");
// 各自的 ram 文件系统互不相通(各自独立实例)
org.apache.commons.vfs.FileObject r1 = m1.resolveFile("ram:///coexist.txt");
try (OutputStream out = r1.getContent().getOutputStream()) {
out.write("written by vfs1".getBytes(StandardCharsets.UTF_8));
}
FileObject r2 = m2.resolveFile("ram:///coexist.txt");
System.out.println("[3] vfs1 写 ram:///coexist.txt 后, vfs2 读取该路径 exists=" + r2.exists());
System.out.println(" vfs1 读回 ram:///coexist.txt 内容=" + new String(r1.getContent().getInputStream().readAllBytes(), StandardCharsets.UTF_8));
}
}
javac -encoding UTF-8 -cp commons-vfs2-2.10.0.jar:commons-logging-1.3.4.jar:commons-lang3-3.17.0.jar:commons-io-2.18.0.jar:commons-vfs-1.0.jar -d target/coexist-classes src/main/java/CoexistDemo.java
java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -cp target/coexist-classes:commons-vfs2-2.10.0.jar:commons-logging-1.3.4.jar:commons-lang3-3.17.0.jar:commons-io-2.18.0.jar:commons-vfs-1.0.jar CoexistDemo
实测回显:
编译成功
Oct 15, 2026 1:21:44 AM org.apache.commons.vfs.impl.DefaultFileReplicator info
INFO: Using "/tmp/vfs_cache" as temporary files store.
[1] vfs1 管理器 = org.apache.commons.vfs.impl.StandardFileSystemManager
vfs2 管理器 = org.apache.commons.vfs2.impl.StandardFileSystemManager
[2] vfs1 读 file:///work/vfs-demo/quick/hello.txt 大小=77 字节
vfs2 读 file:///work/vfs-demo/quick/hello.txt 大小=77 字节
[3] vfs1 写 ram:///coexist.txt 后, vfs2 读取该路径 exists=false
vfs1 读回 ram:///coexist.txt 内容=written by vfs1
验证结果:两代管理器在同一 JVM 内并存,读同一个本地文件结果一致;但各自的 ram 文件系统是独立实例,vfs1 写入的内存文件对 vfs2 不可见。共存仅是过渡手段,建议逐步把代码迁到 vfs2。
五、与 JDK NIO 等方案的关系与选型建议
VFS 不替代 JDK,而是叠加在 java.io/java.net 之上的统一抽象层。选型判断:
| 需求 | 建议 |
|---|---|
| 仅本地文件 + zip 偶尔读 | JDK NIO(Files.walk、Filesystems.newFileSystem)零依赖 |
| 需要在本地、内存、归档之间以同一套代码切换 | Commons VFS 2.10.0,schema 换个前缀即可 |
| 需要默认未带的后端(ftp/sftp/http) | Commons VFS + 对应 optional 依赖(commons-net、jsch、httpclient5) |
| 只缺文件工具方法(复制流、行处理) | Commons IO 即可,不必引入虚拟化层 |
集成建议:
- 版本锁定 2.10.0 并用 dependency:tree 确认传递依赖(commons-logging 1.3.4、commons-lang3 3.17.0、commons-io 2.18.0)。
- 按需追加 optional 依赖,避免一次拉全:tar/bz2 加 commons-compress 1.27.1,LRUFilesCache 加 commons-collections4 4.5.0-M3。
- FileSystemManager 创建成本高,应用生命周期内用 VFS.getManager() 单例;需要定制缓存或注册自定义 scheme 时再单独实例化 StandardFileSystemManager。
六、参考链接
- Apache Commons VFS 官网:https://commons.apache.org/proper/commons-vfs/
- VFS 用户指南:https://commons.apache.org/proper/commons-vfs/userguide.html
- Maven Central 构件页:https://repo1.maven.org/maven2/org/apache/commons/commons-vfs2/
- commons-compress 构件页:https://repo1.maven.org/maven2/org/apache/commons/commons-compress/
- VFS 2.10.0 Javadoc:https://commons.apache.org/proper/commons-vfs/apidocs/index.html