java程序如何實(shí)現(xiàn)hotswap
本文是根據(jù)《Java動(dòng)態(tài)追蹤技術(shù)探究》結(jié)合自己寫的一些demo來對(duì)java熱更新(hotswap)的一些見解妥曲。熱更就是不需重啟也修改程序。下面講一下實(shí)現(xiàn)熱更的幾種方式:
1. classLoader重載類
java中的class文件都是通過classLoader加載到程序中的满着,正常情況下畦攘,classLoader只會(huì)加載一次,經(jīng)過我的實(shí)驗(yàn):如果多次加載同一個(gè)類會(huì)報(bào)如下錯(cuò)誤:
Exception in thread "main" java.lang.LinkageError: loader (instance of hotswap/HotSwapClassLoader): attempted duplicate class definition for name: "hotswap/TestPrint"
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at hotswap.HotSwapClassLoader.loadByPath(HotSwapClassLoader.java:25)
at hotswap.Main.main(Main.java:20)
所以我們需要每次new一個(gè)新的類加載器鸳劳,強(qiáng)制程序重新加載類癞季,下面是我寫的demo:
package hotswap;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Scanner;
public class Main {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
URL url = new URL("file:E:\\testHotswap.jar");
URLClassLoader classloader = null;
while (true) {
Thread.sleep(1000);
classloader = new URLClassLoader(new URL[]{url});
Class clazz = Class.forName("hotswap.TestPrint", true, classloader);
Method method = clazz.getMethod("print");
method.invoke(clazz.newInstance());
}
}
}
package hotswap;
public class TestPrint {
/**
* @param
*/
public void print() {
System.out.println("bbb");
}
}
首先需要將建兩個(gè)project分別這兩個(gè)類放一個(gè)中锡移,至于為啥着逐,因?yàn)閏lassloader依照有雙親委派機(jī)制崔赌,如果在一個(gè)project中會(huì)都會(huì)被父classloader加載。
接下來演示如何進(jìn)行試驗(yàn):
-
把TestPrint所在的那個(gè)project打包成jar耸别,在idea中如此操作即可健芭,把jar包輸出目錄設(shè)在E盤:
設(shè)置輸出目錄 -
輸出jar包
輸出jar包 -
啟動(dòng)Main 類的main函數(shù):
啟動(dòng)并查看輸出
可以看到每隔1000毫秒輸出"aaa"字符串。
-
修改TestPrint.print()函數(shù)秀姐,重新打包jar慈迈,并查看Main 類的輸出:
修改后輸出
試驗(yàn)結(jié)束,通過使用classloader重新加載類實(shí)現(xiàn)了hotswap功能囊扳。《Java動(dòng)態(tài)追蹤技術(shù)探究》文章中提
JSP文件修改過后吩翻,之所以能及時(shí)生效兜看,是因?yàn)閃eb容器(Tomcat)會(huì)檢查請(qǐng)求的JSP文件是否被更改過锥咸。如果發(fā)生過更改,那么就將JSP文件重新解析翻譯成一個(gè)新的Sevlet類细移,并加載到JVM中搏予。之后的請(qǐng)求,都會(huì)由這個(gè)新的Servet來處理弧轧。這里有個(gè)問題雪侥,根據(jù)Java的類加載機(jī)制,在同一個(gè)ClassLoader中精绎,類是不允許重復(fù)的速缨。為了繞開這個(gè)限制,Web容器每次都會(huì)創(chuàng)建一個(gè)新的ClassLoader實(shí)例代乃,來加載新編譯的Servlet類旬牲。之后的請(qǐng)求都會(huì)由這個(gè)新的Servlet來處理,這樣就實(shí)現(xiàn)了新舊JSP的切換搁吓。
其實(shí)我的demo的做法與之相似原茅,但是這種做法成本太高,意味著程序員要寫適用的代碼去重新加載類堕仔,不具備通用型擂橘,即對(duì)任意一java程序可以替換任意class。
2. Instrumentation
這是個(gè)java提供的不通過classloader修改class的方法摩骨。我也是剛知道它的存在通贞。話不多說直接上代碼朗若。
首先我需要三個(gè)project,一個(gè)project1是平時(shí)正常運(yùn)行的項(xiàng)目昌罩,第二個(gè)project2打包成javaagent(jar包)捡偏,第三個(gè)project3將使用javaagent去修改project1中的類。
1. project1
package hotswap;
public class Main2 {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
while (true) {
Thread.sleep(1000);
new Print().print();
}
}
}
package hotswap;
public class Print {
public void print() {
System.out.println("aaa");
}
}
運(yùn)行起來后程序?qū)⒚扛?000ms輸出一個(gè)字符串峡迷,模仿一個(gè)線上一直在運(yùn)行的項(xiàng)目银伟。
2. project2
import java.io.*;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
public class Agent {
private static byte[] getBytes(String filePath){
byte[] buffer = null;
try {
File file = new File(filePath);
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
byte[] b = new byte[1000];
int n;
while ((n = fis.read(b)) != -1) {
bos.write(b, 0, n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return buffer;
}
public static void agentmain(String arg, Instrumentation instrumentation) {
System.err.println("agentmain , args is " + arg);
try {
ClassDefinition classDefinition = new ClassDefinition(Class.forName("hotswap.Print"),getBytes(arg));
instrumentation.redefineClasses(classDefinition);
} catch (Exception e) {
e.printStackTrace();
}
}
}
這個(gè)類將打包成一個(gè)jar包作為javaagent被加載,被加載時(shí)候?qū)?zhí)行· agentmain
方法绘搞,agentmain
有兩個(gè)參數(shù)arg
和instrumentation
彤避,args
是可自定義的參數(shù),instrumentation是程序能重載類的關(guān)鍵夯辖,他提供兩個(gè)關(guān)鍵方法redefineClasses
和retransformClasses
琉预,這里我使用了redefineClasses,他們的用途如下:
retransformClasses
:對(duì)于已經(jīng)加載的類重新進(jìn)行轉(zhuǎn)換處理蒿褂,即會(huì)觸發(fā)重新加載類定義圆米,需要注意的是,新加載的類不能修改舊有的類聲明啄栓,譬如不能增加屬性娄帖、不能修改方法聲明
redefineClasses
:與如上類似,但不是重新進(jìn)行轉(zhuǎn)換處理昙楚,而是直接把處理結(jié)果(bytecode)直接給JVM
(第二次修改:本質(zhì)上他們都起到了替換class
的作用近速,而且替換效果是一樣的:只不過retransformClasses
的修改是可回退的,redefineClasses
不可回退堪旧,因?yàn)?code>retransformClasses相當(dāng)于給字節(jié)碼提供了一層“代理”當(dāng)去掉代理后削葱,可以拿到原來的字節(jié)碼,而redefineClasses
是直接替換了字節(jié)碼淳梦。Java5中引入了重定義功能析砸,Java6中引入了重傳功能。我的猜測(cè)是重傳是作為一種更通用的功能引入的爆袍,但是為了向后兼容首繁,必須保留重定義。 再推薦一個(gè)開源的Java診斷工具arthas螃宙,其中有教程Arthas mc-redefine命令和Arthas mc-retransform命令蛮瞄,就是分別利用了這兩種方式修改class)
redefineClasses
使用需要傳入?yún)?shù)ClassDefinition ,ClassDefinition 可以理解為類定義谆扎,他需要完整類名以及被編譯的.class文件的byte[]數(shù)組來表示一個(gè)類挂捅。這里我傳入的args就是.class文件的目錄。
2. project3
使用javaagent去修改project1中的類堂湖。java調(diào)用javaagent有兩種方式闲先,一種是啟動(dòng)時(shí)加載(啟動(dòng)時(shí)加入?yún)?shù)如:-javaagent:D:\Redefine\out\artifacts\Redefine_jar\Redefine.jar=1111
)状土,另一種是運(yùn)行時(shí)候加載,而這個(gè)project3就需要運(yùn)行時(shí)加載伺糠。
package hotswap;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class AttachTest {
public static void main(String... args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
VirtualMachine virtualMachine = VirtualMachine.attach("12760");
virtualMachine.loadAgent("D:\\Redefine\\out\\artifacts\\Redefine_jar\\Redefine.jar","C:\\Users\\xuecm\\IdeaProjects\\untitled1\\out\\production\\untitled1\\hotswap\\Print.class");
virtualMachine.detach();
}
}
VirtualMachine 是jdk安裝目錄中tools.jar中的一個(gè)類蒙谓。attach
方法接受參數(shù)是要修改的java進(jìn)程pid,loadAgent
接受參數(shù)分別為project2打包成的jar的目錄以及一個(gè)自定義參數(shù)训桶。這里我第二個(gè)參數(shù)為Print.class的目錄累驮。結(jié)合project2,我希望重載Print.class舵揭。
接下來開始測(cè)試:
首先運(yùn)行project1:
修改Print類并重新編譯:
運(yùn)行project2并查看project1的運(yùn)行結(jié)果:
可以看到我的修改已經(jīng)生效