美文网首页
程序员的福音 - Apache Commons VFS(下)

程序员的福音 - Apache Commons VFS(下)

作者: 菜鸟码农的Java笔记 | 来源:发表于2021-08-29 21:02 被阅读0次

    此文是系列文章第十二篇,前几篇请点击链接查看

    程序猿的福音 - Apache Commons简介

    程序员的福音 - Apache Commons Lang

    程序员的福音 - Apache Commons IO

    程序员的福音 - Apache Commons Codec

    程序员的福音 - Apache Commons Compress

    程序员的福音 - Apache Commons Exec

    程序员的福音 - Apache Commons Email

    程序员的福音 - Apache Commons Net

    程序员的福音 - 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中其他好用的工具类库,期待你的关注。

    相关文章

      网友评论

          本文标题:程序员的福音 - Apache Commons VFS(下)

          本文链接:https://www.haomeiwen.com/subject/xkugiltx.html