Java類載入器( CLassLoader ) 死磕5: 自定義一個檔案系統的classLoader
【正文】Java類載入器( CLassLoader ) 死磕5:
自定義一個檔案系統classLoader
本小節目錄
5.1. 自定義類載入器的基本流程
5.2. 入門案例:自定義檔案系統類載入器
5.3. 案例的環境配置
5.4 FileClassLoader 案例實現步驟
5.5. FileClassLoader 的類設計
5.6. FileClassLoader 的原始碼
5.7. FileClassLoader 的使用
5.8. 不同類載入器的名稱空間關係
5.9. 自定義載入器的兩個要點
1.1. 自定義classLoader
不管是Bootstrap ClassLoader還是ExtClassLoader等,這些類載入器都只是載入指定的目錄下的jar包或者資源。
要實現其他的途徑的類載入,比如從D盤某個資料夾載入一個class檔案,或者從網路上下載class主內容然後再進行載入,就需要我們自定義一個classloader。
1.1.1. 自定義類載入器的基本流程
實際上,除了和本地實現密切相關的Bootstrap啟動類載入器之外,包括Extention標準擴充套件類載入器和AppClassLoader應用類載入器在內的所有其他類載入器,都可以當做自定義類載入器來對待。
前面的內容中已經對java.lang.ClassLoader抽象類中的loadClass方法做了介紹,在此方法中,如果所有的上層類載入器都沒有載入成功,則呼叫本類載入器的findClass()方法,在自己的地盤,獲取對應的位元組碼,並完成位元組碼到類的轉變,並且將class載入到快取中。這個findClass()方法,就是自定義載入器的關鍵。
實現一個自定義載入器,總體來說,分成三步:
(1)在自己的地盤,獲取對應的位元組碼;
(2)並完成位元組碼到類的轉變;
(3)將class載入到快取中;
前面兩步,需要在findClass()方法中完成。
廢話少說,先看一個簡單例項。
1.1.2. 入門案例:自定義檔案系統類載入器
在寵物店的案例中,如果需要裝載第三方的寵物庫,並且第三方寵物庫的類路徑非常靈活。
現在需要設計自定義載入器,按照需要,從第三方庫的載入寵物到記憶體。這裡設計一個自定義載入類FileClassLoader。
在設計FileClassLoader之前,先交代一下第三方的寵物類LittleDog ,為了演示,和之前的Dog類程式碼99%相同的。
LittleDog 的程式碼如下:
package com.crazymakercircle.annoDemo; ....... public class LittleDog implements IPet{ //寵物編號 private static int dogNo; protected String name; protected int age; //無參構造器 public LittleDog() { dogNo++; name="LittleDog-"+ dogNo; age = RandomUtil.randInMod(20); } @Tanscation public LittleDog sayHello() { Logger.info("嗨,大家好!我是" + name); return this; } @Tanscation public LittleDog sayAge() { Logger.info("我是" + name + ",我的年齡是:" + age); return this; } }
至此,一個簡單的第三方類——LittleDog寵物類,已經介紹完畢。
下面看看本案例所涉及到的路徑,和其他的環境配置。
1.1.3. 案例的環境配置
這個類的名字,在System.properties 配置檔案的配置項為:
pet.dog.class=com.crazymakercircle.otherPet.pet.LittleDog
編譯完成之後,存在在一個獨立的路徑中。
這個第三方類庫的路徑,為了可以靈活多變,並且與原始碼工程的輸出路徑相不能相同,也在System.properties 配置檔案增加配置項,具體為:
class.server.path=D:/瘋狂創客圈 死磕java/code/out2/
為這方便讀取,給這個兩個配置項,增加其在對應在SystemConfig 配置類中的常量,具體如下:
package com.crazymakercircle.config; @ConfigFileAnno(file = "/system.properties") public class SystemConfig extends ConfigProperties { ............ //第三方的類路徑 @ConfigFieldAnno(proterty = "class.server.path") public static String CLASS_SERVER_PATH; //寵物狗的型別 @ConfigFieldAnno(proterty = "pet.dog.class") public static String PET_DOG_CLASS; ............ }
編譯完成LittleDog類後,將.class檔案,複製到配置項%class.server.path% 所在的目錄下。
至此,環境配置已經交代完畢。
下面馬上進入正題。
1.1.4. FileClassLoader 案例實現步驟
自定義類載入器,首先要繼承ClassLoader抽象類,並且重寫其findClass()方法。
在重寫的findClass()方法中,完成以下三步:
(1)在自己的地盤(查詢路徑),獲取對應的位元組碼;
(2)並完成位元組碼到Class類物件的轉變;
(3)返回Class類物件。
接下來,findClass()方法返回Class類物件之後,ClassLoader抽象類的程式碼,會將新返回Class類物件載入到快取中,這個工作由抽象類ClassLoader載入器去完成。
1.1.5. FileClassLoader 的類設計
這裡的自定義載入的名稱為——FileClassLoader。
類圖如下:
FileClassLoader的成員屬性rootDir,用來存著自己的查詢路徑。當載入一個類時,如果所有的雙親載入器都沒有載入到,就去rootDir下查詢。
FileClassLoader重寫了的findClass()方法。在這個重寫方法中,首先會呼叫getClassData,在自己的地盤(查詢路徑),獲取對應的位元組碼,返回位元組碼的二進位制陣列。
FileClassLoader增加了的getClassData()方法,這是其自己的私有方法。主要是找到類的二進位制class檔案,然後通過讀取檔案流的方式,讀取檔案的位元組碼,供findClass()重寫方法使用。
1.1.6. FileClassLoader 的原始碼
簡單粗暴,直接上原始碼。
package com.crazymakercircle.classLoader;
import java.io.*;
public class FileClassLoader extends ClassLoader {
private String rootDir;
public FileClassLoader(String rootDir) {
this.rootDir = rootDir;
}
public FileClassLoader(ClassLoader parent,String rootDir) {
super(parent);
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name)
throws ClassNotFoundException
{
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
protected byte[] getClassData(String className)
{
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
protected String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
案例路徑:com.crazymakercircle.classLoader.ClassLoader
案例提示:無程式設計不創客、無案例不學習。一定記得看案例哦
上面的findClass()方法中,了呼叫defineClass()方法。這個方法是基類ClassLoader的方法,其作用是,將位元組碼匯入到JVM的方法區記憶體,完成Class類物件載入、驗證、準備、解析四步工作。 這個方法在編寫自定義class loader的時候非常重要,它能將class二進位制內容轉換成Class物件,如果不符合要求的會丟擲各種異常。
總結一下,自定義載入器的步驟為:
(1)編寫一個類繼承自ClassLoader抽象類。
(2)重寫它的findClass()方法。
(3)在自己的地盤,獲取對應的位元組碼;
(4)呼叫defineClass()方法,將位元組碼載入成Class物件,並且返回。
1.1.7. FileClassLoader 的使用
簡單粗暴,先上程式碼:
public class FileLoaderDemo { public static void useFileLoader() { try { String baseDir = SystemConfig.CLASS_SERVER_PATH; FileClassLoader fileClassLoader = new FileClassLoader(baseDir); String className =SystemConfig.PET_DOG_CLASS; Class dogClass = fileClassLoader.loadClass(className); Logger.info("顯示dogClass的ClassLoader =>"); ClassLoaderUtil.showLoader4Class(dogClass); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static void main(String[] args) { useFileLoader(); } }
上面的例子中,所載入的類為:
pet.dog.class=com.crazymakercircle.otherPet.pet.LittleDog
這個類是提前編譯完成之後,放置在在一個獨立的路徑中,這個路徑為:
class.server.path=D:/瘋狂創客圈 死磕java/code/out2/
需要注意的是,這個路徑不包括在當前工程的類路徑%java.class.path%中。否則,自定義的載入器,是鐵定載入不到的。
為什麼呢?
依據雙親委託機制,包括在當前工程的類路徑%java.class.path%中的類,會優先被AppClassLoader載入。因為自定義載入器的parent,預設就是AppClassLoader。
案例路徑:com.crazymakercircle.classLoaderDemo.base.FileLoaderDemo
案例提示:無程式設計不創客、無案例不學習。一定要跑案例哦
執行的結果是:
showLoaderTree |> [email protected] showLoaderTree |> [email protected] showLoaderTree |> [email protected]
1.1.8. 不同類載入器的名稱空間關係
同一個名稱空間內的類是相互可見的。子載入器的名稱空間包含所有父載入器的名稱空間。因此子載入器載入的類能看見父載入器載入的類。例如系統類載入器載入的類能看見根類載入器載入的類。
由父載入器載入的類不能看見子載入器載入的類。
如果兩個載入器之間沒有直接或間接的父子關係,那麼它們各自載入的類相互不可見。當兩個不同名稱空間內的類相互不可見時,可以採用Java的反射機制來訪問例項的屬性和方法。
這裡,需要說明一下 Java 虛擬機器是如何判定兩個 Java 類是相同的。Java 虛擬機器不僅要看類的全名是否相同,還要看載入此類的類載入器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。
即便是同樣的位元組程式碼,被不同的類載入器載入之後所得到的類,也是不同的。
下面看一段程式碼:
package com.crazymakercircle.classLoaderDemo.base; public class LoaderedCompare { public static void showClassSame() { try { String baseDir = SystemConfig.CLASS_SERVER_PATH; FileClassLoader fileClassLoader = new FileClassLoader(baseDir); String className =SystemConfig.PET_DOG_CLASS; Class dogClass = fileClassLoader.loadClass(className); FileClassLoader classLoader2 = new FileClassLoader(baseDir); Class dogClass2 = classLoader2.loadClass(className); Logger.info("dogClass2.equals(dogClass) => "); Logger.info( dogClass2.equals(dogClass)); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static void main(String[] args) { showClassSame(); } }
案例路徑:com.crazymakercircle.classLoaderDemo.base.LoaderedCompare
案例提示:無程式設計不創客、無案例不學習。一定要跑案例哦
執行的結果是:
showClassSame |> dogClass2.equals(dogClass) => showClassSame |> false
上面的例子中,載入的是同樣的位元組碼檔案。甚至兩個載入都是同一個類,只是是兩個不同的類載入器物件。但是,載入完成之後,在記憶體中的Class物件,是不一樣的。
1.1.9. 自定義載入器的兩個要點
要點一:
如果一個自定義載入器建立時如果沒有指定parent,那麼它的parent預設就是AppClassLoader。
為什麼呢?
原因是:如果預設的parent父載入器是AppClassLoader,這樣就能夠保證它能訪問系統內建載入器載入成功的class檔案。
要點二:
一般儘量不要重寫ClassLoader抽象類的loadClass()方法,破壞其中的雙親委託的程式邏輯。
在JVM規範和JDK文件中(1.2或者以後版本中),都沒有建議使用者重寫loadClass()方法,相比而言,明確提示開發者在開發自定義的類載入器時重寫findClass()邏輯。
原始碼:
程式碼工程: classLoaderDemo.zip
下載地址:在瘋狂創客圈QQ群檔案共享。
瘋狂創客圈:如果說Java是一個武林,這裡的聚集一群武痴, 交流程式設計體驗心得
QQ群連結:瘋狂創客圈QQ群
無程式設計不創客,無案例不學習。 一定記得去跑一跑案例哦
類載入器系列全目錄