閱讀這篇文章,你會瞭解到:
1.上面是類加載器
2.爲什麼應該叫做“父委派模型”,而不是“雙親委派機制”
3.在JNDI中,“父委派模型”是怎麼被違背的
4.不只是JNDI,還有TOMCAT的類加載器模型是怎樣的,他們有無違背“父委派模型”?
一.什麼是類加載器
講“雙親委派機制”前,要先要講一講類和類加載器的關係
1.類(Class)
我們在編寫代碼時,創建的每個“*.java”文件都可以認爲是一個類,我們使用“class”去定義一個類,例如String.java。
2.類加載器(Class Loader)
(1)我們定義的類,如果我們要在編碼中用到這個類,首先就是要先把“*.java”這個文件編譯成class文件,然後由對應的“類加載器”加載到JVM中,我們才能夠使用這個“類對象”。
(2)一般的場景下,類的加載是在我們程序啓動的時候由jvm來完成,但是有些場景可能需要我們手動去指定加載某個類或找到某個類,這時候就要用到 Class.forName(String className) 加載/找到 這個className對應的類。
(3)如果比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來自同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那這個兩個類就必定不相等。
在我們日常使用中,類加載器默認有下面3種:
(1)Bootstrap Class Loader:
JDK自帶的一款類加載器,用於加載JDK內部的類。Bootstrap類加載器用於加載JDK中$JAVA_HOME/jre/lib下面的那些類,比如rt.jar包裏面的類。
(2)Extension Class Loader
主要用於加載JDK擴展包裏的類。一般$JAVA_HOME/lib/ext下面的包都是通過這個類加載器加載的,這個包下面的類基本上是以javax開頭的。
(3)Application Class Loader
用來加載開發人員自己平時寫的應用代碼的類的,加載存放在classpath路徑下的那些應用程序級別的類的。
二.爲什麼應該叫做“父委派模型”,而不是“雙親委派機制”?
這是個很蛋疼的翻譯問題,實際上在oracle官方文檔上,人家是這樣描述的:
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.
java平臺通過委派模型去加載類。每個類加載器都有一個父加載器。當需要加載類時,會優先委派當前所在的類的加載器的父加載器去加載這個類。如果父加載器無法加載到這個類時,再嘗試在當前所在的類的加載器中加載這個類。
所以,java的類加載機制應該叫做“父委派模型”,不應該叫做“雙親委派機制”,“雙親委派機制”這個名字太具有誤導性了。
三.“父委派模型”是怎麼工作的?
舉個例子,當前有個Test.class,需要加載rt.jar中的java.lang.String,那麼加載的流程如下圖所示,整體的加載流程是向上委託父加載器完成的。
如果整個鏈路中,父加載器都沒有加載這個類,且無法加載這個類時,纔會由Test.class所在的加載器去加載某個類(例如希望加載開發人員自定義的類 Test2.class)。
四.“父委派模型”有什麼好處?
“父委派模型”保證了系統級別的類的安全性,使一些基礎類不會受到開發人員“定製化”的破壞。
如果沒有使用父委派模型,而是由各個類加載器自行加載的話,如果開發人員自己編寫了一個稱爲java.lang.String的類,並放在程序的ClassPath中,那系統將會出現多個不同的String類, Java類型體系中最基礎的行爲就無法保證。應用程序也將會變得一片混亂。
五. “父委派模型”什麼時候會遭到破壞?
- 通過預加載的方式;
- 通過Thread.getContextClassLoader();
1.通過預加載的方式
這裏通過一個簡單的例子,就拿sql連接來說:
(1)java.sql.DriverManager:rt.jar包中的類,通過Bootstrap加載器加載。
(2)DriverTest:開發人員自定義的實現了java.sql.Driver接口的類型,通過App加載器加載。
開發人員通過DriverManager.registerDriver方法把自己實現的獲取連接的Driver實現類加載並註冊到DriverManager中。然後DriverManager.getConnection方法會遍歷所有註冊的Driver,並觸發Driver的connect接口來獲取連接。(即繞過在DriverManager所在的Bootstrap加載器,因爲Bootstrap加載器不能加載開發人員實現的Driver類)
定義一個 DriverTest 類,實現rt.jar裏面的java.sql.Driver接口
public class DriverTest implements Driver {
static {
try {
java.sql.DriverManager.registerDriver(new DriverTest());
System.out.println("who load DriverTest: " + DriverTest.class.getClassLoader());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
@Override
public Connection connect(String url, Properties info) throws SQLException {
return new Connection() {
//此處省略一堆代碼......
}
}
//啓動代碼
public static void main(String[] args) {
try {
//由AppClassLoader加載DriverTest類
Class.forName("com.jenson.pratice.classloader.DriverTest");
System.out.println("who load DriverManager: "+DriverManager.class.getClassLoader());
//通過rt.jar中的DriverManager去獲取鏈接,DriverManager由BootstrapClassLoader加載
Connection connection = DriverManager.getConnection("jdbc://");
} catch (Exception e) {
e.printStackTrace();
}
}
}
此時運行main方法打印:
who load DriverTest: sun.misc.Launcher$AppClassLoader@18b4aac2
who load DriverManager: null
Process finished with exit code 0
---------------------
DriverManager是由Bootstrap加載器的,因而獲取不了Bootstrap加載器,所以爲null。從父委派模型的機制上看,因爲rt.jar是由Bootstrap加載器加載的,所以裏面的類,都不能用到rt.jar以外的類。
那麼DriverManager.getConnection是怎麼調用DriverTest(App加載器)的getConnection方法呢?
因爲父委派模型的限制,DriverManager不可能自己去加載DriverTest,DriverTest的加載實際上是由AppClassLoader完成的,DriverTest裏面會往
DriverManager中註冊一個驅動。
public class DriverTest implements java.sql.Driver {
static {
try {
//在這裏註冊
java.sql.DriverManager.registerDriver(new DriverTest());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
對於DriverManager而言,他不關注driver的加載,他只需要遍歷“registeredDrivers”,然後檢查驅動類是否能被“調用類的類加載器”識別,如果可以識別,則調用driver.connect方法(即DriverTest中的實現)
public class DriverManager{
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//省略一堆代碼
for(DriverInfo aDriver : registeredDrivers) {
//在這裏做安全校驗
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
//在這裏調用DriverTest的connect方法
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
//省略一堆代碼
整體的流程是這樣的
所以可以看到,在DriverManager中要調用DriverTest的方法,並沒有通過“父委派模型”去加載DriverTest,而是由下層的類加載器自行完成類的加載。這裏實際上是繞過了“父委派模型”的機制。
2. 通過Thread.getContextClassLoader
Thread類中有一個contextClassLoader屬性,稱爲上下文類加載器。在實例化一個線程時,如果沒有設置contextClassLoader屬性,默認會從父線程中繼承。如果在應用程序的全局範圍內都沒有設置過多的話,默認爲Application Class Loader。
舉個例子:rt.jar中的 javax.xml.parsers.FactoryFinder 中的 newInstance方法:
(1)在newInstance中會用到 getProviderClass 方法
(2)在getProviderClass中會用到 SecuritySupport.getContextClassLoader方法
(3)在SecuritySupport.getContextClassLoader中會用到Thread.currentThread().getContextClassLoader()拿到線程上下文類加載器
/**
* Create an instance of a class. Delegates to method
* <code>getProviderClass()</code> in order to load the class.
*
* 定義的JNDI接口
* @param type Base class / Service interface of the factory to instantiate
* JNDI的實現類名
* @param className Name of the concrete class corresponding to the service provider
* 加載器:如果爲null,則通過線程的上下文加載器進行加載
* @param cl <code>ClassLoader</code> used to load the factory class. If <code>null</code>
* current <code>Thread</code>'s context classLoader is used to load the factory class.
* 如果爲true,則使用bootstrap加載器。
* @param useBSClsLoader True if cl=null actually meant bootstrap classLoader. This parameter
* is needed since DocumentBuilderFactory/SAXParserFactory defined null as context classLoader.
*/
static <T> T newInstance(Class<T> type, String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader)
throws FactoryConfigurationError
{
//省略一堆代碼
try {
//在這個方法裏面,可以通過線程上下文加載器進行加載className對應的類
Class<?> providerClass = getProviderClass(className, cl, doFallback, useBSClsLoader);
//省略一堆代碼
static private Class<?> getProviderClass(String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
try {
if (cl == null) {
if (useBSClsLoader) {
return Class.forName(className, false, FactoryFinder.class.getClassLoader());
} else {
//在這裏,會獲得線程的上下文加載器去加載類
//其中 ss是 SecuritySupport.java
cl = ss.getContextClassLoader();
if (cl == null) {
throw new ClassNotFoundException();
}
else {
return Class.forName(className, false, cl);
}
}
}
//省略一堆代碼
}
class SecuritySupport {
ClassLoader getContextClassLoader() throws SecurityException{
return (ClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
ClassLoader cl = null;
//try {
//獲得線程的上下文加載器
cl = Thread.currentThread().getContextClassLoader();
//} catch (SecurityException ex) { }
if (cl == null)
cl = ClassLoader.getSystemClassLoader();
return cl;
}
});
}
六.關於tomcat的類加載機制
不只是Driver驅動的實現是這樣,在tomcat、spring等等的容器框架也是通過一些手段去繞過“父委派機制”。
例如下圖中的tomat類加載器的結構:
從圖中的委派關係中可以看出:
- CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,從而實現了公有類庫的共用。
- CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對方相互隔離。
- WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。
- JasperLoader的加載範圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是爲了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並通過再建立一個新的Jsp類加載器來實現JSP文件的HotSwap功能。
那麼tomcat 違背了父委派模型嗎?
tomcat 違背了父委派模型。因爲雙親委派模型要求除了頂層的啓動類加載器之外,其餘的類加載器都應當由自己的父類加載器加載。
而tomcat 不是這樣實現,tomcat 爲了實現隔離性,沒有遵守這個約定,每個webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器。
七.參考文檔
https://www.cnblogs.com/tiancai/p/9317299.html
https://blog.csdn.net/qq_38182963/article/details/78660779
https://www.cnblogs.com/doit8791/p/5820037.html
https://blog.csdn.net/lengxiao1993/article/details/86689331