聲明:本文轉載,原文鏈接:
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)
所以,我們去自定義一個類加載器時,一般都會繼承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!/
我們會發現這些路徑帶有 !/ 這樣的符號,這個其實代表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.Handler、sun.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
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。