您好,登錄后才能下訂單哦!
這篇文章主要講解了“Java類加載器與雙親委派機制怎么應(yīng)用”,文中的講解內(nèi)容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Java類加載器與雙親委派機制怎么應(yīng)用”吧!
大家想必都有過平時開發(fā)springboot 項目的時候稍微改動一點代碼,就得重啟,就很煩
網(wǎng)上一般介紹 2種方式 spring-boot-devtools
,或者通過JRebel
插件 來實現(xiàn)"熱部署"
熱部署就是當應(yīng)用正在運行時,修改應(yīng)用不需要重啟應(yīng)用。
其中 spring-boot-devtools
其實是自動重啟,主要是節(jié)省了我們手動點擊重啟的時間,不算真正意義上的熱部署。JRebel插件啥都好,就是需要收費
但如果平時我們在調(diào)試debug的情況下,只是在方法塊內(nèi)代碼修改了一下,我們還得重啟項目,就很浪費時間。這個時候我們其實可以直接build ,不重啟項目,即可 實現(xiàn)熱部署。
我們先來寫一個例子演示一下:
@RestController public class TestController { @RequestMapping(value = "/test",method = {RequestMethod.GET, RequestMethod.POST}) public void testclass() { String name = "zj"; int weight = 100; System.out.println("name:"+ name); System.out.println("weight: "+weight); } }
結(jié)果:
name:zj weight: 100
修改代碼,然后直接build項目,不重啟項目,我們再請求這個測試接口:
String name = "ming"; int weight = 300;
神奇的一幕出現(xiàn)了,結(jié)果為:
name:ming weight: 300
當我們修改.java文件,只需重新生成對應(yīng)的.class文件,就能影響到程序運行結(jié)果, 無需重啟,Why? 背后JVM的操作原理且看本文娓娓道來。
首先我們得先了解一下 什么是.class文件
舉個簡單的例子,創(chuàng)建一個Person類:
public class Person { /** * 狀態(tài) or 屬性 */ String name;//姓名 String sex;//性別 int height;//身高 int weight;//體重 /** * 行為 */ public void sleep(){ System.out.println(this.name+"--"+ "睡覺"); } public void eat(){ System.out.println("吃飯"); } public void Dance(){ System.out.println("跳舞"); } }
我們執(zhí)行javac命令,生成Person.class文件
然后我們通過vim 16進制
打開它
#打開file文件 vim Person.class #在命令模式下輸入.. 以16進制顯示 :%!xxd #在命令模式下輸入.. 切換回默認顯示 :%!xxd -r
不同的操作系統(tǒng),不同的 CPU 具有不同的指令集,JAVA能做到平臺無關(guān)性,依靠的就是 Java 虛擬機。.java源碼是給人類讀的,而.class字節(jié)碼是給JVM虛擬機讀的,計算機只能識別 0 和 1組成的二進制文件,所以虛擬機就是我們編寫的代碼和計算機之間的橋梁。
虛擬機將我們編寫的 .java 源程序文件編譯為 字節(jié)碼 格式的 .class 文件,字節(jié)碼是各種虛擬機與所有平臺統(tǒng)一使用的程序存儲格式,class文件主要用于解決平臺無關(guān)性的中間文件
在之前的一篇文章談?wù)凧AVA中對象和類、this、super和static關(guān)鍵字中,我們知曉 Java 是如何創(chuàng)建對象的
Person zhang = new Person();
雖然我們寫的時候是簡單的一句,但是JVM內(nèi)部的實現(xiàn)過程卻是復(fù)雜的:
將硬盤上指定位置的Person.class文件加載進內(nèi)存
執(zhí)行main方法時,在棧內(nèi)存中開辟了main方法的空間(壓棧-進棧),然后在main方法的棧區(qū)分配了一個變量zhang。
執(zhí)行new,在堆內(nèi)存中開辟一個 實體類的 空間,分配了一個內(nèi)存首地址值
調(diào)用該實體類對應(yīng)的構(gòu)造函數(shù),進行初始化(如果沒有構(gòu)造函數(shù),Java會補上一個默認構(gòu)造函數(shù))。
將實體類的 首地址賦值給zhang,變量zhang就引用了該實體。(指向了該對象)
類加載過程
其中 上圖步驟1 Classloader(類加載器) 將class文件加載到內(nèi)存中具體分為3個步驟:加載、連接、初始化
類的生命周期一般有如下圖有7個階段,其中階段1-5為類加載過程,驗證、準備、解析統(tǒng)稱為連接
類的生命周期
1.加載
加載階段:指的是將類對應(yīng)的.class文件中的二進制字節(jié)流讀入到內(nèi)存中,將這個字節(jié)流轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu),然后在堆區(qū)創(chuàng)建一個java.lang.Class 對象,作為對方法區(qū)中這些數(shù)據(jù)的訪問入口
相對于類加載的其他階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節(jié)流的動作)是我們最可以控制的階段,因為開發(fā)人員既可以使用系統(tǒng)提供的類加載器來完成加載,也可以自定義類加載器來完成加載。這個我們文章后面再詳細講
2.驗證
驗證階段:校驗字節(jié)碼文件正確性。這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
這部分對開發(fā)者而言是無法干預(yù)的,以下內(nèi)容了解即可
驗證階段大致會完成4個階段的檢驗動作:
文件格式驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范;例如:是否以0xCAFEBABE
開頭、主次版本號是否在當前虛擬機的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型。
元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規(guī)范的要求;例如:這個類是否有父類,除了java.lang.Object之外。
字節(jié)碼驗證:通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。
符號引用驗證:確保解析動作能正確執(zhí)行。
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經(jīng)過反復(fù)驗證,那么可以考慮采用-Xverifynone
參數(shù)來關(guān)閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
3.準備
準備階段:為類變量(static 修飾的變量)分配內(nèi)存,并將其初始化為默認值
注意此階段僅僅是為類變量 即靜態(tài)變量分配內(nèi)存,并將其初始化為默認值
舉個例子,在這個準備階段:
static int value = 3;//類變量 初始化,設(shè)為默認值 0,不是 3哦 ?。。? int num = 4;//類成員變量,在這個階段不初始化;在 new類,調(diào)用對應(yīng)類的構(gòu)造函數(shù)才進行初始化 final static valFin = 5;//這個比較特殊,在這個階段也不會分配內(nèi)存?。?!
注意: valFin
是被final static修飾的常量
在 **編譯 **的時候已分配好了,所以在準備階段 此時的值為5,所以在這個階段也不會初始化!
4.解析
解析階段:是虛擬機將常量池內(nèi)的符號引用
替換為直接引用
的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符7類符號引用進行。
符號引用就是一組符號來描述目標,可以是任何字面量。
直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
這個階段了解一下即可
5.初始化
直到初始化階段,Java虛擬機才真正開始執(zhí)行類中編寫的Java程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程序。
初始化階段 是類加載過程的最后一個步驟,之前介紹的幾個類加載的動作里,除了在加載階段
用戶應(yīng)用程序可以通過自定義類加載器
的方式局部參與外,其余動作都完全由Java虛擬機來主導(dǎo)控 制。
Java程序?qū)︻惖氖褂梅绞娇煞譃閮煞N:主動使用
與被動使用
。一般來說只有當對類的首次主動使用的時候才會導(dǎo)致類的初始化,所以主動使用又叫做類加載過程中“初始化”開始的時機。
類實例初始化方式,主要是以下幾種:
1、創(chuàng)建類的實例,也就是new的方式
2、訪問某個類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值
3、調(diào)用類的靜態(tài)方法
4、反射(如Class.forName("com.test.Person")
)
5、初始化某個類的子類,則其父類也會被初始化
6、Java虛擬機啟動時被標明為啟動類的類(JavaTest),還有就是Main方法的類會 首先被初始化
這邊就不展開說了,大家記住即可
6.使用
當JVM完成初始化階段之后,JVM便開始從入口方法開始執(zhí)行用戶的程序代碼
7.卸載
當用戶程序代碼執(zhí)行完畢后,JVM便開始銷毀創(chuàng)建的Class對象,最后負責運行的JVM也退出內(nèi)存
在如下幾種情況下,Java虛擬機將結(jié)束生命周期
執(zhí)行了System.exit()方法
程序正常執(zhí)行結(jié)束
程序在執(zhí)行過程中遇到了異?;蝈e誤而異常終止
由于操作系統(tǒng)出現(xiàn)錯誤而導(dǎo)致Java虛擬機進程終止
上文類加載過程中,是需要類加載器的參與,類加載器在Java中非常重要,它使得 Java 類可以被動態(tài)加載到 Java 虛擬機中并執(zhí)行
那什么是類加載器?通過一個類的全限定名來獲取描述此類的二進制字節(jié)流到JVM中,然后轉(zhuǎn)換為一個與目標類對應(yīng)的java.lang.Class對象實例
Java虛擬機支持類加載器的種類:主要包括3中:引導(dǎo)類加載器(Bootstrap ClassLoader)、擴展類加載器(Extension ClassLoader)、應(yīng)用類加載器(系統(tǒng)類加載器,AppClassLoader),另外我們還可以自定義加載器-用戶自定義類加載器
引導(dǎo)類加載器(Bootstrap ClassLoader):BootStrapClassLoader
是由c++實現(xiàn)的。引導(dǎo)類加載器加載java運行過程中的核心類庫JRE\lib\rt.jar,sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar
以及存放 在JRE\classes
里的類,也就是JDK提供的類等常見的比如:Object、Stirng、List
等
擴展類加載器(Extension ClassLoader):它用來加載/jre/lib/ext
目錄以及java.ext.dirs
系統(tǒng)變量指定的類路徑下的類。
應(yīng)用類加載器(AppClassLoader):它主要加載應(yīng)用程序ClassPath下的類(包含jar包中的類)。它是java應(yīng)用程序默認的類加載器。其實就是加載我們一般開發(fā)使用的類
用戶自定義類加載器:用戶根據(jù)自定義需求,自由的定制加載的邏輯,只需繼承應(yīng)用類加載器AppClassLoader,負責加載用戶自定義路徑下的class字節(jié)碼文件
線程上下文類加載器:除了以上列舉的三種類加載器,其實還有一種比較特殊的類型就是線程上下文類加載器
。ThreadContextClassLoader可以是上述類加載器的任意一種,這個我們下文再細說
我們來看一個例子:
public class TestClassLoader { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = TestClassLoader.class.getClassLoader(); System.out.println(classLoader); System.out.println(classLoader.getParent());//獲取其父類加載器 System.out.println(classLoader.getParent().getParent());//獲取父類的父類加載器 } }
結(jié)果:
sun.misc.Launcher
ExtClassLoader@5caf905d null
結(jié)果顯示分別打印應(yīng)用類加載器、擴展類加載器和引導(dǎo)類加載器
由于 引導(dǎo)類加載器 是由c++實現(xiàn)的,所以并不存在一個Java的類,因此會打印出null
我們還可以看到結(jié)果里面打印了 sun.misc.Launcher
,這個是什么東東?
其實Launcher是JRE中用于啟動程序入口main()的類,我們看下Launcher的源碼:
public class Launcher { private static Launcher launcher = new Launcher(); private static String bootClassPath = System.getProperty("sun.boot.class.path"); public static Launcher getLauncher() { return launcher; } private ClassLoader loader; public Launcher() { // Create the extension class loader ClassLoader extcl; try { extcl = ExtClassLoader.getExtClassLoader(); //加載擴展類類加載器 } catch (IOException e) { throw new InternalError( "Could not create extension class loader", e); } // Now create the class loader to use to launch the application try { loader = AppClassLoader.getAppClassLoader(extcl);//加載應(yīng)用程序類加載器,并設(shè)置parent為extClassLoader } catch (IOException e) { throw new InternalError( "Could not create application class loader", e); } Thread.currentThread().setContextClassLoader(loader); //設(shè)置AppClassLoader為線程上下文類加載器 } /* * Returns the class loader used to launch the main application. */ public ClassLoader getClassLoader() { return loader; } /* * The class loader used for loading installed extensions. */ static class ExtClassLoader extends URLClassLoader {} /** * The class loader used for loading from java.class.path. * runs in a restricted security context. */ static class AppClassLoader extends URLClassLoader {}
其中loader = AppClassLoader.getAppClassLoader(extcl);
的核心方法源碼如下:
private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent;//設(shè)置parent if (ParallelLoaders.isRegistered(this.getClass())) { parallelLockMap = new ConcurrentHashMap<>(); package2certs = new ConcurrentHashMap<>(); assertionLock = new Object(); } else { // no finer-grained lock; lock on the classloader instance parallelLockMap = null; package2certs = new Hashtable<>(); assertionLock = this; } }
通過以上源碼我們可以知曉:
Launcher的ClassLoader
是BootstrapClassLoader
,在Launcher創(chuàng)建的同時,還會同時創(chuàng)建ExtClassLoader,AppClassLoader(并設(shè)置其parent為extClassLoader)。其中代碼中 "sun.boot.class.path"是BootstrapClassLoader
加載的jar包路徑。
這幾種類加載器 都遵循 雙親委派機制
雙親委派機制說的其實就是,當一個類加載器收到一個類加載請求時,會去判斷有沒有加載過,如果加載過直接返回,否則該類加載器會把請求先委派給父類加載器。每個類加載器都是如此,只有在父類加載器在自己的搜索范圍內(nèi)找不到指定類時,子類加載器才會嘗試自己去加載。
雙親委派模式優(yōu)勢:
避免類的重復(fù)加載, 當父親已經(jīng)加載了該類時,就沒有必要子ClassLoader再加載一次, 這樣保證了每個類只被加載一次。
保護程序安全,防止核心API被隨意篡改,比如 java核心api中定義類型不會被隨意替換
我們這里看一個例子:
我們新建一個自己的類“String”放在src/java/lang目錄下
public class String { static { System.out.println("自定義 String類"); } }
新建StringTest類:
public class StringTest { public static void main(String[] args) { String str=new java.lang.String(); System.out.println("start test-------"); } }
結(jié)果:
start test-------
可以看出,程序并沒有運行我們自定義的“String”類,而是直接返回了String.class。像String,Integer等類 是JAVA中的核心類,是不允許隨意篡改的!
ClassLoader
是一個抽象類,負責加載類,像 ExtClassLoader,AppClassLoader
都是由該類派生出來,實現(xiàn)不同的類裝載機制。這塊的源碼太多了,就不貼了
我們來看下 它的核心方法loadClass()
,傳入需要加載的類名,它會幫你加載:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 一開始先 檢查是否已經(jīng)加載該類 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 如果未加載過類,則遵循 雙親委派機制,來加載類 if (parent != null) { c = parent.loadClass(name, false); } else { //如果父類是null就是BootstrapClassLoader,使用 啟動類類加載器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { long t1 = System.nanoTime(); // 如果還是沒有加載成功,調(diào)用findClass(),讓當前類加載器加載 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } // 繼承的子類得重寫該方法 protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
loadClass()源碼 展示了,一般加載.class文件大致流程:
先去緩存中 檢查是否已經(jīng)加載該類,有就直接返回,避免重復(fù)加載;沒有就下一步
遵循 雙親委派機制,來加載.class文件
上面兩步都失敗了,調(diào)用findClass()方法,讓當前類加載器加載
注意:由于ClassLoader
類是抽象類,而抽象類是無法通過new創(chuàng)建對象的,所以它最核心的findClass()
方法,沒有具體實現(xiàn),只拋了一個異常,而且是protected的,這是應(yīng)用了模板方法模式
,具體的findClass()方法丟給子類實現(xiàn), 所以繼承的子類得重寫該方法。
那我們仿照 ExtClassLoader,AppClassLoader
來實現(xiàn)一個自定義的類加載器,我們同樣是繼承ClassLoader
類
編寫一個測試類TestPerson
public class TestPerson { String name = "xiao ming"; public void print(){ System.out.println("hello my name is: "+ name); } }
接著 編寫一個自定義類加載器MyTestClassLoader:
public class MyTestClassLoader extends ClassLoader { final String classNameSpecify = "TestPerson"; public MyTestClassLoader() { } public MyTestClassLoader(ClassLoader parent) { super(parent); } protected Class<?> findClass(String name) throws ClassNotFoundException { File file = getClassFile(name); try { byte[] bytes = getClassBytes(file); Class<?> c = this.defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } private File getClassFile(String name) { File file = new File("D:\\ideaProjects\\src\\main\\java\\com\\zj\\ideaprojects\\test2\\"+ classNameSpecify+ ".class"); return file; } private byte[] getClassBytes(File file) throws Exception { // 這里要讀入.class的字節(jié),因此要使用字節(jié)流 FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel wbc = Channels.newChannel(baos); ByteBuffer by = ByteBuffer.allocate(1024); while (true) { int i = fc.read(by); if (i == 0 || i == -1) break; by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); } //我們這邊要打破雙親委派模型,重寫整個loadClass方法 @Override public Class<?> loadClass(String name) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); if (c == null && name.contains(classNameSpecify)){//指定的類,不走雙親委派機制,自定義加載 c = findClass(name); if (c != null){ return c; } } return super.loadClass(name); } }
最后在編寫一個測試controller:
@RestController public class TestClassController { @RequestMapping(value = "testClass",method = {RequestMethod.GET, RequestMethod.POST}) public void testClassLoader() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { MyTestClassLoader myTestClassLoader = new MyTestClassLoader(); Class<?> c1 = Class.forName("com.zj.ideaprojects.test2.TestPerson", true, myTestClassLoader); Object obj = c1.newInstance(); System.out.println("當前類加載器:"+obj.getClass().getClassLoader()); obj.getClass().getMethod("print").invoke(obj); } }
先找到TestPerson所在的目錄, 執(zhí)行命令:javac TestPerson
,生成TestPerson.class
這里沒有使用idea的build,是因為我們代碼的class讀取路徑 是寫死了的,不走默認CLASSPATH
D:\ideaProjects\src\main\java\com\zj\ideaprojects\test2\TestPerson.class
我們?nèi)缓笥胮ostman調(diào)用testClassLoader()測試接口
結(jié)果:
當前類加載器:com.zj.ideaprojects.test2.MyTestClassLoader@1d75e392
hello my name is: xiao ming
然后修改TestPerson,將name 改為 “xiao niu”
public class TestPerson { String name = "xiao niu"; public void print(){ System.out.println("hello my name is: "+ name); } }
然后在當前目錄 重新編譯, 執(zhí)行命令:javac TestPerson
,會在當前目錄重新生成TestPerson.class 不重啟項目,直接用postman 直接調(diào)這個測試接口 結(jié)果:
當前類加載器:com.zj.ideaprojects.test2.MyTestClassLoader@7091bd27
hello my name is: xiao niu
這樣就實現(xiàn)了“熱部署”?。?!
如果不打破的話,結(jié)果 當前類加載器會顯示"sun.misc.Launcher$AppClassLoader",原因是由于idea啟動項目的時候會自動幫我們編譯,將class放到 CLASSPATH路徑下。其實可以把默認路徑下的.class刪除也行。這里也是為了展示如何打破雙親委派機制,才如此實現(xiàn)的。
官方推薦我們自定義類加載器時,遵循雙親委派機制。但是凡事得看實際需求嘛
通過上面的例子我們可以看出:
1、如果不想打破雙親委派機制,我們自定義類加載器,那么只需要重寫findClass方法即可
2、如果想打破雙親委派機制,我們自定義類加載器,那么還得重寫整個loadClass方法
如果你閱讀到這里,你會發(fā)現(xiàn)雙親委派機制的各種好處,但萬物都不是絕對正確的,我們需要一分為二地看待問題。
在某些場景下雙親委派制過于局限,所以有時候必須打破雙親委派機制來達到目的。比如 :SPI機制、線程上下文類加載器
1.SPI(Service Provider Interface)服務(wù)提供接口。它是jdk內(nèi)置的一種服務(wù)發(fā)現(xiàn)機制,將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計中這個機制尤其重要,其核心思想就是 讓服務(wù)定義與實現(xiàn)分離、解耦。
SPI機制圖
2.線程上下文類加載器(context class loader)是可以破壞Java類加載委托機制,使程序可以逆向使用類加載器,使得java類加載體系顯得更靈活。
Java 應(yīng)用運行的初始線程的上下文類加載器是應(yīng)用類加載器,在線程中運行的代碼可以通過此類加載器來加載類和資源。Java.lang.Thread中的方法getContextClassLoader()和 setContextClassLoader(ClassLoader cl)
用來獲取和設(shè)置線程的上下文類加載器。如果沒有通過 setContextClassLoader(ClassLoader cl)
方法進行設(shè)置的話,線程將繼承其父線程的上下文類加載器。
SPI機制在框架的設(shè)計上應(yīng)用廣泛,下面舉幾個常用的例子:
平時獲取jdbc,我們可以這樣:Connection connection =DriverManager.getConnection("jdbc://localhost:3306");
我們讀DriverManager
的源碼發(fā)現(xiàn):其實就是查詢classPath下,所有META-INF下給定Class名的文件,并將其內(nèi)容返回,使用迭代器遍歷,這里遍歷的內(nèi)部使用Class.forName
加載了類。
其中有一處非常重要 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
我們看下它的實現(xiàn):
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader();//important ! return ServiceLoader.load(service, cl); }
我們可以看出JDBC,DriverManager
類和ServiceLoader
類都是屬于核心庫 rt.jar
的,它們的類加載器是Bootstrap ClassLoader類加載器。而具體的數(shù)據(jù)庫驅(qū)動相關(guān)功能卻是第三方提供的,第三方的類不能被引導(dǎo)類加載器(Bootstrap ClassLoader)加載。
所以java.util.ServiceLoader類進行動態(tài)裝載時,使用了線程的上下文類加載器(ThreadContextClassLoader)讓父級類加載器能通過調(diào)用子級類加載器來加載類,這打破了雙親委派機制。
Tomcat是web容器,我們把war包放到 tomcat 的webapp目錄下,這意味著一個tomcat可以部署多個應(yīng)用程序。
不同的應(yīng)用程序可能會依賴同一個第三方類庫的不同版本,但是不同版本的類庫中某一個類的全路徑名可能是一樣的。防止出現(xiàn)一個應(yīng)用中加載的類庫會影響另一個應(yīng)用的情況。如果采用默認的雙親委派類加載機制,那么是無法加載多個相同的類。
Tomcat類加載器種類
如果Tomcat本身的依賴和Web應(yīng)用還需要共享,Common類加載器(CommonClassLoader)來裝載實現(xiàn)共享
Catalina類加載器(CatalinaClassLoader) 用來 隔絕Web應(yīng)用程序與Tomcat本身的類
Shared類加載器(SharedClassLoader):如果WebAppClassLoader自身沒有加載到某個類,那就委托SharedClassLoader去加載
WebAppClassLoader:為了實現(xiàn)隔離性,優(yōu)先加載 Web 應(yīng)用自己定義的類,所以沒有遵照雙親委派的約定,每一個應(yīng)用自己的類加載器WebAppClassLoader(多個應(yīng)用程序,就有多個WebAppClassLoader),負責優(yōu)先加載
本身的目錄下的class文件,加載不到時再交給CommonClassLoader
以及上層的ClassLoader
進行加載,這破壞了雙親委派機制。
Jsp類加載器(JasperLoader):實現(xiàn)熱部署的功能,修改文件不用重啟就自動重新裝載類庫。JasperLoader
的加載范圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現(xiàn)的目的就是為了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader
的實例,并通過再建立一個新的Jsp類加載器來實現(xiàn)JSP文件的HotSwap
功能。
我們來模擬一下tomcat 多個版本代碼共存:
這邊的例子換了個電腦,所以目錄結(jié)構(gòu)、路徑與上面的例子有點變化
我們先編寫 App類
public class App { String name = "webapp 1"; public void print() { System.out.println("this is "+ name); } }
javac App生成的App.class 放入 tomcatTest\war1\com\zj\demotest\tomcatTest
目錄下
將name改為webapp 2
,重新生成的App.class
放入 tomcatTest\war2\com\zj\demotest\tomcatTest
目錄下
然后我們編寫類加載器:
public class MyTomcatClassloader extends ClassLoader { private String classPath; public MyTomcatClassloader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { File file = getClassFile(name); try { byte[] bytes = getClassBytes(file); Class<?> c = this.defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } private File getClassFile(String name) { name = name.replaceAll("\\.", "/"); File file = new File(classPath+ "/"+ name + ".class");//拼接路徑,找到class文件 return file; } private byte[] getClassBytes(File file) throws Exception { // 這里要讀入.class的字節(jié),因此要使用字節(jié)流 FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel wbc = Channels.newChannel(baos); ByteBuffer by = ByteBuffer.allocate(1024); while (true) { int i = fc.read(by); if (i == 0 || i == -1) { break; } by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); } //我們這邊要打破雙親委派模型,重寫整個loadClass方法 @Override public Class<?> loadClass(String name) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); if (c == null && name.contains("tomcatTest")){//指定的目錄下的類,不走雙親委派機制,自定義加載 c = findClass(name); if (c != null){ return c; } } return super.loadClass(name); } }
最后編寫測試controller:
@RestController public class TestController { @RequestMapping(value = "/testTomcat",method = {RequestMethod.GET, RequestMethod.POST}) public void testclass() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { MyTomcatClassloader myTomcatClassloader = new MyTomcatClassloader("D:\\GiteeProjects\\study-java\\demo-test\\src\\main\\java\\com\\zj\\demotest\\tomcatTest\\war1"); Class cl = myTomcatClassloader.loadClass("com.zj.demotest.tomcatTest.App"); Object obj = cl.newInstance(); System.out.println("當前類加載器:"+obj.getClass().getClassLoader()); obj.getClass().getMethod("print").invoke(obj); MyTomcatClassloader myTomcatClassloader22 = new MyTomcatClassloader("D:\\GiteeProjects\\study-java\\demo-test\\src\\main\\java\\com\\zj\\demotest\\tomcatTest\\war2"); Class cl22 = myTomcatClassloader22.loadClass("com.zj.demotest.tomcatTest.App"); Object obj22 = cl22.newInstance(); System.out.println("當前類加載器:"+obj22.getClass().getClassLoader()); obj22.getClass().getMethod("print").invoke(obj22); } }
然后postman 調(diào)一下這個接口, 結(jié)果:
當前類加載器:com.zj.demotest.tomcatTest.MyTomcatClassloader@18fbb876
this is webapp 1
當前類加載器:com.zj.demotest.tomcatTest.MyTomcatClassloader@5f7ed4a9
this is webapp 2
我們發(fā)現(xiàn)2個同樣的類能共存在同一個JVM中,互不影響。
注意:同一個JVM內(nèi),2個相同的包名和類名的對象是可以共存的,前提是他們的類加載器不一樣。所以我們要判斷多個類對象是否是同一個,除了要看包名和類名相同,還得注意他們的類加載器是否一致
springboot自動配置的原因是因為使用了@EnableAutoConfiguration
注解。
當程序包含了EnableAutoConfiguration
注解,那么就會執(zhí)行下面的方法,然后會加載所有spring.factories
文件,將其內(nèi)容封裝成一個map,spring.factories
其實就是一個名字特殊的properties文件。
在spring-boot應(yīng)用啟動時,會調(diào)用loadFactoryNames方法,其中傳遞的一個參數(shù)就是:org.springframework.boot.autoconfigure.EnableAutoConfiguration
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct."); return configurations; }
META-INF/spring.factories會被讀取到。
它還使用了this.getBeanClassLoader() 獲取類加載器。所以我們立刻明白了文章一開始的例子,SpringBoot項目直接build項目,不重啟項目,就能實現(xiàn)熱部署效果。
感謝各位的閱讀,以上就是“Java類加載器與雙親委派機制怎么應(yīng)用”的內(nèi)容了,經(jīng)過本文的學習后,相信大家對Java類加載器與雙親委派機制怎么應(yīng)用這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。