我們知道java語言是一次編譯炫贤,多平臺運(yùn)行疆导。這得益于Java在設(shè)計(jì)的時(shí)候饮戳,把編譯和運(yùn)行是獨(dú)立的兩個(gè)流程。編譯負(fù)責(zé)把源代碼編譯成 JVM 可識別的字節(jié)碼另假,運(yùn)行時(shí)加載字節(jié)碼,并解釋成機(jī)器指令運(yùn)行怕犁。
因?yàn)槭窃创a編譯成字節(jié)碼,所以 JVM 平臺除了java語言外,還有g(shù)roovy奏甫,scala等戈轿。
因?yàn)槭羌虞d字節(jié)碼運(yùn)行,所以有apm阵子,自定義classloader思杯,動(dòng)態(tài)語言等技術(shù)。構(gòu)成了豐富的Java 世界挠进。
javac 編譯流程
- parse:讀壬.java源文件,做詞法分析(LEXER)和語法分析(PARSER)
- enter:生成符號表
- process:處理注解
- attr:檢查語義合法性领突、常量折疊
- flow:數(shù)據(jù)流分析
- desugar:去除語法糖
- generate:生成字節(jié)碼
編譯期主要的目的是把 java 源代碼編譯為 符合 jvm 規(guī)范的的字節(jié)碼暖璧。在運(yùn)行期,由 jvm 加載字節(jié)碼并執(zhí)行君旦,程序就運(yùn)行起來了澎办。
其實(shí)java語言和 jvm 是沒有綁定關(guān)系。只要符合jvm規(guī)范的字節(jié)碼都可以執(zhí)行金砍,但是字節(jié)碼不一定由Java語言編譯而來局蚀。正因如此,jvm 平臺涌現(xiàn)出了groovy恕稠,scala琅绅,kotlin等眾多語言。
如果你感興趣鹅巍,也可以把把你喜歡的語言搬到 jvm 上運(yùn)行千扶。
類的生命周期
- loading:加載。是第一個(gè)階段昆著,主要是加載字節(jié)碼县貌,靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)數(shù)據(jù)結(jié)構(gòu),生成class對象凑懂。這里沒有限制字節(jié)碼的來源煤痕,可以是文件、zip接谨,網(wǎng)絡(luò)摆碉、jsp,甚至是加密文件脓豪。這個(gè)階段可以使用自定義 classloader 實(shí)現(xiàn)自定義行為巷帝,這就給字節(jié)碼帶來了很多可能的玩法。
- verification:驗(yàn)證扫夜。確保字節(jié)碼符合 jvm 規(guī)范楞泼。
- preparation:準(zhǔn)備驰徊。是正式為類中定義的變量設(shè)置初始值。
- resolution:解析堕阔。將常量池內(nèi)的符號引用替換為直接引用的過程棍厂。
- initialization: 初始化。這里將程序的主導(dǎo)權(quán)交給了應(yīng)用程序超陆,會執(zhí)行·<clinit>()和構(gòu)造函數(shù)牺弹。
- using:使用。使用初始化后的類时呀,這里就到了應(yīng)用邏輯的范疇张漂。
- unloading:卸載。需要滿足該類所有實(shí)例已經(jīng)被GC谨娜,加載該類的ClassLoader已經(jīng)被GC航攒,該類的java.lang.Class對象已經(jīng)沒有被引用。在tomcat jsp 熱加載的場景會用到瞧预,每個(gè)jsp都是單獨(dú)的 classloader屎债,當(dāng)jsp由變動(dòng)時(shí),會卸載舊的classloader垢油,創(chuàng)建新的classloader加載jsp盆驹,這樣就實(shí)現(xiàn)了熱加載。
在 initialization 階段之前滩愁,只有 loading 段可以通過自定義 Classloader 添加自定義邏輯躯喇,其他階段都是由 JVM 完成的。這就是本文想要表達(dá)的重點(diǎn)硝枉,Classloader 究竟能做什么呢廉丽。
雙親委派
在了解 Classloader 究竟能做什么之前,必須要先了解一下雙親委派模型妻味。眾所周知正压,java 是單繼承的,classloader 也繼承了這種設(shè)計(jì)思想责球。
這里針對 JDK 8 版本介紹焦履,JDK9 之后引入了模塊功能,classloader 繼承關(guān)系有所變化雏逾。
站在 JVM 的角度嘉裤,只有兩種加載器,一種是Bootstrap classloader栖博,由C++或者java實(shí)現(xiàn)屑宠。另一種是其他 classloader。都是用java語言編寫仇让,繼承自 java.lang.ClassLoader 抽象類典奉。
- Application Classloader躺翻。負(fù)責(zé)加載用戶路徑下的類,如果沒有自定義類加載器秋柄,這個(gè)就是默認(rèn)的類加載器获枝。
- Extension Classloader。負(fù)責(zé)加載<JAVA_HOME>\lib\ext骇笔,或java.ext.dirs系統(tǒng)變量所
指定的路徑中所有的類庫。 - BootStrap Classloader嚣崭。負(fù)責(zé)加載<JAVA_HOME>\lib笨触,-Xbootclasspath參數(shù)指定的類。應(yīng)用獲取不到這個(gè) Classloader 雹舀,以null代替芦劣。
ClassLoader 應(yīng)用案例
上面簡單介紹的是背景知識,下面是重頭戲说榆。在了解了javac 編譯流程虚吟,類的生命周期,classloader 雙親委派之后签财,能用它來做什么呢串慰。
在了解“類的生命周期”之后,知道 ClassLoader 只有在 loading 階段課可以可以自定義唱蒸,其他階段都是由 JVM 實(shí)現(xiàn)的邦鲫。下面我看看幾個(gè)應(yīng)用場景,直觀的感受一下神汹。
Java SPI 中的應(yīng)用
Java SPI (Service Provider Interface) 是動(dòng)態(tài)加載服務(wù)的機(jī)制庆捺。可以按照規(guī)則實(shí)現(xiàn)自己的SPI屁魏,使用 ServiceLoader 加載服務(wù)滔以。
Java SPI 的組件:
- 服務(wù)接口: 一個(gè)接口或者抽象類定義服務(wù)功能。
- 服務(wù)提供方: 服務(wù)接口的實(shí)現(xiàn)氓拼,提供具體的服務(wù)你画。
- 配置文件:需要在 META-INF/services 目錄下放置一個(gè)服務(wù)接口名相同的文件,每一行是一個(gè)實(shí)現(xiàn)類的全類名披诗。
- ServiceLoader:Java SPI 的主類撬即,用來通過服務(wù)接口加載服務(wù)實(shí)現(xiàn),有很多工具方法呈队,可實(shí)現(xiàn)重新加載服務(wù)剥槐。
Java SPI Example
實(shí)現(xiàn)一個(gè) SPI 并且使用 ServiceLoader 加載服務(wù)。
- 定義服務(wù)接口
public interface MessageServiceProvider {
void sendMessage(String message);
}
- 定義服務(wù)接口
實(shí)現(xiàn) email 和 推送消息連個(gè)實(shí)現(xiàn)宪摧。
public class EmailServiceProvider implements MessageServiceProvider {
public void sendMessage(String message) {
System.out.println("Sending Email with Message = "+message);
}
}
public class PushNotificationServiceProvider implements MessageServiceProvider {
public void sendMessage(String message) {
System.out.println("Sending Push Notification with Message = "+message);
}
}
- 編寫服務(wù)配置
在 META-INF/services 創(chuàng)建 util.spi.MessageServiceProvider 文件粒竖,內(nèi)容是服務(wù)類全路徑
util.spi.EmailServiceProvider
util.spi.PushNotificationServiceProvider
- ServiceLoader 加載服務(wù)
最后颅崩,通過 ServiceLoader 加載服務(wù)并測試。
public class ServiceLoaderTest {
public static void main(String[] args) {
ServiceLoader<MessageServiceProvider> serviceLoader = ServiceLoader
.load(MessageServiceProvider.class);
for (MessageServiceProvider service : serviceLoader) {
service.sendMessage("Hello");
}
}
輸出如下:
Sending Email with Message = Hello
Sending Push Notification with Message = Hello
下面是項(xiàng)目文件結(jié)構(gòu):
Java SPI class loader 的思考
ServiceLoader 類在 rt.jar 包中蕊苗,應(yīng)該是由 Bootstrap Classloader 加載沿后,而 EmailServiceProvider 是我定義的類,應(yīng)該是由 Application Classloader 加載朽砰。先驗(yàn)證一下這個(gè)想法尖滚。
ServiceLoader<MessageServiceProvider> serviceLoader = ServiceLoader.load(MessageServiceProvider.class);
System.out.println(ServiceLoader.class.getClassLoader());
for (MessageServiceProvider service : serviceLoader) {
System.out.println(service.getClass().getClassLoader());
}
結(jié)果如下:
// ServiceLoader 由 Bootstrap Classloader 加載,獲取不到classLoader
null
// 由 Application Classloader 加載
jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
按照classloader的繼承關(guān)系瞧柔,Bootstrap Classloader 是不能加載應(yīng)用類的漆弄,那ServiceLoader是如何引用到 SPI 服務(wù)的呢?
看下load方法做了什么造锅。
- ①撼唾,③,是同一個(gè) ClassLoader 哥蔚,是main線程的 contextClassLoader倒谷,而main線程的 contextClassLoader 是jvm設(shè)置的。有了這個(gè)線程糙箍,可以推測 ServiceLoader 是通過 contextClassLoader 加載服務(wù)的渤愁。
- ②是要加載的服務(wù)。
- 從調(diào)用棻睹遥可以看到 ServiceLoader 的迭代器是通過懶加載的方式加載服務(wù)猴伶。
- ① 是 Application Classloader,從線程上下文中獲取的塌西。
- ② 使用線程 contextClassLoader 加載的服務(wù)實(shí)現(xiàn)他挎,繞開了雙親委派。
jdbc driver 也是SPI服務(wù)
mysql 驅(qū)動(dòng)包中也由驅(qū)動(dòng)服務(wù)接口的實(shí)現(xiàn)配置捡需。
DriverManager 在加載的時(shí)候會調(diào)用 loadInitialDrivers 方法加載驅(qū)動(dòng)服務(wù)
// DriverManager.loadInitialDrivers()
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
}
}
}
}
// com.mysql.cj.jdbc.Driver
// 把自己注冊到 DriverManager 中
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
因?yàn)榉?wù)是懶加載的办桨,所以會遍歷迭代器,在Mysql 驅(qū)動(dòng)類中站辉,會把自己注冊到 DriverManager 中呢撞,這樣就 DriverManager 中就管理了所有的驅(qū)動(dòng)程序。
自定義文件名
有些時(shí)候可能需要防止正常的訪問饰剥,可以通過自定義 ClassLoader 殊霞,在loading的時(shí)候進(jìn)行處理
比如 lombok,使用 ShadowClassLoader 加載SCL.lombok文件 汰蓉。
加密 class 文件
實(shí)現(xiàn)一個(gè)加密class文件绷蹲,并使用自定義 ClassLoader 加載的 demo。
- 加密 class 文件
使用 xor 的方式加密,因?yàn)閮纱?xor 等于原值祝钢,是一種比較簡單的方式比规,安全級別更高的話可以通過JNI或者公私鑰的方式。
/**
* 解密/解密 class文件
*/
public static byte[] decodeClassBytes(byte[] bytes) {
byte[] decodedBytes = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
decodedBytes[i] = (byte) (bytes[i] ^ 0xFF);
}
return decodedBytes;
}
- 編寫加密類
類的邏輯比較簡單拦英,構(gòu)造的時(shí)候打印一句話蜒什。編譯后的class會通過上一步的方法加密,重命名為.class_文件用來區(qū)分疤估。
public class MyClass {
public MyClass(){
System.out.println("My class");
}
}
加密后的文件是不能通過正常方式解析的灾常,可以用javap命令驗(yàn)證一下
D:\workspace\mygit\jdk-learn\jdk8\src\main\resources>javap -v lang.classloader.encrypt.Myclass
錯(cuò)誤: 讀取lang.classloader.encrypt.Myclass的常量池時(shí)出錯(cuò): unexpected tag at #1: 245
- 編寫自定義 ClassLoader
首先定義一個(gè)引導(dǎo)類,引導(dǎo)類由自定義 ClassLoader加載做裙。之后引導(dǎo)類創(chuàng)建類時(shí)會使用 自定義 ClassLoader 加載岗憋。這個(gè)流程和 Tomcat 自定義classLoader 是一樣的。
public class MyCustomClassLoader extends ClassLoader {
// 加密的 class
private Collection<String> encryptClass = new HashSet<>();
// 忽略的類锚贱,未加密的類
private Collection<String> skipClass = new HashSet<>();
public void init() {
skipClass.add("lang.classloader.encrypt.EncryptApp");
encryptClass.add("lang.classloader.encrypt.MyClass");
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 由父類加載的類
if (name.startsWith("java.")
&& !encryptClass.contains(name)
&& !skipClass.contains(name)) {
return super.loadClass(name);
}
// 未加密的類
else if (skipClass.contains(name)) {
try {
String classPath = name.replace('.', '/') + ".class";
//返回讀取指定資源的輸入流
URL resource = getClass().getClassLoader().getResource(classPath);
InputStream is = resource != null ? resource.openStream() : null;
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
//將一個(gè)byte數(shù)組轉(zhuǎn)換為Class類的實(shí)例
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
// 加密的類
return findClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 加載類文件內(nèi)容
byte[] bytes = getClassFileBytesInDir(name);
// 解密
byte[] decodedBytes = decodeClassBytes(bytes);
// 初始化類,由 jvm 實(shí)現(xiàn)
return defineClass(name, decodedBytes, 0, bytes.length);
}
// 讀取加密class文件
private static byte[] getClassFileBytesInDir(String className) throws ClassNotFoundException {
try {
return FileUtils.readFileToByteArray(
new File(className.replace(".", "http://") + ".class_"));
} catch (IOException e) {
throw new ClassNotFoundException(className, e);
}
}
}
- 測試程序
測試時(shí)关串,先創(chuàng)建自定義類加載器拧廊,然后用自定義類加載器去加載啟動(dòng)類,啟動(dòng)類會使用自定義類加載器去加載MyClass晋修。
通過反射調(diào)用 EncryptApp 方法的說明很重要吧碾,可以嘗試直接類型轉(zhuǎn)換看看拋出的異常。
public class EncryptApp {
public void printClassLoader() {
System.out.println("EncryptApp:" + this.getClass().getClassLoader());
System.out.println("MyClass.class.getClassLoader() = " + MyClass.class.getClassLoader());
new MyClass();
}
}
public static void main(String[] args)
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
MyCustomClassLoader myCustomClassLoader = new MyCustomClassLoader();
myCustomClassLoader.init();
Class<?> startupClass = myCustomClassLoader.loadClass("lang.classloader.encrypt.EncryptApp");
// 重要:必須通過反射的方式獲取方法墓卦,
// 因?yàn)楫?dāng)前線程的classloader倦春,和加載 EncryptApp 的不一樣,
// 所以不能類型轉(zhuǎn)換落剪,必須用object
Object encryptApp = startupClass.getConstructor().newInstance();
String methodName = "printClassLoader";
Method method = encryptApp.getClass().getMethod(methodName);
method.invoke(encryptApp);
}
結(jié)果如下:
// EncryptApp 是有 MyCustomClassLoader 加載
EncryptApp:lang.classloader.encrypt.MyCustomClassLoader@1a6c5a9e
// EncryptApp 啟動(dòng)類加載 MyClass 也是使用 MyCustomClassLoader
MyClass.class.getClassLoader() = lang.classloader.encrypt.MyCustomClassLoader@1a6c5a9e
My class
總結(jié)
ClassLoader 是一個(gè)重要的工具睁本,但是平時(shí)很少需要自定義一個(gè) ClassLoader 。通過自定義 ClassLoader 加載字節(jié)碼還是令人興奮的忠怖。
從類的生命周期理解 ClassLoader呢堰,更清楚它能做什么。很多時(shí)候需要結(jié)合字節(jié)碼技術(shù)凡泣,更能發(fā)揮他的威力枉疼。很多框架也是這么做的,比如 APM鞋拟。
參考資料
- 深入理解Java虛擬機(jī):JVM高級特性與最佳實(shí)踐(第3版)
- 深入理解 jvm 字節(jié)碼