java類加載器和jar路徑解析


聲明:本文轉載,原文鏈接

java類加載器和jar路徑解析 - 簡書  https://www.jianshu.com/p/546a7e3dc427

一、類加載器基本原理

 

虛擬機提供了3種類加載器:Bootstrap類加載器、Ext類加載器、App類加載器。他們之間通過雙親委派模式進行類的加載

Bootstrap類加載器:主要加載的是JVM自身需要的類,這個類加載使用C++語言實現的,是虛擬機自身的一部分,它負責將 {jdk}/lib路徑下的核心類庫或-Xbootclasspath參數指定的路徑下的jar包加載到內存中,注意必由於虛擬機是按照文件名識別加載jar包的,如rt.jar,如果文件名不被虛擬機識別,即使把jar包丟到lib目錄下也是沒有作用的(出於安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類)。

Ext類加載器:是指sun.misc.Launcher$ExtClassLoader類,由Java語言實現的,是Launcher的靜態內部類,它負責加載{jdk}/lib/ext目錄下或者由系統變量-Djava.ext.dir指定位路徑中的類庫,開發者可以直接使用標准擴展類加載器。

App類加載器:sun.misc.Launcher$AppClassLoader。它負責加載系統類路徑java -classpath或-D java.class.path 指定路徑下的類庫,也就是我們經常用到的classpath路徑,開發者可以直接使用系統類加載器,一般情況下該類加載是程序中默認的類加載器。

BootStrap 是最頂級類加載器,Ext持有BootStrap引用,App持有Ext引用。當去加載一個類時,首先由上級加載器去加載,上級加載器不能加載,才由自己進行加載。(具體可自行搜索 雙親委派模型)

其中ExtClassLaoder、AppClassLoader 都是URLClassLaoder子類(Bootstrap是C++實現的,所以不是它的子類),當我們去定義自己的ClassLoader時,一般去繼承URLClassLoader。

ClassLoader

ClassLoader 是所有類加載器的父類,其中主要有三個方法:loadClass(加載一個class)、findClass(找到class文件所在磁盤的位置(也可以是網絡流))、defineClass(將class轉載到jvm內存)
當去加載一個類時,會通過loadClass去加載,loadClass主要邏輯如下:

// 代碼只保留了核心邏輯
protected Class<?> loadClass(String name, boolean resolve) {
    Class<?> c = findLoadedClass(name);  //判斷有沒有加載過
    if (c == null) {
        if (parent != null) {
            c = parent.loadClass(name, false);   //首先父加載器加載
        }
        if (c == null) {
            c = findClass(name);    //找到該class並裝在在內存中
        }
    }
    return c;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

URLClassLoader

URLClassLoader 繼承了ClassLoader,其主要實現的功能,就是通過類的全限定名(包名+類名)來定位到class文件的位置。
我們看一下URLClassLoader構造方法

URLClassLoader(URL[] urls, ClassLoader parent) 
URLClassLoader(URL[] urls, ClassLoader parent,AccessControlContext acc) 
public URLClassLoader(URL[] urls)
URLClassLoader(URL[] urls, AccessControlContext acc)
構造函數,都包含URL[] 這個參數。其實這個參數就代表 類所在的路徑(可以是:文件路徑、網絡流、jar路徑。)這樣當去加載一個類時,就通過這些路徑去尋找。
所以,我們去自定義一個類加載器時,一般都會繼承URLClassLoader,這樣我們把類所在的路徑URL傳遞給URLClassLoader,urlClassLoader就會幫我們在路徑尋找並加載類,不用我們過問其中的邏輯了。
URLClassLoader 對findClass進行了重寫,主要邏輯如下
protected Class<?> findClass(final String name) {
    final Class<?> result;
    String path = name.replace('.', '/').concat(".class");  //name代表類的全限定名
    Resource res = ucp.getResource(path, false);   //ucp就是對 URL[] 封裝,在URL[] 路徑列表里查找要裝載的類
    if (res != null) {
        try {
            return defineClass(name, res);  //將類裝在jvm內存
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    } else {
        return null;
    }

    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

可以看到URLClassLoader實現了:在路徑查找class文件,並裝載到內存中。
下面我們演示一下

示例

例1:

public class TestClass {
    public static void main(String[] args) {
        TestClass testClass = new TestClass();
        ClassLoader classLoader = testClass.getClass().getClassLoader();
        URL[] urls = ((URLClassLoader) classLoader).getURLs();
        for(URL url :urls) {
            System.out.println(url);
        }
    }
}

通過上面我們知道,我們運行代碼默認為AppClassLoader,也就是一個URLClassLoader,我們把其中的路徑打印出來,結果如下:

file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/deploy.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/ext/jaccess.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/ext/localedata.jar
.......

可以看到,都是我們classPath下面的jar包。


例2:
我們把這段測試代碼放到springBoot項目中,然后打成一個jar包,進行運行。上面代碼會得到下面的輸出:

jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/classes!/
jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/api-core-0.0.4-SNAPSHOT.jar!/
jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/raptor-es-common-1.0.3-SNAPSHOT.jar!/
jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/httpclient-4.5.7.jar!/
jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/httpmime-4.5.7.jar!/
jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/httpcore-4.4.11.jar!/
這些jar包,都是我們項目中引入的第三方jar包。我們可以看到這些jar包路徑被傳入到classloader中,供classloader加載類時,進行路徑搜索。
我們會發現這些路徑帶有 !/ 這樣的符號,這個其實代表java特有的路徑符號,表示一個jar文件,這樣java去讀取的時候,就會使用jar形勢進行解壓讀取。(因為讀取jar文件不能像其他文件那樣讀取,jar其實是一種壓縮文件,必須對其解壓)
我們現在拋出一個問題:為什么例1中URL形式是file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/charsets.jar 而不是:file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/charsets.jar!/,結尾帶上!/。既然!/代表是一個jar文件,jvm會使用jar形勢解壓讀取,那么jar文件就要帶有!/, 就像我們在例2的時候,jar以!/結尾。為什么這里的jar沒有帶有!/

二、jar文件路徑解析

URL類解析

URLClassLoader 會通過URL[] 來搜索類所在的位置,我們看一下這個URL的實現,首先看一下構造函數:

 public URL(String spec) throws MalformedURLException {
        this(null, spec);
    }
public URL(URL context, String spec) throws MalformedURLException {
        this(context, spec, null);
    }
public URL(URL context, String spec, URLStreamHandler handler) {
        protocol = getProto(spec);  //解析出:前面的字符,作為該協議
        this.handler = getURLStreamHandler(protocol)  //獲取該協議對應的處理類。負責對該協議進行讀寫
        this.handler.parseURL(this, spec, start, limit); //校驗
    }

我們看一下getURLStreamHandler:

static URLStreamHandler getURLStreamHandler(String protocol) {
        //GetPropertyAction("java.protocol.handler.pkgs", "") 就是獲取jvm有沒有這個property變量,
       //也就說我們可以自己定義URL協議,自己定義協議處理方式。並把類名 寫到jvm property變量中
        packagePrefixList = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction("java.protocol.handler.pkgs", "")
        );
        if (packagePrefixList != "") {
            packagePrefixList += "|";
        }

        packagePrefixList += "sun.net.www.protocol";

        StringTokenizer packagePrefixIter =
                new StringTokenizer(packagePrefixList, "|");

        while (handler == null && packagePrefixIter.hasMoreTokens()) {

            String packagePrefix = packagePrefixIter.nextToken().trim();
            try {
                String clsName = packagePrefix + "." + protocol +
                        ".Handler";
                Class<?> cls = null;
                try {
                    cls = Class.forName(clsName);
                } catch (ClassNotFoundException e) {
                    ClassLoader cl = ClassLoader.getSystemClassLoader();
                    if (cl != null) {
                        cls = cl.loadClass(clsName);
                    }
                }
                if (cls != null) {
                    handler =
                            (URLStreamHandler) cls.newInstance();
                }
            } catch (Exception e) {
                // any number of exceptions can get thrown here
            }
        }
        return handler;

    }

通過上面的代碼我們可以看出。當我們new URL("jar:file:/yt/test/test.jar"),就會構造一個URL,其中負責和jar文件進行交互的Handler是sun.net.www.protocol.jar.Hnadler(除此之外,還有sun.net.www.protocol.file.Handlersun.net.www.protocol.http.Handler等)當我們對該URL進行讀寫時,其內部就是用這個Handler進行處理。這樣對一個jar文件讀取,就是用jar.Handler去處理;對一個http進行讀取,就是使用http.Handler處理

this.handler.parseURL(this, spec, start, limit); 這段代碼主要是對URL進行校驗,對於jar這種協議,會校驗字符含有!/,如果缺少會報錯。所以我們要這樣寫new URL("jar:file:/yt/test/test.jar!/") 才不會報錯。parseURL主要邏輯如下:

Object var2 = null;
boolean var3 = true;
int var6;
if ((var6 = indexOfBangSlash(var1)) == -1) {
    throw new NullPointerException("no !/ in spec");
} else {
    try {
        String var4 = var1.substring(0, var6 - 1);
        new URL(var4);
        return var1;
    } catch (MalformedURLException var5) {
        throw new NullPointerException("invalid url: " + var1 + " (" + var5 + ")");
    }
}

URLClassLoader

URLClassLoader最重要的功能,就是從URL[]列表中查詢到要裝在的類所在的路徑,就是findClass這個方法

protected Class<?> findClass(final String name) {
        String path = name.replace('.', '/').concat(".class");  //name代表類的全限定名
        Resource res = ucp.getResource(path, false);   //ucp就是對 URL[] 封裝,在URL[] 路徑列表里查找轉載的類
       return defineClass(name, res);  //將類裝在jvm內存
}

ucp 就是 URLClassPath對象,我們看一下ucp.getResouce方法. (原方法太復雜,這邊對其進行了抽象總結)

 public Enumeration<Resource> getResources(final String var1, final boolean var2) {
       for url: urls{     //urls 就是URLClassLoader那個URL[] 列表,用於搜索類的路徑列表
            URLClassPath.Loader  loader = getLoader(url);
            res = loader.getResource(var1, var2);
            if (res != null) retun null;
       }
    //原方法會對這邊邏輯進行緩存等高效運算處理
}

private URLClassPath.Loader getLoader(final URL var1) throws IOException {
        String var1x = var1.getFile();
        if (var1x != null && var1x.endsWith("/")) {
            return (URLClassPath.Loader)("file".equals(var1.getProtocol()) ? new URLClassPath.FileLoader(var1) : new URLClassPath.Loader(var1));
        } else {
            return new URLClassPath.JarLoader(var1, URLClassPath.this.jarHandler, URLClassPath.this.lmap);
        }
}

我們看一下URLClassPath.Loader這個內部類,getResource邏輯主要是:判斷class是否在該url路徑下。
現在我們回到上面的問題:

1、當我們運行一個非jar包時,其class路徑是這樣形勢(其實對應AppClassLoader):file:/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/charsets.jar

那我們 getLoader的時候,就會走 new URLClassPath.JarLoader()邏輯,可以看到這是一個jarLoader,也就是說他會按jar包讀取方式讀取。

 

2、當我們運行一個springboot打包的jar時,其class路徑是這樣的形式(其實對應的是springboot自定義的classloader):jar:file:/Users/yt/test/spring-boot-test.jar!/BOOT-INF/lib/api-core-0.0.4-SNAPSHOT.jar!/

那我們getLaoder的時候,會走這個邏輯,new URLClassPath.Loader(var1));本身該URL就是jar協議,所以會通過jar協議進行讀取。

三、getResource

我們創建一個項目,其目錄如下:

src/main/java: TestClass.java
src/main/resouce: /res.txt

public class TestClass {
    public static void main(String[] args) {
        TestClass testClass = new TestClass();
        URL fileURL = testClass.getClass().getResource("/res.txt");
        System.out.println(fileURL.getFile());
    }
}

我們運行這個方法

運行后結果:
/Users/yt/test/res.text
我們對這個項目打成jar包(test.jar),運行后的結果:
/Users/yt/test.jar!/res.text

所以對於jar包里的文件路徑,其格式為 jar:file:{path}!/{path}



作者:一天的
鏈接:https://www.jianshu.com/p/546a7e3dc427
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM