# 前言

刚接触 java 的时候很困惑一个事情 File相对路径,以哪个目录为参照物。

随着 io 模型的发展,java 1.7 的 nio,使用 PathPathsFiles 等来方便 io 的操作。

ClassLoader 用于获取class 文件 的 io,我们也可以用于获取文件的 io,以便于我们读取文件内容。

# 本文设计内容

  • File ,ZipFile,JarFile 读取相对路径和绝对路径文件内容。
  • System.getProperty("user.dir”) 是怎么来的。
  • Paths、Path、Files 读取文件内容。
  • 类加载器获取文件内容,Class.getResourceAsStream 和 ClassLoader.getResourceAsStream。
  • 介绍类加载器的双亲委派模型,及在代码中找到对应的加载逻辑。

代码基于 Mac 10.15.4,JDK 1.8。

# 基于 File 获取文件内容

绝对路径的内容获取比较简单,直接获取文件 io ,然后利用工具类读取文件内容。

# 获取绝对路径文件内容

final File file = new File("/Users/zhangpanqin/github/fly-java/demo.txt");
final byte[] bytes = cn.hutool.core.io.FileUtil.readBytes(file);
System.out.println(new String(bytes, StandardCharsets.UTF_8));

# 获取 JarFile 中的内容

JarFile 继承 ZipFile 用于获取 jar 包中的内容。比如我想获取 jar 中的某个文件的的内容。


final File file = new File("/Users/zhangpanqin/github/fly-java/src/main/resources/fastjson-1.2.68.jar");
final JarFile jarFile = new JarFile(file);
final JarEntry jarEntry = jarFile.getJarEntry("META-INF/LICENSE.txt");
final InputStream inputStream = jarFile.getInputStream(jarEntry);
// 工具类是 hutool
System.out.println(IoUtil.read(inputStream, StandardCharsets.UTF_8));
IoUtil.close(inputStream);

# 获取相对路径的内容

File.getAbsolutePath 查看源码可以发现,相对路径其实就是在前面拼接了 System.getProperty("user.dir")

class UnixFileSystem extends FileSystem {
    public String resolve(File f) {
        if (isAbsolute(f)) return f.getPath();
        return resolve(System.getProperty("user.dir"), f.getPath());
    }
}

只要我们弄清楚 System.getProperty("user.dir"),问题就迎刃而解。Java System Properties (opens new window) 中介绍 user.dir 是用户的工作目录。

image-20200418232813977

什么是用户工作目录呢?就是执行 java 命令的目录。在那个目录下执行命令,usr.dir 就会被 java 虚拟机赋值为执行命令的路径。我在 /Users/zhangpanqin/github/fly-java/test 目录下运行编译的 class 文件。-cp 指定 classpath 路径。

image-20200418233359327

# nio 读取文件内容

Path 可以类比 File 理解使用。然后工具类 Paths 可以获得 PathFiles 更是提供了丰富的 api 用于 crud 操作文件 Path

# 获取绝对路径内容

@Test
public void run33() throws IOException {
   final Path path = Paths.get("/Users/zhangpanqin/github/fly-java/demo.txt");
   final byte[] bytes = Files.readAllBytes(path);
   System.out.println(new String(bytes,StandardCharsets.UTF_8));
}

# 获取相对路径内容

Paths 获取相对路径时,路径不以 / 开头。也可以理解成相对于 System.getProperty("user.dir") 路径。

public static void main(String[] args) {
    System.out.println(System.getProperty("user.dir"));
    System.out.println(Paths.get("").toAbsolutePath());
}

image-20200419001203097

# 基于 ClassLoader 获取文件内容

# ClassLoader.getResourceAsStream

// ClassLoader.getResourceAsStream java 1.8 源码
public InputStream getResourceAsStream(String name) {
    URL url = getResource(name);
    try {
        return url != null ? url.openStream() : null;
    } catch (IOException e) {
        return null;
    }
}

从代码可以看到主要逻辑还是集中在 getResource

public URL getResource(String name) {
    URL url;
    if (parent != null) {
        url = parent.getResource(name);
    } else {
        url = getBootstrapResource(name);
    }
    if (url == null) {
        url = findResource(name);
    }
    return url;
}

以上代码的逻辑也即是我们常听到的 双亲委派机制。先让 父类加载 去加载资源,找不到再有自己找。类加载器单独讲

类加载器读取读取资源,先从自己负责的路径查找。比如应用类加载器 sun.misc.Launcher.AppClassLoader#AppClassLoader 负责 classpath 查找资源。类加载器读取资源相对于 FilePath 优势在哪里呢?比如当我想获取一个 jar 中的资源,你用路径就比较麻烦了,ClassLoader 可以从负责的路径下寻找,还可以去 jar 包中寻找。

final URL resource = Test2.class.getClassLoader().getResource("com/alibaba/fastjson/JSONArray.class");
System.out.println(resource);

上述打印结果

jar:file:/Users/zhangpanqin/.m2/repository/com/alibaba/fastjson/1.2.62/fastjson-1.2.62.jar!/com/alibaba/fastjson/JSONArray.class

我们还可以获取一个路径的 inputstream

@Test
public void run222(){
    final InputStream resourceAsStream = Test2.class.getClassLoader().getResourceAsStream("META-INF/maven/com.alibaba/fastjson/pom.properties");
    System.out.println(IoUtil.read(resourceAsStream, StandardCharsets.UTF_8));
}

上述结果为:

#Generated by Maven
#Mon Oct 07 22:09:36 CST 2019
version=1.2.62
groupId=com.alibaba
artifactId=fastjson

# Class.getResourceAsStream

Class.getResourceAsStream 实际也调用的 ClassLoader 加载资源,但是它会将路径补充到相对于当前类所在包的路径。

比如

// com.fly.study.java.classloader.Test2
@Test
public void run555(){
    System.out.println(Test2.class.getResource("name"));
}

实际查找的资源为 com.fly.study.java.classloader.name。相对于当前类所在包的资源。

#

# 类加载器

我们经常会听到类加载器的 双亲委派模型

img

去哪里可以看到这些类加载呢。

启动类加载器 不是 java 代码实现的我们看不到源码。

sun.misc.Launcher 类中有我们知道的 扩展类加载器 sun.misc.Launcher.ExtClassLoader应用类加载器 sun.misc.Launcher.AppClassLoader

java.lang.ClassLoader#getSystemClassLoader 代码看的话,实际返回的应用类加载器。

public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}
private static synchronized void initSystemClassLoader() {
        if (!sclSet) {
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l != null) {
                Throwable oops = null;
                scl = l.getClassLoader();
            }
        }
}

运行代码测试,返回的是应用类加载器。

// sun.misc.Launcher$AppClassLoader@18b4aac2
@Test
public void run66(){
	System.out.println(ClassLoader.getSystemClassLoader());
}

这三个类加载器负责不同路径下的类加载。

# 启动类加载器介绍

启动类加载器 BootClassLoader 负责System.getProperty("sun.boot.class.path") 的类加载。也即是以下类。

${JAVA_HOME}/jre/lib/*.jar${JAVA_HOME}/jre/classes 类的加载。

/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/resources.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/rt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/sunrsasign.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jsse.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jce.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jfr.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/classes
@Test
public void run77() {
    final URLClassPath bootstrapClassPath = Launcher.getBootstrapClassPath();
    final URL[] urLs = bootstrapClassPath.getURLs();
    Stream.of(urLs).forEach(item->{
        System.out.println(item.getFile());
    });
}

# 扩展类加载器

扩展类加载器 sun.misc.Launcher.ExtClassLoader 为加载 System.getProperty("java.ext.dirs") 中的类。

@Test
public void run99() {
    final String property = System.getProperty("java.ext.dirs");
    final String[] split = property.split(":");
    Stream.of(split).forEach(item -> {
        System.out.println(item);
    });
}
/Users/zhangpanqin/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java

# 应用类加载器

应用类加载器 sun.misc.Launcher.AppClassLoader 加载 System.getProperty("java.class.path");

# -cp 指定 classpath 路径,多个路径可以使用 : 分开(linux 下为 :,window 下为 ;),
java -cp /Users/zhangpanqin/github/fly-java/target/classes com.fly.study.java.classloader.Test2