Java中的ClassLoader和SPI机制
深入探討 Java 類加載器
成富是著名的Java專家,在IBM技術網站發表很多Java好文,也有著作。
線程上下文類加載器
線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入的。類?java.lang.Thread中的方法?getContextClassLoader()和?setContextClassLoader(ClassLoader cl)用來獲取和設置線程的上下文類加載器。如果沒有通過?setContextClassLoader(ClassLoader cl)方法進行設置的話,線程將繼承其父線程的上下文類加載器。Java 應用運行的初始線程的上下文類加載器是系統類加載器。在線程中運行的代碼可以通過此類加載器來加載類和資源。
前面提到的類加載器的代理模式并不能解決 Java 應用開發中會遇到的類加載器的全部問題。Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在?javax.xml.parsers包中。這些 SPI 的實現代碼很可能是作為 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI 的?Apache Xerces所包含的 jar 包。SPI 接口中的代碼經常需要加載具體的實現類。如 JAXP 中的?javax.xml.parsers.DocumentBuilderFactory類中的?newInstance()方法用來生成一個新的?DocumentBuilderFactory的實例。這里的實例的真正的類是繼承自?javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實現所提供的。如在 Apache Xerces 中,實現的類是?org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在于,SPI 的接口是 Java 核心庫的一部分,是由引導類加載器來加載的;SPI 實現的 Java 類一般是由系統類加載器來加載的。引導類加載器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。它也不能代理給系統類加載器,因為它是系統類加載器的祖先類加載器。也就是說,類加載器的代理模式無法解決這個問題。
線程上下文類加載器正好解決了這個問題。如果不做任何的設置,Java 應用的線程的上下文類加載器默認就是系統上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實現的類。線程上下文類加載器在很多 SPI 的實現中都會用到。
?
譯文:走出類加載器迷宮
原文:https://www.javaworld.com/article/2077344/find-a-way-out-of-the-classloader-maze.html
Q:我什么時候該用Thread.getContextClassLoader()?
當動態加載一個資源時,至少有三種類加載器可供選擇:?系統類加載器(也被稱為應用類加載器)(system?classloader),當前類加載器(current?classloader),和當前線程的上下文類加載器(?the current thread?context?classloader)。上面提到的問題指的是最后一種加載器。
容易排除的一個選擇:系統類加載器。這個類加載器處理classpath環境變量所指定的路徑下的類和資源,可以通過ClassLoader.getSystemClassLoader()方法以編程式訪問。所有的ClassLoader.getSystemXXX()API方法也是通過這個類加載器訪問。
當前類加載器加載和定義當前方法所屬的那個類。這個類加載器在你使用帶單個參數的Class.forName()方法,Class.getResource()方法和相似方法時會在運行時類的鏈接過程中被隱式調用。
線程上下文類加載器是在J2SE中被引進的。每一個線程分配一個上下文類加載器(除非線程由本地代碼創建)。該加載器是通過Thread.setContextClassLoader()方法來設置。如果你在線程構造后不調用這個方法,這個線程將會從它的父線程中繼承上下文類加載器。如果你在整個應用中不做任何設置,所有線程將以系統類加載器作為它們自己的上下文加載器。重要的是明白自從Web和J2EE應用服務器為了像JNDI,線程池,組件熱部署等特性而采用復雜的類加載器層次結構后,這是很少見的情況。
上下文類加載器提供了一個后門繞過在J2SE中介紹的類的加載委托機制。通常情況下,一個JVM中的所有類加載器被組織成一個層次結構,使得每一個類加載器(除了啟動整個JVM的原始類加載器)都有一個父加載器。當被要求加載一個類時,每一個類加載器都將先委托父加載器來加載,只有父加載器都不能成功加載時當前類加載器才會加載。
有時這種加載順序不能正常工作,通常發生在有些JVM核心代碼必須動態加載由應用程序開發人員提供的資源時。以JNDI舉例:它的核心內容(從J2SE1.3開始)在rt.jar中的引導類中實現了,但是這些JNDI核心類可能加載由獨立廠商實現和部署在應用程序的classpath中的JNDI提供者。這個場景要求一個父類加載器(這個例子中的原始類加載器,即加載rt.jar的加載器)去加載一個在它的子類加載器(系統類加載器)中可見的類。此時通常的J2SE委托機制不能工作,解決辦法是讓JNDI核心類使用線程上下文加載器,從而有效建立一條與類加載器層次結構相反方向的“通道”達到正確的委托。
?
Tomcat官網類裝載機如何操作
?
【Tomcat學習筆記】9-ClassLoader
Tomcat的三大ClassLoader
為什么 Tomcat 里要自定義 ClassLoader 呢,先來考慮一個問題:一個Tomcat 部署兩個應用,App1 和 App2, App1 里定義了一個 com.fdx.AAA 類,App2 也定義了一個 com.fdx.AAA 類,但是里面的實現是不一樣的,如果不自定義 ClassLoader,
而都用 AppClassLoader 來加載的話,你讓它加載哪一個呢,一個 ClassLoader 是不能加載兩個一樣的類的。所以,ClassLoader 最重要的一個功能就是 類隔離。
?
SPI機制
JavaSPI 實際上是“基于接口的編程+策略模式+配置文件”組合實現的動態加載機制。具體而言:
? ? ? ?STEP1. 定義一組接口, 假設是 autocomplete.PrefixMatcher;
? ? ? ?STEP2. 寫出接口的一個或多個實現(autocomplete.EffectiveWordMatcher,?autocomplete.SimpleWordMatcher);
? ? ? ?STEP3. 在 src/main/resources/ 下建立 /META-INF/services 目錄, 新增一個以接口命名的文件?autocomplete.PrefixMatcher, 內容是要應用的實現類(autocomplete.EffectiveWordMatcher 或 autocomplete.SimpleWordMatcher 或兩者);
? ? ? ?STEP4. 使用 ServiceLoader 來加載配置文件中指定的實現。?
SPI 的應用之一是可替換的插件機制。比如查看 JDBC 數據庫驅動包,mysql-connector-java-5.1.18.jar?就有一個 /META-INF/services/java.sql.Driver 里面內容是?com.mysql.jdbc.Driver 。
package org.foo.demo;public interface IShout {void shout();
}
package org.foo.demo;import java.util.ServiceLoader;public class SPIMain {public static void main(String[] args) {ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);for (IShout s : shouts) {s.shout();}System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());}
}
package org.foo.demo.animal;import org.foo.demo.IShout;public class Cat implements IShout {@Overridepublic void shout() {System.out.println("喵喵");System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());}
}
package org.foo.demo.animal;import org.foo.demo.IShout;public class Dog implements IShout {@Overridepublic void shout() {System.out.println("旺旺");System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());}
}
《高級開發必須理解的Java中SPI機制》
優點:
使用Java SPI機制的優勢是實現解耦,使得第三方服務模塊的裝配控制的邏輯與調用者的業務代碼分離,而不是耦合在一起。應用程序可以根據實際業務情況啟用框架擴展或替換框架組件。
缺點:
- 雖然ServiceLoader也算是使用的延遲加載,但是基本只能通過遍歷全部獲取,也就是接口的實現類全部加載并實例化一遍。如果你并不想用某些實現類,它也被加載并實例化了,這就造成了浪費。獲取某個實現類的方式不夠靈活,只能通過Iterator形式獲取,不能根據某個參數來獲取對應的實現類。
- 多個并發多線程使用ServiceLoader類的實例是不安全的。
---------------------------------------------
Tomcat的類加載機制是違反了雙親委托原則的,對于一些未加載的非基礎類(Object,String等),各個web應用自己的類加載器(WebAppClassLoader)會優先加載,加載不到時再交給commonClassLoader走雙親委托。?
對于JVM來說:
因此,按照這個過程可以想到,如果同樣在CLASSPATH指定的目錄中和自己工作目錄中存放相同的class,會優先加載CLASSPATH目錄中的文件。
1、既然 Tomcat 不遵循雙親委派機制,那么如果我自己定義一個惡意的HashMap,會不會有風險呢?
答: 顯然不會有風險,如果有,Tomcat都運行這么多年了,那群Tomcat大神能不改進嗎? tomcat不遵循雙親委派機制,只是自定義的classLoader順序不同,但頂層還是相同的,
還是要去頂層請求classloader.
2、我們思考一下:Tomcat是個web容器, 那么它要解決什么問題:?
1. 一個web容器可能需要部署兩個應用程序,不同的應用程序可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個服務器只有一份,因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。?
2. 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果服務器有10個應用程序,那么要有10份相同的類庫加載進虛擬機,這是扯淡的。?
3. web容器也有自己依賴的類庫,不能于應用程序的類庫混淆。基于安全考慮,應該讓容器的類庫和程序的類庫隔離開來。?
4. web容器要支持jsp的修改,我們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機中運行,但程序運行后修改jsp已經是司空見慣的事情,否則要你何用? 所以,web容器需要支持 jsp 修改后不用重啟。
再看看我們的問題:Tomcat 如果使用默認的類加載機制行不行??
答案是不行的。為什么?我們看,第一個問題,如果使用默認的類加載器機制,那么是無法加載兩個相同類庫的不同版本的,默認的累加器是不管你是什么版本的,只在乎你的全限定類名,并且只有一份。第二個問題,默認的類加載器是能夠實現的,因為他的職責就是保證唯一性。第三個問題和第一個問題一樣。我們再看第四個問題,我們想我們要怎么實現jsp文件的熱修改(樓主起的名字),jsp 文件其實也就是class文件,那么如果修改了,但類名還是一樣,類加載器會直接取方法區中已經存在的,修改后的jsp是不會重新加載的。那么怎么辦呢?我們可以直接卸載掉這jsp文件的類加載器,所以你應該想到了,每個jsp文件對應一個唯一的類加載器,當一個jsp文件修改了,就直接卸載這個jsp類加載器。重新創建類加載器,重新加載jsp文件。
《一看你就懂,超詳細java中的ClassLoader詳解》
《java中的反射》
《深入探討Java類加載機制》
《Java類加載器ClassLoader總結》
《圖解Tomcat類加載機制(阿里面試題)》
《java attach機制源碼閱讀》
=====================
《Class熱替換與卸載》代碼實現
所以一個class被一個ClassLoader實例加載過的話,就不能再被這個ClassLoader實例再次加載(這里的加載指的是,調用了defileClass(...)方法,重新加載字節碼、解析、驗證)。而系統默認的AppClassLoader加載器,他們內部會緩存加載過的class,重新加載的話,就直接取緩存。所與對于熱加載的話,只能重新創建一個ClassLoader,然后再去加載已經被加載過的class文件。
GIT@OSC工程路徑:http://git.oschina.net/taomk/king-training/tree/master/class-loader
------------------
《Java服務器熱部署的實現原理》
Java中類的加載方式。每一個應用程序的類都會被ClassLoader加載,所以,要實現一個支持熱部署的應用,我們可以對每一個用戶自定義的應用程序使用一個單獨的ClassLoader進行加載。然后,當某個用戶自定義的應用程序發生變化的時候,我們首先銷毀原來的應用,然后使用一個新的ClassLoader來加載改變之后的應用。而所有其他的應用程序不會受到一點干擾。
?
有了總體實現思路之后,我們可以想到如下幾個需要完成的目標:
1、定義一個用戶自定義應用程序的接口,這是因為,我們需要在容器應用中去加載用戶自定義的應用程序。
2、我們還需要一個配置文件,讓用戶去配置他們的應用程序。
3、應用啟動的時候,加載所有已有的用戶自定義應用程序。
4、為了支持熱部署,我們需要一個監聽器,來監聽應用發布目錄中每個文件的變動。這樣,當某個應用重新部署之后,我們就可以得到通知,進而進行熱部署處理。
?
要實現熱部署,我們之前說過,需要一個監聽器,來監聽發布目錄applications,這樣當某個應用程序的jar文件改變時,我們可以進行熱部署處理。其實,要實現目錄文件改變的監聽,有很多種方法,這個例子中我使用的是apache的一個開源虛擬文件系統——common-vfs。如果你對其感興趣,你可以訪問http://commons.apache.org/proper/commons-vfs/。
當某個文件改變的時候,該方法會被回調。所以,我們在這個方法中調用了ApplicationManager的reloadApplication方法,重現加載該應用程序。
public void reloadApplication (String name){IApplication oldApp = this.apps .remove(name);if(oldApp == null){return;}oldApp.destory(); //call the destroy method in the user's applicationAppConfig config = this.configManager .getConfig(name);if(config == null){return;}createApplication(getBasePath(), config);}
重新加載應用程序時,我們首先從內存中刪除該應用程序,然后調用原來應用程序的destory方法,最后按照配置重新創建該應用程序實例。
為了讓整個應用程序可以持續的運行而不會結束,我們修改下啟動方法無限循環,300ms.
public static void main(String[] args){Thread t = new Thread(new Runnable() {@Overridepublic void run() {ApplicationManager manager = ApplicationManager.getInstance();manager.init();}});t.start();while(true ){try {Thread. sleep(300);} catch (InterruptedException e) {e.printStackTrace();}}}
?
談談Java的的SPI機制
當服務的提供者提供了服務接口的一種實現之后,必須根據SPI約定在 META-INF/services/目錄里創建一個以服務接口命名的文件,該文件里寫的就是實現該服務接口的具體實現類。當程序調用ServiceLoader的load方法的時候,ServiceLoader能夠通過約定的目錄找到指定的文件,并裝載實例化,完成服務的發現。
JDBC中的SPI機制
回到之前的一個問題,為什么只需要下面的一行代碼,再提供商不同廠商的jar包,就可以輕松創建連接了呢?
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
DriverManager中有一個靜態代碼塊,在調用getConnection之前就會被調用:
/*** Load the initial JDBC drivers by checking the System property* jdbc.properties and then use the {@code ServiceLoader} mechanism*/static {loadInitialDrivers();println("JDBC DriverManager initialized");}private static void loadInitialDrivers() {String drivers;// 1、處理系統屬性jdbc.drivers配置的值try {drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// If the driver is packaged as a Service Provider, load it.// Get all the drivers through the classloader// exposed as a java.sql.Driver.class service.// ServiceLoader.load() replaces the sun.misc.Providers()AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {
// 2、處理通過ServiceLoader加載的Driver類ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();/* Load these drivers, so that they can be instantiated.* It may be the case that the driver class may not be there* i.e. there may be a packaged driver with the service class* as implementation of java.sql.Driver but the actual class* may be missing. In that case a java.util.ServiceConfigurationError* will be thrown at runtime by the VM trying to locate* and load the service.** Adding a try catch block to catch those runtime errors* if driver not available in classpath but it's* packaged as service and that service is there in classpath.*/
// 加載配置在META-INF/services/java.sql.Driver文件里的Driver實現類try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});println("DriverManager.initialize: jdbc.drivers = " + drivers);if (drivers == null || drivers.equals("")) {return;}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for (String aDriver : driversList) {try {println("DriverManager.Initialize: loading " + aDriver);
// 3、加載driver類Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());} catch (Exception ex) {println("DriverManager.Initialize: load failed: " + ex);}}}
JDBC使用了SPI機制,讓所有的任務都交給不同的數據庫廠商各自去完成,無論是實現Driver接口,還是SPI要求的接口文件,都做到了讓用戶不需要關心一點細節,一行代碼建立連接。
?
[討論]?關于Thread.getContextClassLoader的使用場景問題
=================
以下出自尚學堂高琪課程 ,說的還是比較全面和準確的
類加載器的作用
– 將class文件字節碼內容加載到內存中,并將這些靜態數據轉換成方法區中的運行時數據結構,在堆中生成一個代表這個類的java.lang.Class對象,作為方法區類數據的訪問入口。
? 類緩存
? 標準的Java SE類加載器可以按要求查找類,但一旦某個類被加載到類加載器中,它將維持加載(緩存)一段時間。不過,JVM垃圾收集器可以回收這些Class對象。
?
類加載器的代理模式
? 代理模式
– 交給其他加載器來加載指定的類
? 雙親委托機制
– 就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次追溯,直到最高的爺爺輩的,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
– 雙親委托機制是為了保證 Java 核心庫的類型安全。
? 這種機制就保證不會出現用戶自己能定義java.lang.Object類的情況。
– 類加載器除了用于加載類,也是安全的最基本的屏障。
? 雙親委托機制是代理模式的一種
– 并不是所有的類加載器都采用雙親委托機制。
– tomcat服務器類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器。
這與一般類加載器的順序是相反的
?
自定義類加載器的流程:
– 1、首先檢查請求的類型是否已經被這個類裝載器裝載到命名空間中了,如果已經裝載,直接返回;否則轉入步驟2
– 2、委派類加載請求給父類加載器(更準確的說應該是雙親類加載器,真個虛擬機中各種類加載器最終會呈現樹狀結構),如果父類加載器能夠完成,則返回父類加載器加載的Class實例;否則轉入步驟3
– 3、調用本類加載器的findClass(…)方法,試圖獲取對應的字節碼,如果獲取的到,則調用defineClass(…)導入類型到方法區;如果獲取不到對應的字節碼或者其他原因失敗,返回異常給loadClass(…), loadClass(…)轉拋異常,終止加載過程(注意:這里的異常種類不止一種)。
– 注意:被兩個類加載器加載的同一個類,JVM不認為是相同的類。
? 文件類加載器
? 網絡類加載器
? 加密解密類加載器(取反操作,DES對稱加密解密)?
?
線程上下文類加載器
雙親委托機制以及默認類加載器的問題
– 一般情況下, 保證同一個類中所關聯的其他類都是由當前類的類加載器所加載的.。
比如,ClassA本身在Ext下找到,那么他里面new出來的一些類也就只能用Ext去查找了(不會低一個級別),所以有
些明明App可以找到的,卻找不到了。
– JDBC API,他有實現的driven部分(mysql/sql server),我們的JDBC API都是由Boot或者Ext來載入的,但是
JDBC driver卻是由Ext或者App來載入,那么就有可能找不到driver了。在Java領域中,其實只要分成這種Api+SPI(
Service Provide Interface,特定廠商提供)的,都會遇到此問題。
– 常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定
義包含在 javax.xml.parsers 包中。SPI 的接口是 Java 核心庫的一部分,是由引導類加載器來加載的;SPI 實現的
Java 類一般是由系統類加載器來加載的。引導類加載器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。
? 通常當你需要動態加載資源的時候 , 你至少有三個 ClassLoader 可以選擇 :
– 1.系統類加載器或叫作應用類加載器 (system classloader or application classloader)
– 2.當前類加載器
– 3.當前線程類加載器
? 當前線程類加載器是為了拋棄雙親委派加載鏈模式。
– 每個線程都有一個關聯的上下文類加載器。
如果你使用new Thread()方式生成新的線程,新線程將繼承其父線程的上下文類加載器。
如果程序對線程上下文類加載器沒有任何改動的話,程序中所有的線程將都使用系統類加載器作為上下文類加載器。
? Thread.currentThread().getContextClassLoader()
public class TCCC {public static void main(String[] args) throws Exception {ClassLoader loader = TCCC.class.getClassLoader();System.out.println(loader);ClassLoader loader2 = Thread.currentThread().getContextClassLoader();System.out.println(loader2);Thread.currentThread().setContextClassLoader(new FileSystemClassLoader("d:/"));System.out.println(Thread.currentThread().getContextClassLoader());Class<Test> c = (Class<Test>) Thread.currentThread().getContextClassLoader().loadClass("com.current.www.Test");System.out.println(c);System.out.println(c.getClassLoader());}
}
TOMCAT服務器的類加載機制
? 一切都是為了安全!
– TOMCAT不能使用系統默認的類加載器。
? 如果TOMCAT跑你的WEB項目使用系統的類加載器那是相當危險的,你可以直接是無忌憚操作操作系統的各個目錄了。
? 對于運行在 Java EE?容器中的 Web 應用來說,類加載器的實現方式與一般的 Java 應用有所不同。
? 每個 Web 應用都有一個對應的類加載器實例。該類加載器也使用代理模式(不同于前面說的雙親委托機制),所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的但也是為了保證安全,這樣核心庫就不在查詢范圍之內。
? 為了安全TOMCAT需要實現自己的類加載器。
? 我可以限制你只能把類寫在指定的地方,否則我不給你加載!
總結
以上是生活随笔為你收集整理的Java中的ClassLoader和SPI机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我爱你你爱她她爱他这是什么歌?
- 下一篇: Go语言源码分析CAS的实现和Java如