Apache Commons VFS 2.10.0 快速入门与集成示例

Apache Commons VFS 2.10.0 的 10 分钟最小可用工程、Maven 集成常见坑、与 vfs1 同 classpath 共存实测及选型建议

Apache Commons VFSApache Commons VFS2026/10/15

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.CHILDRENNameScope.CHILD(其余取值 DESCENDENT、DESCENDENT_OR_SELF、FILE_SYSTEM)
org.apache.commons.vfs2.filter.FileFilterSelectororg.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 即可,不必引入虚拟化层

集成建议:

  1. 版本锁定 2.10.0 并用 dependency:tree 确认传递依赖(commons-logging 1.3.4、commons-lang3 3.17.0、commons-io 2.18.0)。
  2. 按需追加 optional 依赖,避免一次拉全:tar/bz2 加 commons-compress 1.27.1,LRUFilesCache 加 commons-collections4 4.5.0-M3。
  3. FileSystemManager 创建成本高,应用生命周期内用 VFS.getManager() 单例;需要定制缓存或注册自定义 scheme 时再单独实例化 StandardFileSystemManager。

六、参考链接