類加載機制與ClassLoader

類加載機制與ClassLoader

類加載器ClassLoader即用於加載其它類的類,將字節碼加載進內存,創建Class對象,輸入完全限定的類名,輸出Class對象。

ClassLoader分三類:

  • 啓動類加載器Bootstrap ClassLoader:Java虛擬機的一部分,使用C++實現,負責加載java的基礎類,主要是<Java_Home>/lib/rt.jar
  • 擴展類加載器Extension ClassLoader(java9已刪除,新增Platform Class Loader):負責加載java的擴展類,主要是<Java_Home>/lib/ext裏的jar包
  • 應用程序類加載器Application ClassLoader:負責加載應用程序類。

雙親委派模型

Bootstrap ClassLoader→Extension ClassLoader→Application ClassLoader,後面的類有一個指針parent指向前面的類,ClassLoader在加載一個類時,基本步驟爲:

  1. 判斷該類是否被加載過,有則直接返回Class對象,沒有則被ClassLoader加載一次
  2. 加載時,先讓指針parent指向的“父”ClassLoader進行加載,加載成功返回Class對象
  3. 加載不成功,自己嘗試加載

該步驟所描述的模型即雙親委派模型,優先讓父ClassLoader去加載。

這樣做的原因是可以避免java類庫被覆蓋的問題,優先讓父ClassLoader加載,避免自定義的類覆蓋java類庫,確保java安全機制,即使自定義ClassLoader可以不遵從雙親委派模型,但java安全機制保證以java開頭的類不被其它類加載器加載。

ClassLoader

ClassLoader是一個抽象類,提供將字節碼(class文件)加載至內存成爲Class的基本方法,每一個Class都有一個基本方法 :

  • ClassLoader getClassLoader() :獲取該Class的實際類加載器

默認情況下,上述三類類加載器都有默認實現,Bootstrap ClassLoader用C++實現,Extension ClassLoader與

Application ClassLoader的實現位於sun.misc.Launcher中。

ClassLoader的基本方法爲:

  • ClassLoader getParent():獲取父ClassLoader,如果是Bootstrap ClassLoader則返回null
  • static ClassLoader getSystemClassLoader():獲取系統默認ClassLoader
  • Class<?> loadClass(String name):給定類的完全限定名,解析字節碼,將類加載進內存並返回Class

Class.forName()

Class的靜態方法forName()同樣用於加載類進入內存並返回Class對象,區別在於Class.forName加載時可執行static靜態代碼塊,而ClassLoader的loadClass不會執行,Class.forName方法可指定參數boolean initialize決定是否執行,ClassLoader的loadClass內部調用方法loadClass(name,false),這個方法內部調用findClass(name),是真正的類加載方法,第二個參數名爲resolve,爲true時才鏈接去執行static語句塊。由於雙親委派模型,即使設置爲true,父類傳遞的仍然是false。

ClassLoader應用

讀取文件加載類

廣泛應用於各種框架中,使用ClassLoader,可通過讀取配置文件直接加載Class進入內存,這樣在面向接口編程中不用使用new 聲明具體的實現,通過加載配置文件加載指定的類並返回實例,不動代碼動配置,可以幫助完成依賴注入這個抽象概念的一部分實現。

  1. 自定義一個類(或許是一個bean)
public class HelloWorld {
    public static void say(){
        System.out.println("HelloWorld");
    }
}
  1. 定義配置問件test.properties
#注意路徑要正確,當前包下,也可以是其它包
word=com.lsl.kennen.common.HelloWorld 
  1. 使用ClassLoader
    public static void main(String[] args) {
        try {
            //讀取配置文件,注意文件名路徑要正確
            Properties properties=new Properties();
            String fileName=new File("").getAbsolutePath()+ "/kennen/src/main/java/com/lsl/kennen/common/test.properties";
            properties.load(new FileInputStream(fileName));
            //從配置文件中獲取源數據
            String className =properties.getProperty("word");
            //根據信息加載類進入內存返回Class
            Class<?> cls=Class.forName(className);
            //獲取實例
            HelloWorld helloWorld=(HelloWorld) cls.newInstance();
            helloWorld.say();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
//結果爲HelloWorld

使用ClassLoader加載類,讀取配置文件動態將Class加載進內存

自定義ClassLoader

自定義ClassLoader,利用defineClass方法直接返回一個Class<?>,可以遠程調用Class的bytes讀取Class文件將其加載進入本地內存並執行,這種功能就不是本地類加載器能辦到的(沒辦法new,沒辦法forName,loadClass,因爲讀取的都是本地類的路徑名加載本地bytes)。同時,面向接口編程時,可以動態加載calss,兩個class,使用不同的ClassLoader加載,JVM就認爲他們加載的對象是不同的,可以實現隔離與熱部署。

自定義ClassLoader:繼承ClassLoader,重寫findClass,返回一個Class<?>

需要注意的是:

方法defineClass()用於實際讀取bytes()返回Class<?>,然而指定的類名必須符合規範,如com.lsl.xxx這樣的,內部類則是com.lsl.xxx$Iservice這樣的

因爲是通過反射創建實例,所以對象的構造器不能設置爲private,否則拋出異常。

首先,先定義一個Class:

public class HelloWorld{
    public void say(){
        System.out.println("HelloWorld");
    }
}

然後重寫類加載器:

public class Main {

    public static void main(String[] args) {
        //注意路徑
        String className=new File("").getAbsolutePath()+ "/kennen/target/classes/com/lsl/kennen/common/HelloWorld.class";
        ClassLoader classLoader=new MyClassLoader();
        try {
            Class<?> c=classLoader.loadClass(className);
            ((HelloWorld) c.newInstance()).say();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    //重寫ClassLoader
    static class MyClassLoader extends ClassLoader{
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException{
            //本地測試,指定從本地讀取class
            FileInputStream fileInputStream;//指定源
            ByteArrayOutputStream outputStream=new ByteArrayOutputStream();//指定目的
            try{
                fileInputStream=new FileInputStream(name);
                byte[] buf=new byte[1024];
                int ite=0;
                while((ite=fileInputStream.read(buf))!=-1){//從文件源中讀取數據寫入到buf
                    outputStream.write(buf,0,ite);//從buf中讀取數據寫入到outputStream
                }
            }catch (Exception e){
                e.printStackTrace();
            }
            return defineClass("com.lsl.kennen.common.HelloWorld",outputStream.toByteArray(),0,outputStream.toByteArray().length);//加載class文件的byte數組(bytes),返回Class<?>
        }
    }
}

然而直接這樣寫會拋出異常,重寫的ClassLoader將指定的class文件加載爲Class,加載至內存,返回Class<?>,使用反射強制類型轉換時如果直接強轉爲一個類會拋出異常,因爲本地類沒有沒加載,就算加載了,使用的不是一個類加載器,加載出來的對象JVM是不認爲相同的,這個時候就只能使用接口,可以強轉爲接口,只要加載的類實現了接口那麼就可以強轉爲接口,調用接口方法。

定義接口:

public interface World {
    public void say();
}

定義實現:

public class HelloWorld implements World{
    @Override
    public void say(){
        System.out.println("HelloWorld");
    }
}

將 ((HelloWorld) c.newInstance()).say();改爲 ((World) c.newInstance()).say();

運轉正常,World恢復。

熱部署

熱部署即在不停止程序運行的情況下動態更改Class,只要修改了Class就能立刻看出變化。

使用自定義ClassLoader,因爲不同的類加載器可以加載出不同的Class,因此利用這一特性即可以實現模塊隔離,也可以實現熱部署。

定義服務實現:

public class HelloWorld implements World{
    private static volatile World helloWorld;

    public static World getHelloWorld(){
        if(helloWorld==null){
            synchronized (HelloWorld.class){
                helloWorld=createHelloWorld();
            }
        }
        return helloWorld;
    }

    public static World createHelloWorld(){
        try{
            MyClassLoader myClassLoader=new MyClassLoader();
            String className=new File("").getAbsolutePath()+ "/kennen/target/classes/com/lsl/kennen/common/HelloWorld.class";
            Class<?> c=myClassLoader.loadClass(className);
            return ((World)c.newInstance());
        }catch (Exception e){
            e.printStackTrace();
        }
       return null;
    }

    public static void update(){
        helloWorld=createHelloWorld();
    }

    @Override
    public void say(){
        System.out.println("HelloWorld");
    }
}

服務內部實現內部維護一個接口World,提供服務功能,實際上是通過自定義的ClassLoader加載的Class返回的實例

類加載器爲:

public class MyClassLoader extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException{
        //本地測試,指定從本地讀取class
        FileInputStream fileInputStream;//指定源
        ByteArrayOutputStream outputStream=new ByteArrayOutputStream();//指定目的
        try{
            fileInputStream=new FileInputStream(name);
            byte[] buf=new byte[1024];
            int ite=0;
            while((ite=fileInputStream.read(buf))!=-1){//從文件源中讀取數據寫入到buf
                outputStream.write(buf,0,ite);//從buf中讀取數據寫入到outputStream
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return defineClass("com.lsl.kennen.common.HelloWorld",outputStream.toByteArray(),0,outputStream.toByteArray().length);//加載class文件的byte數組(bytes),返回Class<?>
    }

}

模擬熱部署,定義兩個線程,一個不斷訪問服務,一個不斷檢測字節碼問件是否改變以確定是否更新服務

public class Main {

    public static void main(String[] args) {

        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    World world=HelloWorld.getHelloWorld();
                    world.say();
                    try {
                        Thread.sleep(1000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread thread1=new Thread(new Runnable() {
            private long lastModilied=new File(new File("").getAbsolutePath()+ "/kennen/target/classes/com/lsl/kennen/common/HelloWorld.class").lastModified();
            @Override
            public void run() {
                while (true){
                    try{
                        Thread.sleep(100);
                        //根據時間差判斷文件是否被修改
                        long now=new File(new File("").getAbsolutePath()+ "/kennen/target/classes/com/lsl/kennen/common/HelloWorld.class").lastModified();
                        if(now!=lastModilied){
                            //由時間差更新內部服務,完成熱部署,動態變更執行的Class
                            lastModilied=now;
                            HelloWorld.update();
                        }
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        });
        thread1.setDaemon(true);

        thread.start();
        thread1.start();
    }

}

當然直接運行時,沒辦法直接改Class文件,預先編譯好Class,在運行時進行替換即可看到變化

URLClassLoader

對於遠程加載Class,可以直接通過URLClassLoader加載,位於java.net包下,同樣的,因爲基於ClassLoader,所以一定要面向接口編程,具體的類強轉是行不通的。參見: [locez.com/JAVA/urlcla…](

參考 :

<<java 編程的邏輯>> 馬俊昌著

www.ibm.com/developerwo…

locez.com/JAVA/urlcla…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章