自定義一個類載入器

追尋北極發表於2017-11-23

為什麼要自定義類載入器

類載入機制:http://www.cnblogs.com/xrq730/p/4844915.html

類載入器:http://www.cnblogs.com/xrq730/p/4845144.html

這兩篇文章已經詳細講解了類載入機制和類載入器,還剩最後一個問題沒有講解,就是自定義類載入器。為什麼我們要自定義類載入器?因為雖然Java中給使用者提供了很多類載入器,但是和實際使用比起來,功能還是匱乏。舉一個例子來說吧,主流的Java Web伺服器,比如Tomcat,都實現了自定義的類載入器(一般都不止一個)。因為一個功能健全的Web伺服器,要解決如下幾個問題:

1、部署在同一個伺服器上的兩個Web應用程式所使用的Java類庫可以實現相互隔離。這是最基本的要求,兩個不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求一個類庫在一個伺服器中只有一份,伺服器應當保證兩個應用程式的類庫可以互相使用

2、部署在同一個伺服器上的兩個Web應用程式所使用的Java類庫可以相互共享。這個需求也很常見,比如相同的Spring類庫10個應用程式在用不可能分別存放在各個應用程式的隔離目錄中

3、支援熱替換,我們知道JSP檔案最終要編譯成.class檔案才能由虛擬機器執行,但JSP檔案由於其純文字儲存特性,執行時修改的概率遠遠大於第三方類庫或自身.class檔案,而且JSP這種網頁應用也把修改後無須重啟作為一個很大的優勢看待

由於存在上述問題,因此Java提供給使用者使用的ClassLoader就無法滿足需求了。Tomcat伺服器就有自己的ClassLoader架構,當然,還是以雙親委派模型為基礎的:

 

JDK中的ClassLoader

在實現自己的ClassLoader之前,我們先看一下JDK中的ClassLoader是怎麼實現的:

 1 protected synchronized Class<?> loadClass(String name, boolean resolve)
 2     throws ClassNotFoundException
 3     {
 4     // First, check if the class has already been loaded
 5     Class c = findLoadedClass(name);
 6     if (c == null) {
 7         try {
 8         if (parent != null) {
 9             c = parent.loadClass(name, false);
10         } else {
11             c = findBootstrapClass0(name);
12         }
13         } catch (ClassNotFoundException e) {
14             // If still not found, then invoke findClass in order
15             // to find the class.
16             c = findClass(name);
17         }
18     }
19     if (resolve) {
20         resolveClass(c);
21     }
22     return c;
23     }

方法原理很簡單,一步一步解釋一下:

1、第5行,首先查詢.class是否被載入過

2、第6行~第12行,如果.class檔案沒有被載入過,那麼會去找載入器的父載入器。如果父載入器不是null(不是Bootstrap ClassLoader),那麼就執行父載入器的loadClass方法,把類載入請求一直向上拋,直到父載入器為null(是Bootstrap ClassLoader)為止

3、第13行~第17行,父載入器開始嘗試載入.class檔案,載入成功就返回一個java.lang.Class,載入不成功就丟擲一個ClassNotFoundException,給子載入器去載入

4、第19行~第21行,如果要解析這個.class檔案的話,就解析一下,解析的作用類載入的文章裡面也寫了,主要就是將符號引用替換為直接引用的過程

我們看一下findClass這個方法:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
    }

是的,沒有具體實現,只拋了一個異常,而且是protected的,這充分證明了:這個方法就是給開發者重寫用的

 

自定義類載入器

從上面對於java.lang.ClassLoader的loadClass(String name, boolean resolve)方法的解析來看,可以得出以下2個結論:

1、如果不想打破雙親委派模型,那麼只需要重寫findClass方法即可

2、如果想打破雙親委派模型,那麼就重寫整個loadClass方法

當然,我們自定義的ClassLoader不想打破雙親委派模型,所以自定義的ClassLoader繼承自java.lang.ClassLoader並且只重寫findClass方法。

第一步,自定義一個實體類Person.java,我把它編譯後的Person.class放在D盤根目錄下:

 1 package com.xrq.classloader;
 2 
 3 public class Person
 4 {
 5     private String name;
 6     
 7     public Person()
 8     {
 9         
10     }
11     
12     public Person(String name)
13     {
14         this.name = name;
15     }
16     
17     public String getName()
18     {
19         return name;
20     }
21     
22     public void setName(String name)
23     {
24         this.name = name;
25     }
26     
27     public String toString()
28     {
29         return "I am a person, my name is " + name;
30     }
31 }

第二步,自定義一個類載入器,裡面主要是一些IO和NIO的內容,另外注意一下defineClass方法可以把二進位制流位元組組成的檔案轉換為一個java.lang.Class----只要二進位制位元組流的內容符合Class檔案規範。我們自定義的MyClassLoader繼承自java.lang.ClassLoader,就像上面說的,只實現findClass方法:

public class MyClassLoader extends ClassLoader
{
    public MyClassLoader()
    {
        
    }
    
    public MyClassLoader(ClassLoader parent)
    {
        super(parent);
    }
    
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        } 
        catch (Exception e)
        {
            e.printStackTrace();
        }
        
        return super.findClass(name);
    }
    
    private File getClassFile(String name)
    {
        File file = new File("D:/Person.class");
        return file;
    }
    
    private byte[] getClassBytes(File file) throws Exception
    {
        // 這裡要讀入.class的位元組,因此要使用位元組流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        
        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        
        fis.close();
        
        return baos.toByteArray();
    }
}

第三步,Class.forName有一個三個引數的過載方法,可以指定類載入器,平時我們使用的Class.forName("XX.XX.XXX")都是使用的系統類載入器Application ClassLoader。寫一個測試類:

 1 public class TestMyClassLoader
 2 {
 3     public static void main(String[] args) throws Exception
 4     {
 5         MyClassLoader mcl = new MyClassLoader();        
 6         Class<?> c1 = Class.forName("com.xrq.classloader.Person", true, mcl); 
 7         Object obj = c1.newInstance();
 8         System.out.println(obj);
 9         System.out.println(obj.getClass().getClassLoader());
10     }
11 }

看一下執行結果:

I am a person, my name is null
com.xrq.classloader.MyClassLoader@5d888759

個人的經驗來看,最容易出問題的點是第二行的列印出來的是"sun.misc.Launcher$AppClassLoader"。造成這個問題的關鍵在於MyEclipse是自動編譯的,Person.java這個類在ctrl+S儲存之後或者在Person.java檔案不編輯若干秒後,MyEclipse會幫我們使用者自動編譯Person.java,並生成到CLASSPATH也就是bin目錄下。在CLASSPATH下有Person.class,那麼自然是由Application ClassLoader來載入這個.class檔案了。解決這個問題有兩個辦法:

1、刪除CLASSPATH下的Person.class,CLASSPATH下沒有Person.class,Application ClassLoader就把這個.class檔案交給下一級使用者自定義ClassLoader去載入了

2、TestMyClassLoader類的第5行這麼寫"MyClassLoader mcl = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent());", 即把自定義ClassLoader的父載入器設定為Extension ClassLoader,這樣父載入器載入不到Person.class,就交由子載入器MyClassLoader來載入了

 

ClassLoader.getResourceAsStream(String name)方法作用

ClassLoader中的getResourceAsStream(String name)其實是一個挺常見的方法,所以要寫一下。這個方法是用來讀入指定的資源的輸入流,並將該輸入流返回給使用者用的,資源可以是影象、聲音、.properties檔案等,資源名稱是以"/"分隔的標識資源名稱的路徑名稱。

不僅ClassLoader中有getResourceAsStream(String name)方法,Class下也有getResourceAsStream(String name)方法,它們兩個方法的區別在於:

1、Class的getResourceAsStream(String name)方法,引數不以"/"開頭則預設從此類對應的.class檔案所在的packge下取資源,以"/"開頭則從CLASSPATH下獲取

2、ClassLoader的getResourceAsStream(String name)方法,預設就是從CLASSPATH下獲取資源,引數不可以以"/"開頭

其實,Class的getResourceAsStream(String name)方法,只是將傳入的name進行解析一下而已,最終呼叫的還是ClassLoader的getResourceAsStream(String name),看一下Class的getResourceAsStrea(String name)的原始碼:

 1 public InputStream getResourceAsStream(String name) {
 2     name = resolveName(name);
 3     ClassLoader cl = getClassLoader0();
 4     if (cl==null) {
 5         // A system class.
 6         return ClassLoader.getSystemResourceAsStream(name);
 7     }
 8     return cl.getResourceAsStream(name);
 9 }
10 
11 private String resolveName(String name) {
12     if (name == null) {
13         return name;
14     }
15     if (!name.startsWith("/")) {
16         Class c = this;
17         while (c.isArray()) {
18             c = c.getComponentType();
19         }
20         String baseName = c.getName();
21         int index = baseName.lastIndexOf('.');
22         if (index != -1) {
23             name = baseName.substring(0, index).replace('.', '/')
24                 +"/"+name;
25         }
26     } else {
27         name = name.substring(1);
28     }
29     return name;
30 }

程式碼不難,應該很好理解,就不解釋了。

 

.class和getClass()的區別

最後講解一個內容,.class方法和getClass()的區別,這兩個比較像,我自己沒對這兩個東西總結前,也常弄混。它們二者都可以獲取一個唯一的java.lang.Class物件,但是區別在於:

1、.class用於類名,getClass()是一個final native的方法,因此用於類例項

2、.class在編譯期間就確定了一個類的java.lang.Class物件,但是getClass()方法在執行期間確定一個類例項的java.lang.Class物件

相關文章