自定義一個類加載器


為什么要自定義類加載器

類加載機制: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對象

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM