天天看點

ClassLoader 隔離性的基石是namespace,證明給你看

作者:老誠不bug

本篇是對ClassLoader中的namespace做個直覺的介紹和驗證。這個知識點筆者個人認為很重要,多次幫助筆者解決日常工作中遇到的疑難雜症,如果你尚未認真研究過ClassLoader,但懵懂的認知讓你覺得這個應該很簡單,那請看下圖,若看不懂則表明你可能并不了解ClassLoader中得一些關鍵邏輯;不販賣焦慮,不感興趣則忽略,知道個大概即可,不會它并不影響你做一個優秀的程式員。

ClassLoader 隔離性的基石是namespace,證明給你看

二、Classloader的分治和委派機制

上下文同步,已了解這部分的讀者朋友可直接跳過,進過第三小節。

ClassLoader 是個抽象類,其子類 UrlClassLoader 引入 URL 資源概念,以此方式來限制自己隻加載 URL 覆寫範圍内的類檔案;ExtClassLoader、AppClassLoader 都是 UrlClassLoader 的子類,各自分管的資源路徑不同,即給定的 URL 不同則管轄範圍不同,并通過委派執行類加載來保障這種分治能力,進而達到了類資源的隔離性。

ClassLoader 隔離性的基石是namespace,證明給你看

上圖是标準的委派機制,總結為2個方面:

  1. 父加載器能加載 父加載器來加載:
  2. 自己在加載資源之前,先讓父類加載器去加載。父類再找其父類,直到BootStrapClassLoader(它沒有父類加載器)。
  3. 保證了等級越高,加載的優先權越高
  4. 父加載器不加載 我就來加載(findClass);我加載不了的子加載器來加載:
  5. 若父類加載器沒有加載成功,才逐級下放這個加載權。
  6. 子類加載器不能加載父類加載器能加載的類,如 java.lang.String ,即使使用者自己編造一份這個類型,啟動類加載器優先将 java.lang.String 加載成功後,應用類加載器就不會再加載使用者自己編造的。

下圖描述了 SkyWalking Agent 通過自定義的類加載器AgentClassLoader 加載插件中的類,不會對宿主應用中的類産生污染。

ClassLoader 隔離性的基石是namespace,證明給你看

三、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無法加載。達到我們的運作環境要求:

  1. Parent由AppClassLoader加載
  2. Son由自定義的子加載器加載.
  3. 調用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
           
來源:https://juejin.cn/post/7177671182550827063