Refresh your Java skills--聊聊Java9 中模块化所基于的文件系统 JRTFS

380 阅读9分钟
原文链接: zhuanlan.zhihu.com

说到文件系统我们很容易就想到Linux,windows操作系统的文件系统,对应到我们的生活中,我们想去一所学校找到某个学生,假如你不了解学号所代表的意义,那就只能是一点一点的找了,不过绝对知道这个学生是几年级,然后一个班一个班的找,假如了解学号的意义的话我们就可以直接定位到哪一栋楼,哪一间教室。

说的再直白点,不就是是个找啊找啊找朋友的游戏么。这也就是我们排序查找的算法了,而面向大量有用数据最好的实践就是用树形结构来统筹,于是我们的数据库的索引,我们的zookeeper的节点管理,小到我们Java里使用的红黑树,以及对hashmap的优化等等,就是因为其复杂度可以降到最低,只需要凭借树的高度就可以快速找到我们所要找的数据了。

说了这么多,就是想要表达的是,我们的Java9中所设计的全新的JRTFS也是基于树来表达的。

文件系统的设计

我们往往会将一堆数据分析其成分,然后抽取出结构来对其组织,往往我们碰到的最多的是表结构和其数据,结构定义和数据要分开存放,这里我们首先对其进行结构的定义,接着我们要将每一份数据进行穿针引线,做成一个体系,其实就是一个索引体系,我们要做的就是对其每一个节点的管理。而最后所建立起的索引系统可以作为一个专门的文件来存放(windows系统下面的话请参照C:\Program Files\Java\jdk-9.0.1\lib\modules这个文件),我们的结构定义作为一个专门的jar文件来存放(windows系统下面的话请参照C:\Program Files\Java\jdk-9.0.1\lib\jrt-fs.jar)

组织结构定义中基本文件的设计

我们可以参考Linux文件系统,其一个文件应该包含什么样的基本属性:name,可读性,创建时间,最后修改时间,最后访问时间。

我们把我们的目光转向jdk.internal.jrtfs这个包下。找到jdk.internal.jrtfs.JrtFileAttributes,因为Java9要兼容Java8的东西,所以势必要做两种不一样的考虑,那么此处就应该开始做一个岔路口。里面定义了上面所说的这些基本属性。同样,我们可以看到它是基于树的节点控制来做到的。

/**
 * File attributes implementation for jrt image file system.
 *
 * @implNote This class needs to maintain JDK 8 source compatibility.
 *
 * It is used internally in the JDK to implement jimage/jrtfs access,
 * but also compiled and delivered as part of the jrtfs.jar to support access
 * to the jimage file provided by the shipped JDK by tools running on JDK 8.
 */
final class JrtFileAttributes  implements BasicFileAttributes {
    private final Node node;
    JrtFileAttributes(Node node) {
        this.node = node;
    }
    ///////// basic attributes ///////////
    @Override
    public FileTime creationTime() {
        return node.creationTime();
    }
    @Override
    public boolean isDirectory() {
        return node.isDirectory();
    }
    @Override
    public boolean isOther() {
        return false;
    }
    @Override
    public boolean isRegularFile() {
        return !isDirectory();
    }
    @Override
    public FileTime lastAccessTime() {
        return node.lastAccessTime();
    }
    @Override
    public FileTime lastModifiedTime() {
        return node.lastModifiedTime();
    }
    @Override
    public long size() {
        return node.size();
    }
    @Override
    public boolean isSymbolicLink() {
        return node.isLink();
    }
    @Override
    public Object fileKey() {
        return node.resolveLink(true);
    }
    ///////// jrtfs specific attributes ///////////
    /**
     * Compressed resource file. If not available or not applicable, 0L is
     * returned.
     *
     * @return the compressed resource size for compressed resources.
     */
    public long compressedSize() {
        return node.compressedSize();
    }
    /**
     * "file" extension of a file resource.
     *
     * @return extension string for the file resource
     */
    public String extension() {
        return node.extension();
    }
    @Override
    public final String toString() {
        StringBuilder sb = new StringBuilder(1024);
        try (Formatter fm = new Formatter(sb)) {
            if (creationTime() != null) {
                fm.format("    creationTime    : %tc%n", creationTime().toMillis());
            } else {
                fm.format("    creationTime    : null%n");
            }
            if (lastAccessTime() != null) {
                fm.format("    lastAccessTime  : %tc%n", lastAccessTime().toMillis());
            } else {
                fm.format("    lastAccessTime  : null%n");
            }
            fm.format("    lastModifiedTime: %tc%n", lastModifiedTime().toMillis());
            fm.format("    isRegularFile   : %b%n", isRegularFile());
            fm.format("    isDirectory     : %b%n", isDirectory());
            fm.format("    isSymbolicLink  : %b%n", isSymbolicLink());
            fm.format("    isOther         : %b%n", isOther());
            fm.format("    fileKey         : %s%n", fileKey());
            fm.format("    size            : %d%n", size());
            fm.format("    compressedSize  : %d%n", compressedSize());
            fm.format("    extension       : %s%n", extension());
        }
        return sb.toString();
    }
}

这样,我们就可以有组成一个树形文件系统的节点定义了。

文件系统镜像的入口设定

接着通过jdk.internal.jrtfs.SystemImage来作为文件系统的加载入口,在初始化这个类的时候,会首先把静态代码块给执行,接着,我们会在jdk.internal.jrtfs.JrtFileSystem 其构造函数中发现其调用了SystemImage.open()方法,可以知道其首先会检查C:\Program Files\Java\jdk-9.0.1\lib\modules这个文件是否存在,存在,就使用jdk.internal.jimage.ImageReader中的静态内部类jdk.internal.jimage.ImageReader.SharedImageReader来对此文件的进行读取然后建立相应的文件系统镜像:

abstract class SystemImage {
    abstract Node findNode(String path) throws IOException;
    abstract byte[] getResource(Node node) throws IOException;
    abstract void close() throws IOException;
    static SystemImage open() throws IOException {
        if (modulesImageExists) {
            // open a .jimage and build directory structure
            final ImageReader image = ImageReader.open(moduleImageFile);
            image.getRootDirectory();
            return new SystemImage() {
                @Override
                Node findNode(String path) throws IOException {
                    return image.findNode(path);
                }
                @Override
                byte[] getResource(Node node) throws IOException {
                    return image.getResource(node);
                }
                @Override
                void close() throws IOException {
                    image.close();
                }
            };
        }
        if (Files.notExists(explodedModulesDir))
            throw new FileSystemNotFoundException(explodedModulesDir.toString());
        return new ExplodedImage(explodedModulesDir);
    }
    static final String RUNTIME_HOME;
    // "modules" jimage file Path
    final static Path moduleImageFile;
    // "modules" jimage exists or not?
    final static boolean modulesImageExists;
    // <JAVA_HOME>/modules directory Path
    static final Path explodedModulesDir;
    static {
        PrivilegedAction<String> pa = SystemImage::findHome;
        RUNTIME_HOME = AccessController.doPrivileged(pa);
        FileSystem fs = FileSystems.getDefault();
        moduleImageFile = fs.getPath(RUNTIME_HOME, "lib", "modules");
        explodedModulesDir = fs.getPath(RUNTIME_HOME, "modules");
        modulesImageExists = AccessController.doPrivileged(
            new PrivilegedAction<Boolean>() {
                @Override
                public Boolean run() {
                    return Files.isRegularFile(moduleImageFile);
                }
            });
    }
    /**
     * Returns the appropriate JDK home for this usage of the FileSystemProvider.
     * When the CodeSource is null (null loader) then jrt:/ is the current runtime,
     * otherwise the JDK home is located relative to jrt-fs.jar.
     */
    private static String findHome() {
        CodeSource cs = SystemImage.class.getProtectionDomain().getCodeSource();
        if (cs == null)
            return System.getProperty("java.home");
        // assume loaded from $TARGETJDK/lib/jrt-fs.jar
        URL url = cs.getLocation();
        if (!url.getProtocol().equalsIgnoreCase("file"))
            throw new InternalError(url + " loaded in unexpected way");
        try {
            Path lib = Paths.get(url.toURI()).getParent();
            if (!lib.getFileName().toString().equals("lib"))
                throw new InternalError(url + " unexpected path");
            return lib.getParent().toString();
        } catch (URISyntaxException e) {
            throw new InternalError(e);
        }
    }
}

也就是说,上面这个类的定义,我们可以把启动封装一个open方法,最后在大一统实现文件系统的时候集中调用,每个类做好自己那份事情就好。

jdk.internal.jrtfs.JrtFileSystem的构造器:

class JrtFileSystem extends FileSystem {
    private final JrtFileSystemProvider provider;
    private final JrtPath rootPath = new JrtPath(this, "/");
    private volatile boolean isOpen;
    private volatile boolean isClosable;
    private SystemImage image;
    JrtFileSystem(JrtFileSystemProvider provider, Map<String, ?> env)
            throws IOException
    {
        this.provider = provider;
        this.image = SystemImage.open();  // open image file
        this.isOpen = true;
        this.isClosable = env != null;
    }
...
}

提供结构定义并设定加载文件系统入口

通过前面提到的索引数据和结构定义数据分开的可以知道,我们的结构定义也是需要有的,那么,走进jdk.internal.jrtfs.JrtFileSystemProvider来看看其内在乾坤,从下面的源码中可以知道,JrtFileSystemProvider 会判断区分当前的环境状态(这里要求必须存在C:\Program Files\Java\jdk-9.0.1\lib\jrt-fs.jar),首先拿到jrt-fs.jar的路径,其实通过URLClassLoader.loadClass(String name, boolean resolve)得到Classloader实例,加载完这些结构定义之后,返回一个FileSystem实例(return new JrtFileSystem(this, env);)

public final class JrtFileSystemProvider extends FileSystemProvider {
    private volatile FileSystem theFileSystem;
    public JrtFileSystemProvider() {
    }
    @Override
    public String getScheme() {
        return "jrt";
    }
    /**
     * Need RuntimePermission "accessSystemModules" to create or get jrt:/
     */
    private void checkPermission() {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            RuntimePermission perm = new RuntimePermission("accessSystemModules");
            sm.checkPermission(perm);
        }
    }
...
   @Override
    public FileSystem newFileSystem(URI uri, Map<String, ?> env)
            throws IOException {
        Objects.requireNonNull(env);
        checkPermission();
        checkUri(uri);
        if (env.containsKey("java.home")) {
            return newFileSystem((String)env.get("java.home"), uri, env);
        } else {
            return new JrtFileSystem(this, env);
        }
    }
    private static final String JRT_FS_JAR = "jrt-fs.jar";
    private FileSystem newFileSystem(String targetHome, URI uri, Map<String, ?> env)
            throws IOException {
        Objects.requireNonNull(targetHome);
        Path jrtfs = FileSystems.getDefault().getPath(targetHome, "lib", JRT_FS_JAR);
        if (Files.notExists(jrtfs)) {
            throw new IOException(jrtfs.toString() + " not exist");
        }
        Map<String,?> newEnv = new HashMap<>(env);
        newEnv.remove("java.home");
        ClassLoader cl = newJrtFsLoader(jrtfs);
        try {
            Class<?> c = Class.forName(JrtFileSystemProvider.class.getName(), false, cl);
            @SuppressWarnings("deprecation")
            Object tmp = c.newInstance();
            return ((FileSystemProvider)tmp).newFileSystem(uri, newEnv);
        } catch (ClassNotFoundException |
                 IllegalAccessException |
                 InstantiationException e) {
            throw new IOException(e);
        }
    }
...
}

文件系统路径定义

既然是文件系统,路径这块总要有定义的,就好像Linux使用/作为根,对于Jrtfs来说,同样要有相应定义的。jdk.internal.jrtfs.JrtPath 就是jrt file systems关于Path的基本实现类。

作为一个Path其解析的肯定是一个URI字符串路径,对于操作字符串,我们用的比较多的有切分,而且字符串内部用的比较多的同样有offset,和判断/home/abc/ddd一样,我们通过确认/这个约定来对文件系统进行分层,确定父子 关系,就好像我们的/Base/A模块/B模块/C模块,要获取某些操作,我们都需要先对这个路径以/做偏移量操作,以方便快速获取到某模块的名字。而我们的很多方法刚开始都会调用initOffsets();,那我们来看看这个方法的具体操作:

// create offset list if not already created
//首先确定`/`的字符数量,来确定模块数量
    private void initOffsets() {
        if (this.offsets == null) {
            int len = path.length();
            // count names
            int count = 0;
            int off = 0;
            while (off < len) {
                char c = path.charAt(off++);
              //排除多个"//..."相连的情况,两个,三个等等,当"/"后面是其他的时候,说明就是一个模块
                if (c != '/') {
                    count++;
                    off = path.indexOf('/', off);
                    if (off == -1)
                        break;
                }
            }
            // populate offsets
          //计算这个模块路径上,每个模块所在的偏移量位置,方便快速拿到
            int[] offsets = new int[count];
            count = 0;
            off = 0;
            while (off < len) {
                char c = path.charAt(off);
                if (c == '/') {
                    off++;
                } else {
                    offsets[count++] = off++;
                    off = path.indexOf('/', off);
                    if (off == -1)
                        break;
                }
            }
            this.offsets = offsets;
        }
    }

然后再加入一个JrtFileSystem,自然很多事情就可以做到了,此处就不再多说了。

Jrt文件系统的文件存储实现

其实Jrt file systems的文件存储实现很简单,可以说没什么内容,因为是内存里建立起来的镜像文件系统,它也只提供了一些基本的约束,如,文件系统应该以什么为开头等等。

final class JrtFileStore extends FileStore {
    protected final FileSystem jrtfs;
    JrtFileStore(JrtPath jrtPath) {
        this.jrtfs = jrtPath.getFileSystem();
    }
    @Override
    public String name() {
        return jrtfs.toString()/*"jrt:/"*/ + "/";
    }
    @Override
    public String type() {
        return "jrtfs";
    }
	//JRT文件系统的话,返回的是true
    @Override
    public boolean isReadOnly() {
        return jrtfs.isReadOnly();
    }
    @Override
    public boolean supportsFileAttributeView(String name) {
        return name.equals("basic") || name.equals("jrt");
    }
  ...
}

Jrtfs中文件属性视图的设定

我们在写web项目的时候,往往会使用DTO来展示这些公开的数据,对于文件系统中的文件也是,这就出现了文件属性视图的需求,包括读取和对这些公开属性的设定,比如文件的创建修改时间。

我们找到java.nio.file.attribute.BasicFileAttributeView这个接口,里面定义了上面所说的这些基本属性。然后我们通过jdk.internal.jrtfs.JrtFileAttributeView来对其进行实现。

我们可以通过文件系统类的类型是否相等来判断到底是使用老版本的通过classpath来加载的方式,还是通过Jrtfs的方式来加载。请看如下代码:

@SuppressWarnings("unchecked") // Cast to V
   static <V extends FileAttributeView> V get(JrtPath path, Class<V> type, LinkOption... options) {
       Objects.requireNonNull(type);
       if (type == BasicFileAttributeView.class) {
           return (V) new JrtFileAttributeView(path, false, options);
       }
       if (type == JrtFileAttributeView.class) {
           return (V) new JrtFileAttributeView(path, true, options);
       }
       return null;
   }

也可以通过一个String关键字来判断:

static JrtFileAttributeView get(JrtPath path, String type, LinkOption... options) {
        Objects.requireNonNull(type);
        if (type.equals("basic")) {
            return new JrtFileAttributeView(path, false, options);
        }
        if (type.equals("jrt")) {
            return new JrtFileAttributeView(path, true, options);
        }
        return null;
    }
    @Override
    public String name() {
        return isJrtView ? "jrt" : "basic";
    }

基本属性的话,首先对所操作属性进行判断了:

static Object attribute(AttrID id, JrtFileAttributes jrtfas, boolean isJrtView) {
        switch (id) {
            case size:
                return jrtfas.size();
            case creationTime:
                return jrtfas.creationTime();
            case lastAccessTime:
                return jrtfas.lastAccessTime();
            case lastModifiedTime:
                return jrtfas.lastModifiedTime();
            case isDirectory:
                return jrtfas.isDirectory();
            case isRegularFile:
                return jrtfas.isRegularFile();
            case isSymbolicLink:
                return jrtfas.isSymbolicLink();
            case isOther:
                return jrtfas.isOther();
            case fileKey:
                return jrtfas.fileKey();
            case compressedSize:
                if (isJrtView) {
                    return jrtfas.compressedSize();
                }
                break;
            case extension:
                if (isJrtView) {
                    return
                }
                break
        }
        return null
    }

这里的枚举类型,也是我们这个类中定义的:

private static enum
       size,
       creationTime,
       lastAccessTime,
       lastModifiedTime,
       isDirectory,
       isRegularFile,
       isSymbolicLink,
       isOther,
       fileKey,
       compressedSize,
       extension
   };

就到此吧,关于更多对模块的解读,留在下一篇去说。

原文:Refresh your Java skills--聊聊Java9 中模块化所基于的文件系统 JRTFS