java程序如何实现hotswap
本文是根据《Java动态追踪技术探究》结合自己写的一些demo来对java热更新(hotswap)的一些见解。热更就是不需重启也修改程序。下面讲一下实现热更的几种方式:
1. classLoader重载类
java中的class文件都是通过classLoader加载到程序中的,正常情况下,classLoader只会加载一次,经过我的实验:如果多次加载同一个类会报如下错误:
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一个新的类加载器,强制程序重新加载类,下面是我写的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");
}
}
首先需要将建两个project分别这两个类放一个中,至于为啥,因为classloader依照有双亲委派机制,如果在一个project中会都会被父classloader加载。
接下来演示如何进行试验:
-
把TestPrint所在的那个project打包成jar,在idea中如此操作即可,把jar包输出目录设在E盘:
-
输出jar包
-
启动Main 类的main函数:
可以看到每隔1000毫秒输出"aaa"字符串。
-
修改TestPrint.print()函数,重新打包jar,并查看Main 类的输出:
试验结束,通过使用classloader重新加载类实现了hotswap功能。《Java动态追踪技术探究》文章中提
JSP文件修改过后,之所以能及时生效,是因为Web容器(Tomcat)会检查请求的JSP文件是否被更改过。如果发生过更改,那么就将JSP文件重新解析翻译成一个新的Sevlet类,并加载到JVM中。之后的请求,都会由这个新的Servet来处理。这里有个问题,根据Java的类加载机制,在同一个ClassLoader中,类是不允许重复的。为了绕开这个限制,Web容器每次都会创建一个新的ClassLoader实例,来加载新编译的Servlet类。之后的请求都会由这个新的Servlet来处理,这样就实现了新旧JSP的切换。
其实我的demo的做法与之相似,但是这种做法成本太高,意味着程序员要写适用的代码去重新加载类,不具备通用型,即对任意一java程序可以替换任意class。
2. Instrumentation
这是个java提供的不通过classloader修改class的方法。我也是刚知道它的存在。话不多说直接上代码。
首先我需要三个project,一个project1是平时正常运行的项目,第二个project2打包成javaagent(jar包),第三个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");
}
}
运行起来后程序将每隔1000ms输出一个字符串,模仿一个线上一直在运行的项目。
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();
}
}
}
这个类将打包成一个jar包作为javaagent被加载,被加载时候将执行· agentmain
方法,agentmain
有两个参数arg
和instrumentation
,args
是可自定义的参数,instrumentation是程序能重载类的关键,他提供两个关键方法redefineClasses
和retransformClasses
,这里我使用了redefineClasses,他们的用途如下:
retransformClasses
:对于已经加载的类重新进行转换处理,即会触发重新加载类定义,需要注意的是,新加载的类不能修改旧有的类声明,譬如不能增加属性、不能修改方法声明
redefineClasses
:与如上类似,但不是重新进行转换处理,而是直接把处理结果(bytecode)直接给JVM
(第二次修改:本质上他们都起到了替换class
的作用,而且替换效果是一样的:只不过retransformClasses
的修改是可回退的,redefineClasses
不可回退,因为retransformClasses
相当于给字节码提供了一层“代理”当去掉代理后,可以拿到原来的字节码,而redefineClasses
是直接替换了字节码。Java5中引入了重定义功能,Java6中引入了重传功能。我的猜测是重传是作为一种更通用的功能引入的,但是为了向后兼容,必须保留重定义。 再推荐一个开源的Java诊断工具arthas,其中有教程Arthas mc-redefine命令和Arthas mc-retransform命令,就是分别利用了这两种方式修改class)
redefineClasses
使用需要传入参数ClassDefinition ,ClassDefinition 可以理解为类定义,他需要完整类名以及被编译的.class文件的byte[]数组来表示一个类。这里我传入的args就是.class文件的目录。
2. project3
使用javaagent去修改project1中的类。java调用javaagent有两种方式,一种是启动时加载(启动时加入参数如:-javaagent:D:\Redefine\out\artifacts\Redefine_jar\Redefine.jar=1111
),另一种是运行时候加载,而这个project3就需要运行时加载。
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中的一个类。attach
方法接受参数是要修改的java进程pid,loadAgent
接受参数分别为project2打包成的jar的目录以及一个自定义参数。这里我第二个参数为Print.class的目录。结合project2,我希望重载Print.class。
接下来开始测试:
首先运行project1:
修改Print类并重新编译:
运行project2并查看project1的运行结果:
可以看到我的修改已经生效