# 前言
在之前的 深入淺出 JVM ClassLoader 一文中,我們說可以通過修改默認的類加載器實現熱部署,但在 Java 開發領域,熱部署一直是一個難以解決的問題,目前的 Java 虛擬機只能實現方法體的修改熱部署,對於整個類的結構修改,仍然需要重啟虛擬機,對類重新加載才能完成更新操作。對於某些大型的應用來說,每次的重啟都需要花費大量的時間成本,所以,如果能像我們之前說的那樣,在不重啟虛擬機的情況下更新一個類,在某些業務場景下變得十分重要。比如很多腳本語言就支持熱替換,例如 PHP,只要替換了PHP 源文件,這種改動就會立即生效,且無需重啟服務器。
今天我們就來一個簡單的熱部署,注意:不要小看他,這也是 JSP 支持修改的實現方式。
# 1. 怎么實現?
在上篇文章中,我們貼了一幅圖:
我們知道,一個類加載器只能加載一個同名類,在Java默認的類加載器層面作了判斷,如果已經有了該類,則不再重復加載,如果強行繞過判斷並使用自定義類加載器重復加載(比如調用 defineClass 方法),JVM 將會拋出 LinkageError:attempted duplicate class definition for name。
但請注意,我們說同一個類加載器不可以加載兩個同名的類,但不同的類加載器是可以加載同名的類的,加載完成之后,這兩個類雖然同名,但不是同一個 Class 對象,無法進行轉換。
那么我們是否可以利用這個特性,實現熱部署呢?如同上圖的步驟:使用自定義的類加載器,加載一個類,當需要進行替換類的時候,我們就丟棄之前的類加載器和類,使用新的類加載器去加載新的 Class 文件,然后運行新對象的方法。
讓我們按照這個思路寫段代碼試試吧!
class AccountMain {
public static void main(String[] args)
throws ClassNotFoundException, InterruptedException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
while (true) {
ClassLoader loader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
}
};
Class clazz = loader.loadClass("cn.think.in.java.clazz.loader.asm.Account");
Object account = clazz.newInstance();
account.getClass().getMethod("operation", new Class[]{}).invoke(account);
Thread.sleep(20000);
}
}
}
上面這個類是一個 mian 方法類,該方法是一個間隔 20 秒的死循環,步驟如下:
- 創建一個自定義的 ClassLoader 對象,加載類的步驟不遵守雙親委派模型,而是直接加載。
- 使用剛剛創建的類加載器加載指定的類。
- 得到剛剛的Class 對象,使用反射創建對象,並調用對象的 operation 方法。
為什么間隔20秒呢?因為我們要在啟動之后,修改類,並重新編譯。因此需要20秒時間。
再看看 Account 類:
public class Account {
public void operation() {
System.out.println("operation...");
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
該類很簡單,只有一個方法,就是打印 operation...字符串。
我們還需要一個類,干什么用呢?我們剛剛說,需要修改 Account 類,然后重新編譯,為了方便,我們創建一個類,專門用於執行修改后的 Account 類,因為執行后肯定重新編譯了,省的我們去命令行使用 javac 了。
代碼如下:
class ReCompileAccount {
public static void main(String[] args) {
new Account().operation();
}
}
如何測試呢?
- 啟動AccountMain main 方法。會立刻打印出 operation... 字符串,並開始等待20秒。
- 修改 Account 類的字符串為 operation.....new,
- 啟動 ReCompileAccount 類,目的是重新編譯 Accoutn類。
- 等待 AccountMain 類的打印。
不出意外的話,最后結果如下:
看到了吧,我們已經成功的把Accout 類修改了,並且是在不重啟 JVM 的情況下,實現了熱部署。就像我們剛剛說的,JSP 支持修改也是這么實現的,每一個 JSP 頁面都對應着一個類加載器,當JSP 頁面被修改了,就重新創建類加載器,然后使用新的類加載器加載 JSP (JSP 其實就是 Java 類)。
# 總結
基於 ClassLoader 的原理,我們實現了 Java 層面的熱部署,但大家如果自己實現一遍的話,還是覺得很麻煩,誠然,JSP 使用這種方式沒什么問題,因為他是自動編譯的。但如果我們自己的應用的話,難道每次修改一個類,都要重新編譯一遍,然后在給定的時間里去替換?我們能不能把這些手工活都交給 JVM 呢?實際上,Tocmat 也已經通過這種方式實現過了。限於篇幅,我們將在下一篇文章中講述。
good luck!!!!!