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 |> com.crazymakercircle.classLoader.FileClassLoader@2a18f23c showLoaderTree |> sun.misc.Launcher$AppClassLoader@18b4aac2 showLoaderTree |> sun.misc.Launcher$ExtClassLoader@6fdb1f78
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羣
無編程不創客,無案例不學習。 一定記得去跑一跑案例哦
類加載器 系列 全目錄