一、背景
朋友:在我知識體系中ClassLoader
的雙親委派機制是流暢絲滑的,可是看到通過委派執行類加載來保障這種分治能力,進而達到了類資源的隔離性突然就感覺有點陌生和排斥呢?
我:類的命名空間有了解嘛?
朋友:你是說package
嘛?
我:我說的是ClassLoader
中的 nameSpace
朋友:啥玩意兒?
本篇是對ClassLoader
中的namespace
做個直觀的介紹和驗證。這個知識點筆者個人認爲很重要,多次幫助筆者解決日常工作中遇到的疑難雜症,如果你尚未認真研究過ClassLoader
,但懵懂的認知讓你覺得這個應該很簡單,那請看下圖,若看不懂則表明你可能並不瞭解ClassLoader
中得一些關鍵邏輯;不販賣焦慮,不感興趣則忽略,知道個大概即可,不會它並不影響你做一個優秀的程序員。
二、Classloader的分治和委派機制
上下文同步,已瞭解這部分的讀者朋友可直接跳過,進過第三小節。
ClassLoader
是個抽象類,其子類 UrlClassLoader
引入 URL
資源概念,以此方式來約束自己只加載 URL
覆蓋範圍內的類文件;ExtClassLoader
、AppClassLoader
都是 UrlClassLoader
的子類,各自分管的資源路徑不同,即給定的 URL
不同則管轄範圍不同,並通過委派執行類加載來保障這種分治能力,進而達到了類資源的隔離性。
上圖是標準的委派機制,總結爲2個方面:
-
父加載器能加載 父加載器來加載:
- 自己在加載資源之前,先讓父類加載器去加載。父類再找其父類,直到
BootStrapClassLoader
(它沒有父類加載器)。 - 保證了等級越高,加載的優先權越高
- 自己在加載資源之前,先讓父類加載器去加載。父類再找其父類,直到
-
父加載器不加載 我就來加載(findClass);我加載不了的子加載器來加載:
- 若父類加載器沒有加載成功,才逐級下放這個加載權。
- 子類加載器不能加載父類加載器能加載的類,如
java.lang.String
,即使用戶自己編造一份這個類型,啓動類加載器優先將java.lang.String
加載成功後,應用類加載器就不會再加載用戶自己編造的。
下圖描述了 SkyWalking Agent 通過自定義的類加載器AgentClassLoader
加載插件中的類,不會對宿主應用中的類產生污染。
三、namespace
是什麼?
每個類加載器都對應一個namespace
,漢化叫命名空間(我個人其實更喜歡漢化爲名字空間),命名空間由該加載器及所有父類加載器所加載的類組成。這樣的介紹很抽象,網絡資料中也多是這麼幾句話,我會從更多的細粒度視角帶您進一步瞭解它。
3.1 同一個命名空間中,類只加載一份
比如AppClassLoader
加載程序中編寫的類。無論加載多少次,只要是被AppClassLoader
加載的,其Class
信息的hashcode
都是相同的。
3.2 子加載器可見父加載器加載的類
這個更容易舉例,核心類庫的類由BootStrapClassLoder
加載的,比如String
;由AppClassLoader
所加載的類,都能使用String
對吧?
3.3 父加載器不可見子加載器所加載的類
SPI
技術的誕生也是這個原因,什麼是SPI
:程序運行過程中要用到的類,通過當前類加載器
的自動加載
,加載不到(不在當前類加載器的類資源管轄範圍),如果要使用這個類,必須指定一個能夠加載這個類的加載器去加載,而怎麼獲取這個加載器是個問題。
程序都是在線程中執行,那麼從線程的上下文中去拿最合理,所以就誕生了線程上下文類加載器,這中場景下加載器就得采用非自動加載
,即通過 forName
或者 loadClass
的方式去加載類。
下邊通過示例+科技來驗證加載不到的情況
示例中有Parent
類,Son
類。Parent
類中有個getSon
方法中,會使用到Son
類。 編譯後,在classpath
下,把Son
類移到自定義子加載器的資源目錄中,讓AppClassLoader
無法加載。達到我們的運行環境要求:
-
Parent
由AppClassLoader
加載 -
Son
由自定義的子加載器加載. - 調用
Parent
的getSon
方法的時候要new Son()
,這樣會觸發Son
類的加載,但是加載不到Son
類,報錯信息爲:java.lang.NoClassDefFoundError: com/rock/Son
public class Parent {
private Object son;
public Object getSon(){
return new Son();
}
public Object setSon(Object son){
this.son = son;
return this.son;
}
}
public class Son {
}
/**
* 父加載器加載不了,子加載器所加載的類,
* 父加載器加載Parent
* 子加載器加載son
* Parent#getSon 方法裏 new Son()對象.//報錯,找不到Son的類定義.
*/
@Test
public void testParentCanntFindSon(){
CustomClassLoader01 customClassLoader01 = new CustomClassLoader01(ClassLoader.getSystemClassLoader());
try {
Class<?> classParent = customClassLoader01.loadClass("com.rock.Parent");
System.out.println("classParent:" + classParent.getClassLoader());
Class<?> classSon = customClassLoader01.loadClass("com.rock.Son");
System.out.println("classSon:" + classSon.getClassLoader());
Object objParent = classParent.newInstance();
Object objSon = classSon.newInstance();
Method setSon = classParent.getMethod("setSon",Object.class);
Object resultSon = setSon.invoke(objParent, objSon);
System.out.println(resultSon.getClass());
System.out.println(resultSon.getClass().getClassLoader());
try {
Method getSon = classParent.getMethod("getSon");
Object invoke = getSon.invoke(objParent);
System.out.println(invoke.getClass().getClassLoader());
}catch (Exception exp){
//java.lang.NoClassDefFoundError: com/rock/Son
exp.printStackTrace();
}
}catch (Exception exp){
exp.printStackTrace();
}
}
3.4 不同命名空間的類互相不可見
這個特徵也通過實例來驗證一下,自定義類加載器 new
出來兩個實例,每個實例各自加載一次Man
類,通過newInstance
方式實例化出兩個對象,對象之間互相調用setFather
賦值就會報錯。即a空間的不識別b空間的類,即使全限定名相同也不識別。這種特性所導致的坑你踩到過嘛?
public class Man {
private Man father;
public void setFather(Object obj){
father = (Man)obj;
}
}
@Test
public void testSifferentNamespaceClass(){
CustomClassLoader01 customClassLoader01 = new CustomClassLoader01(ClassLoader.getSystemClassLoader());
CustomClassLoader01 customClassLoader02 = new CustomClassLoader01(ClassLoader.getSystemClassLoader());
try {
Class<?> aClass1 = customClassLoader01.loadClass("com.rock.Man");
System.out.println("class1 : " + aClass1.getClassLoader());
System.out.println("class1 : " + aClass1.);
Class<?> aClass2 = customClassLoader02.loadClass("com.rock.Man");
System.out.println("class2 : " + aClass1.getClassLoader());
System.out.println("class2 : " + aClass2);
Object man1 = aClass1.newInstance();
Object man2 = aClass2.newInstance();
Method setFather = aClass1.getMethod("setFather", Object.class);
setFather.invoke(man1,man2);
}catch (Exception exp){
exp.printStackTrace();
}
}
class1 : com.rock.classLoader.CustomClassLoader01@1f28c152
class1 : 2006034581
class2 : com.rock.classLoader.CustomClassLoader01@1f28c152
class2 : 488044861
...
Caused by: java.lang.ClassCastException: com.rock.Man cannot be cast to com.rock.Man
at com.rock.Man.setFather(Man.java:6)
... 27 more