此文是系列文章第十二篇,前几篇请点击链接查看
程序员的福音 - Apache Commons Compress
程序员的福音 - Apache Commons Collections
程序员的福音 - Apache Commons HttpClient
程序员的福音 - Apache Commons VFS(上)
Apache Commons VFS 为访问各种不同的文件系统提供了一个统一API。支持本地磁盘、HTTP服务器、FTP服务器、HDFS文件系统、ZIP压缩包等,支持自行扩展存储客户端。
上一篇我们主要介绍了 VFS 的整体结构和使用方法,本篇我将给大家介绍下在 VFS 接口的基础上扩展自己的文件系统客户端程序。
01. 扩展机制
Commons VFS 内部包含 providers.xml 配置文件,里面配置了若干现成的 provider,在类路径下创建"META-INF/vfs-providers.xml"文件,可以添加额外的配置。可以配置自己的实现,如亚马逊 S3 对象存储客户端。
配置文件是XML文件。配置文件的根元素是<providers>元素。<providers>元素可能包含:
零个或多个<provider>元素。
可选的<default provider>元素。
零个或多个<extension map>元素。
零个或多个<mime type map>元素。
下面是一个配置文件示例:
<providers>
<provider class-name="org.apache.commons.vfs2.provider.zip.ZipFileProvider">
<scheme name="zip"/>
</provider>
<extension-map extension="zip" scheme="zip"/>
<mime-type-map mime-type="application/zip" scheme="zip"/>
<provider class-name="org.apache.commons.vfs2.provider.ftp.FtpFileProvider">
<scheme name="ftp"/>
<if-available class-name="org.apache.commons.net.ftp.FTPFile"/>
</provider>
<default-provider class-name="org.apache.commons.vfs2.provider.url.UrlFileProvider"/>
</providers>
<provider>:元素定义了一个文件提供者。它必须具有 class name 属性,该属性指定提供程序类的完全限定名。provider 类必须是 public 的,并且必须有一个带有 FileSystemManager 参数的公共构造函数,该参数允许系统传递所使用的文件系统管理器。
<provider>元素可以包含零个或多个<scheme>元素,以及零个或多个<if-available>元素。<scheme>元素定义提供者将处理的 URI 协议。它必须有一个 name 属性,用于指定 URI 协议。<if-available>元素用于禁用提供程序。它必须具有 class name 属性,该属性指定要测试的类的完全限定名。如果找不到类,则不注册 provider 程序。
<default-provider>:元素定义默认提供者。它的格式与<provider>元素相同。
<extension-map>:元素定义了从文件扩展名到具有该扩展名的 provider 的映射。它必须有一个 extension 属性(指定扩展)和一个 scheme 属性(指定提供程序的 URI 方案)。
<mime-map>:元素定义了从文件的 mime 类型到应该处理该 mime 类型文件的提供程序的映射。它必须有一个 mime type 属性(指定 mime 类型)和一个 scheme 属性(指定提供程序的 URI 方案)。
StandardFileSystemManager 的 init 方法在加载自带的 providers.xml 后还会加载自定义配置。以下是他的源代码
@Override
public void init() throws FileSystemException {
// Set the replicator and temporary file store (use the same component)
final DefaultFileReplicator replicator = createDefaultFileReplicator();
setReplicator(new PrivilegedFileReplicator(replicator));
setTemporaryFileStore(replicator);
if (configUri == null) {
// Use default config
final URL url = getClass().getResource(CONFIG_RESOURCE);
FileSystemException.requireNonNull(url, "vfs.impl/find-config-file.error", CONFIG_RESOURCE);
configUri = url;
}
// 配置自带的providers.xml
configure(configUri);
// 配置自定义的 vfs-providers.xml, 如果存在
configurePlugins();
// Initialize super-class
super.init();
}
// 类路径如果存在多个vfs-providers.xml则依次解析加载
protected void configurePlugins() throws FileSystemException {
final Enumeration<URL> enumResources;
try {
enumResources = enumerateResources(PLUGIN_CONFIG_RESOURCE);
} catch (final IOException e) {
throw new FileSystemException(e);
}
while (enumResources.hasMoreElements()) {
// 解析xml配置并创建provider等对象
configure(enumResources.nextElement());
}
}
后面就是解析 xml 文件并根据配置情况将 provider 类放入一个 providers 属性中,providers 是一个HashMap
/**
* Mapping from URI scheme to FileProvider.
*/
private final Map<String, FileProvider> providers = new HashMap<>();
02. 扩展步骤
下面我用一个例子介绍下扩展的步骤。
例子是实现一个 S3 存储服务的客户端,S3 是需要花钱去购买。我们可以使用 minio 来代替。minio 是一款开源的对象存储服务器,兼容亚马逊的S3协议。访问 http://www.minio.org.cn/download.shtml#/linux 下载 minio 服务器二进制程序,由于我们只是本机测试,所以直接用最简单的方式启动一个单机版的 minio,使用以下命令启动
export MINIO_ACCESS_KEY=minio
export MINIO_SECRET_KEY=123456
./minio server /mnt/data
这样我们就启动好了,浏览器访问 http://localhost:9000/ 用户名是 minio,密码 123456,能访问能登录成功则说明启动成功。
手动创建一个 bucket,名称为 bucket。
由于我们使用的minio,需要引入 Java 的 minio-client 依赖。依赖如下:
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.3.0</version>
</dependency>
由于需要存在 minio-client 依赖才加载我们的 provider,所以需要配置 if-avalilble,自定义 vfs-providers.xml 如下:
<providers>
<provider class-name="com.example.vfs.s3.S3FileProvider">
<scheme name="minio"/>
<scheme name="s3"/>
<if-available class-name="io.minio.MinioClient"/>
<if-available class-name="io.minio.ObjectArgs"/>
</provider>
</providers>
VFS 获取文件大致流程如下:
FileSystemManager 解析文件名,通过文件名中的协议(如ftp://中的ftp)获取对应 FileProvider 对象,FileProvider 通过 FileNameParser 对象解析文件名获取对应的 FileSystem 对象,通过 FileSystem 对象的 resolveFile 方法获取文件对象 FileObject(默认先从缓存中查找,不存在再调用 createFile 方法创建 FileObject 对象,FileObject 就是实体文件的抽象,提供读取和修改等相关能力)
FileSystemManager fsMgr = VFS.getManager();
// s3是协议名
String path = "s3://[IP]:9000/bucket/path/a.txt";
FileObject fo = fsMgr.resolveFile(path);
我们主要关注 resolveFile 方法,以下是 VFS 的部分源码
@Override
public FileObject resolveFile(String uri) throws FileSystemException {
return resolveFile(getBaseFile(), uri);
}
@Override
public FileObject resolveFile(FileObject baseFile, String uri)
throws FileSystemException {
return resolveFile(baseFile, uri,
baseFile == null ? null : baseFile.getFileSystem().getFileSystemOptions());
}
public FileObject resolveFile(FileObject baseFile, String uri,
FileSystemOptions fileSystemOptions)
final FileObject realBaseFile;
if (baseFile != null && VFS.isUriStyle() && baseFile.getName().isFile()) {
realBaseFile = baseFile.getParent();
} else {
realBaseFile = baseFile;
}
// decode url, 如果不合法则抛出异常
UriParser.checkUriEncoding(uri);
if (uri == null) {
throw new IllegalArgumentException();
}
// 提取scheme,本例就是"s3"
String scheme = UriParser.extractScheme(getSchemes(), uri);
if (scheme != null) {
// 通过scheme获取注册的FileProvider
// 此处获取的就是我们配置文件注册的 com.example.vfs.s3.S3FileProvider
FileProvider provider = providers.get(scheme);
if (provider != null) {
// provider 入口方法,下面主要看下这里的逻辑
return provider.findFile(realBaseFile, uri, fileSystemOptions);
}
// ... ... 省略其他逻辑
}
}
我们主要关注 32 行 provider.findFile() 方法,此处就是我们定义的 S3FileProvider 了,其中 findFile 是父类 AbstractOriginatingFileProvider 中的方法,以下是其部分源码
/**
* Locates a file object, by absolute URI.
*
* @param baseFileObject The base file object.
* @param uri The URI of the file to locate
* @param fileSystemOptions The FileSystem options.
* @return The located FileObject
* @throws FileSystemException if an error occurs.
*/
@Override
public FileObject findFile(FileObject baseFileObject, String uri, FileSystemOptions fileSystemOptions)
throws FileSystemException {
// Parse the URI
final FileName name;
try {
// 解析uri获取FileName
name = parseUri(baseFileObject != null ? baseFileObject.getName() : null, uri);
} catch (final FileSystemException exc) {
throw new FileSystemException("vfs.provider/invalid-absolute-uri.error", uri, exc);
}
// Locate the file
return findFile(name, fileSystemOptions);
}
/**
* Locates a file from its parsed URI.
*
* @param fileName The file name.
* @param fileSystemOptions FileSystem options.
* @return A FileObject associated with the file.
* @throws FileSystemException if an error occurs.
*/
protected FileObject findFile(FileName fileName, FileSystemOptions fileSystemOptions)
throws FileSystemException {
// Check in the cache for the file system
FileName rootName = getContext().getFileSystemManager().resolveName(fileName, FileName.ROOT_PATH);
FileSystem fs = getFileSystem(rootName, fileSystemOptions);
// Locate the file
// 此处会调用FileSystem的createFile方法
return fs.resolveFile(fileName);
}
/**
* Returns the FileSystem associated with the specified root.
*
* @param rootFileName The root path.
* @param fileSystemOptions The FileSystem options.
* @return The FileSystem.
* @throws FileSystemException if an error occurs.
* @since 2.0
*/
protected synchronized FileSystem getFileSystem(FileName rootFileName, final FileSystemOptions fileSystemOptions)
throws FileSystemException {
// 先从缓存中取
FileSystem fs = findFileSystem(rootFileName, fileSystemOptions);
if (fs == null) {
// Need to create the file system, and cache it
// doCreateFileSystem是抽象方法,需要我们去实现的
fs = doCreateFileSystem(rootFileName, fileSystemOptions);
addFileSystem(rootFileName, fs);
}
return fs;
}
上述源码 59 行的 doCreateFileSystem 方法是抽象方法,需要我们的 provider 去实现的,下面看看我们自己的 S3FileProvider 实现类
public class S3FileProvider extends AbstractOriginatingFileProvider {
public static final UserAuthenticationData.Type[] AUTHENTICATOR_TYPES = new UserAuthenticationData.Type[] {
UserAuthenticationData.USERNAME, UserAuthenticationData.PASSWORD };
// 支持的能力
static final Collection<Capability> CAPABILITIES = Collections
.unmodifiableCollection(Arrays.asList(Capability.GET_TYPE, Capability.READ_CONTENT,
Capability.CREATE,
Capability.DELETE,
Capability.RENAME,
Capability.WRITE_CONTENT,
Capability.URI, Capability.GET_LAST_MODIFIED,
Capability.SET_LAST_MODIFIED_FILE,
Capability.LIST_CHILDREN));
public S3FileProvider() {
// 使用自己的FileNameParser
this.setFileNameParser(S3FileNameParser.getInstance());
}
/**
* S3FileSystem创建关键方法
*/
@Override
protected FileSystem doCreateFileSystem(FileName rootName, FileSystemOptions fileSystemOptions) throws FileSystemException {
UserAuthenticationData authData = UserAuthenticatorUtils.authenticate(fileSystemOptions, AUTHENTICATOR_TYPES);
MinioClient client = createClient((GenericFileName) rootName, authData);
// S3FileSystem,依赖 MinioClient
return new S3FileSystem((GenericFileName) rootName, client, fileSystemOptions);
}
private MinioClient createClient(GenericFileName rootName, UserAuthenticationData authData) {
String accessKey = new String(UserAuthenticatorUtils.getData(authData, USERNAME, UserAuthenticatorUtils.toChar(rootName.getUserName())));
String secretKey = new String(UserAuthenticatorUtils.getData(authData, PASSWORD, UserAuthenticatorUtils.toChar(rootName.getUserName())));
MinioClient client = new MinioClient.Builder()
.endpoint("http://" + rootName.getHostName() + ":" + rootName.getPort() + "/")
.credentials(accessKey, secretKey)
.build();
return client;
}
@Override
public Collection<Capability> getCapabilities() {
return CAPABILITIES;
}
}
上述代码 17 行设置我们自己的 S3FileNameParser 用于解析 S3 的 uri,23 行的 doCreateFileSystem 方法返回我们自己定义的 S3FileSystem,其中依赖 MinioClient。接下来就是 S3FileSystem 类了
public class S3FileSystem extends AbstractFileSystem {
private MinioClient client;
protected S3FileSystem(GenericFileName rootFileName, MinioClient client, FileSystemOptions fileSystemOptions) {
super(rootFileName, (FileObject)null, fileSystemOptions);
this.client = client;
}
/**
* 创建 FileObject 关键方法
* FileSystem.resolveFile() 方法会调用此函数
* @return S3FileObject
*/
@Override
protected FileObject createFile(AbstractFileName name) throws Exception {
return new S3FileObject((S3FileName) name, this);
}
@Override
protected void addCapabilities(Collection<Capability> caps) {
caps.addAll(S3FileProvider.CAPABILITIES);
}
/**
* 获取MinioClient, 主要给 FileObject 类去调用
* @return MinioClient
*/
public MinioClient getClient() {
return client;
}
}
下面就是关键的 S3FileObject 类了,核心思想就是将所有读写请求,代理给 MinioClient
/**
* 将文件读写请求代理给MinioClient
*/
public class S3FileObject extends AbstractFileObject<S3FileSystem> {
private final S3FileName fileName;
protected S3FileObject(S3FileName name, S3FileSystem fileSystem) {
super(name, fileSystem);
this.fileName = name;
}
/**
* 写入文件
* @param bAppend 是否追加
* @return
* @throws Exception
*/
@Override
protected OutputStream doGetOutputStream(boolean bAppend) throws Exception {
if (bAppend) {
throw new FileSystemException("vfs.provider/write-append-not-supported.error", this.getName().getURI());
}
return new S3OutputStream(new File(FileUtils.getTempDirectoryPath() + "/s3", RandomStringUtils.random(10)));
}
@Override
protected InputStream doGetInputStream(int bufferSize) throws Exception {
return getObject();
}
@Override
protected long doGetContentSize() throws Exception {
return getStat().size();
}
/**
* 获取文件类型
* 目录,文件 or 不存在
*/
@Override
protected FileType doGetType() throws Exception {
try {
if (getStat() == null) {
return FileType.IMAGINARY;
}
} catch (Exception e) {
return FileType.IMAGINARY;
}
return fileName.getType();
}
private StatObjectResponse getStat() throws Exception {
try {
StatObjectArgs args = StatObjectArgs.builder().bucket(fileName.getBucketName()).object(fileName.getS3path()).build();
return getClient().statObject(args);
} catch (ErrorResponseException e) {
if (e.response().code() == 404) {
return null;
}
throw e;
}
}
private GetObjectResponse getObject() throws Exception {
GetObjectArgs args = GetObjectArgs.builder().bucket(this.fileName.getBucketName()).object(fileName.getS3path()).build();
try {
GetObjectResponse res = getClient().getObject(args);
Iterator<Pair<String, String>> ite = res.headers().iterator();
while (ite.hasNext()) {
Pair<String, String> pair = ite.next();
System.out.println(pair.toString());
}
return res;
} catch (ErrorResponseException e) {
if (e.response().code() == 404) {
throw new FileNotFoundException(fileName.getURI());
}
throw e;
}
}
/**
* 获取子文件,url字符串形式
*/
@Override
protected String[] doListChildren() throws Exception {
ListObjectsArgs args = ListObjectsArgs.builder().bucket(this.fileName.getBucketName()).prefix(fileName.getS3path()).build();
Iterator<Result<Item>> ite = getClient().listObjects(args).iterator();
List<String> result = new ArrayList<>();
while (ite.hasNext()) {
Result<Item> r = ite.next();
result.add(r.get().objectName());
}
return result.toArray(new String[0]);
}
/**
* 删除
*/
@Override
protected void doDelete() throws Exception {
RemoveObjectArgs args = RemoveObjectArgs.builder().bucket(fileName.getBucketName()).object(fileName.getS3path()).build();
getClient().removeObject(args);
}
@Override
public Path getPath() {
return null;
}
/**
* 获取子文件,FileObject形式
*/
@Override
protected FileObject[] doListChildrenResolved() throws Exception {
if (this.doGetType() != FileType.FOLDER) {
return null;
}
final String[] children = doListChildren();
final FileObject[] fo = new FileObject[children.length];
for (int i = 0; i < children.length; i++) {
String name = fileName.getRootURI() + fileName.getBucketName() + children[i];
fo[i] = getFileSystem().getFileSystemManager().resolveFile(name, getFileSystem().getFileSystemOptions());
}
return fo;
}
private MinioClient getClient() {
return getAbstractFileSystem().getClient();
}
// 自定义输出流,用于写入文件
// 在写入完成后会调用MinioClient将文件写入Minio中
private class S3OutputStream extends MonitorOutputStream {
private File tempFile;
public S3OutputStream(File tempFile) throws IOException {
super(FileUtils.openOutputStream(tempFile));
this.tempFile = tempFile;
}
@Override
protected void onClose() throws IOException {
try {
InputStream is = new FileInputStream(tempFile);
PutObjectArgs args = PutObjectArgs.builder().bucket(fileName.getBucketName()).object(fileName.getS3path()).stream(is, tempFile.length(), -1).build();
// 调用MinioClient写入服务端
ObjectWriteResponse owr = getClient().putObject(args);
if (owr.versionId() == null) {
throw new FileSystemException("vfs.provider.ftp/finish-put.error", getName());
}
} catch (Exception e) {
throw new FileSystemException("vfs.provider.ftp/finish-put.error", getName(), e);
} finally {
FileUtils.deleteQuietly(tempFile);
}
}
}
}
篇幅原因,S3FileName 和 S3FileNameParser 类的源码比较简单就不贴出来了
03. 测试
编写测试代码,注意其中的用户名密码和 IP 按照第二节安装的实际情况修改。
@Test
public void s3Test() throws IOException {
FileSystemManager fsMgr = VFS.getManager();
StaticUserAuthenticator auth = new StaticUserAuthenticator("", "username", "password");
FileSystemOptions opts = new FileSystemOptions();
DefaultFileSystemConfigBuilder.getInstance().setUserAuthenticator(opts, auth);
FileObject fo = fsMgr.resolveFile("s3://192.168.1.11:9000/bucket/files/test.txt", opts);
if (!fo.exists()) {
System.out.println("fo not exists");
return;
}
System.out.println("parent:"+fo.getParent().toString());
System.out.println("name:"+fo.getName());
System.out.println("path:"+fo.getPath());
System.out.println("pubURI:"+fo.getPublicURIString());
System.out.println("URI:"+fo.getURI().toString());
System.out.println("URL:"+fo.getURL());
boolean isFile = fo.isFile();
boolean isFolder = fo.isFolder();
// 是否符号链接
boolean isSymbolic = fo.isSymbolicLink();
boolean executable = fo.isExecutable();
boolean isHidden = fo.isHidden();
boolean isReadable = fo.isReadable();
boolean isWriteable = fo.isWriteable();
System.out.println("type:"+fo.getType());
if (fo.getType().hasChildren()) {
System.out.println("child:"+fo.getChild("child"));
System.out.println(fo.getChild("child").getContent().getSize());
System.out.println("children:"+ Arrays.toString(fo.getChildren()));
}
if (fo.getType().hasContent()) {
FileContent fc = fo.getContent();
InputStream is = fc.getInputStream();
FileUtils.copyInputStreamToFile(is, new File("/test/t.txt"));
// byte[] bytes = fc.getByteArray();
System.out.println(fc.getString("UTF-8"));
}
// if (fo.isWriteable()) {
// int suc = fo.delete(Selectors.EXCLUDE_SELF);
// System.out.println(suc);
// }
// 会同时关闭FileContent并释放FileObject
fo.close();
// 关闭文件系统,释放连接,清除缓存等
fsMgr.close();
}
04. 总结
本章主要讲解 Commons VFS 的扩展机制。如果有其他存储客户端的需求 VFS 不支持的情况可以自行扩展。这样可以使用统一的 API 实现多个文件系统的读写操作,有对应需求可以考虑扩展。
后续章节我将继续给大家介绍commons中其他好用的工具类库,期待你的关注。
网友评论