引言
知识储备先看这篇文章:JAVA Instrument
在这个案例中我们会利用Instrument
机制实现一个简单的热更新案例。
总体来说,步骤如下:
- 创建一个带
premain
方法的jar
包。这个方法定时检测某个文件然后进行热更新。 - 命令行启动业务类时使用参数
-javaagent
,例如java -javaagent:jarpath[=选项] Main
。
网上有很多案例都是用Maven
打jar
包的,但是这里我讲的是纯命令行的做法。
代码实现
结构如下
这个是编译后的结构,这里先从逻辑实现说起。
1. 编写一个带premain
方法的代理类
首先声明一个带premain
方法的类。类名我们定为Agent
:
这个类首先启动一个线程, 这个线程在25秒后会进行User
类的替换。
其中读取文件成字节然后替换则是Instrument
相关的知识。
package com.wyw;
import java.io.File;
import java.io.FileInputStream;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.util.concurrent.TimeUnit;
public class Agent {
public static void premain(String arg, Instrumentation instrumentation) {
System.out.println("premain starting ~");
Thread thread = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
Class<?> person = Class.forName("com.wyw.User");
File file = new File("C:\\Users\\QTZ\\IdeaProjects\\hotfix\\src\\com\\wyw\\User.class");
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bytes = fileInputStream.readAllBytes();
instrumentation.redefineClasses(new ClassDefinition(person, bytes));
} catch (Exception e) {
e.printStackTrace();
}
});
thread.start();
}
}
2. 编写业务类
主启动类:
声明一个名为wyw
的User
,然后50s内打印该类的属性。
package com.wyw;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) throws InterruptedException {
User user = new User("wyw");
int i = 0;
while (i < 50) {
TimeUnit.SECONDS.sleep(1);
System.out.println(i++ + ": " + user.getName());
}
}
}
User类定义如下:
package com.wyw;
public class User {
private static final String DEFAULT_PREFIX = "User: ";
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return DEFAULT_PREFIX + name;
}
}
创建jar
包,编译类
我们首先需要先用javac
编译上面的实现:
PS C:\Users\QTZ\IdeaProjects\hotfix\src> javac .\com\wyw\User.java .\com\wyw\Agent.java .\com\wyw\Main.java
执行后可以看到生成了几个class
文件。
随后创建这个jar
包需要先声明一份清单用来打包用,其中需要指定Premain-Class
的路径,这个清单名为MANIFEST.MF
:
Manifest-Version: 1.0
Premain-Class: com.wyw.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
随后在命令行中:
PS C:\Users\QTZ\IdeaProjects\hotfix\src> jar -c -f agent.jar -m .\com\wyw\MANIFEST.MF
执行后会生成一个agent.jar
。
这两部基础步骤搞定后,我们启动类:
PS C:\Users\QTZ\IdeaProjects\hotfix\src> java -javaagent:agent.jar Main
随后控制台就会产生类似一下的输出:
PS C:\Users\QTZ\IdeaProjects\hotfix\src> java -javaagent:agent.jar com.wyw.Main
premain starting ~
0: User: wyw
1: User: wyw
2: User: wyw
3: User: wyw
4: User: wyw
在案例的代码逻辑中在25秒后会进行类的替换,也就是说我们需要在期间修改User
类并且编译。
例如我们将User
修改为:
package com.wyw;
public class User {
private static final String DEFAULT_PREFIX = "!!!User: ";
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return DEFAULT_PREFIX + name;
}
}
随后进行编译:
PS C:\Users\QTZ\IdeaProjects\hotfix\src> javac .\com\wyw\User.java
可以看到控制台的输出为:
17: User: wyw
18: User: wyw
19: User: wyw
20: User: wyw
21: User: wyw
22: User: wyw
23: User: wyw
24: !!!User: wyw
25: !!!User: wyw
26: !!!User: wyw
27: !!!User: wyw
28: !!!User: wyw
29: !!!User: wyw
即替换成功。
注意我们无法通过热更新添加或者删除一个方法
将原本的User
替换成以下方法:
package com.wyw;
public class User {
private static final String DEFAULT_PREFIX = "User: ";
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return addMethod() + DEFAULT_PREFIX + name;
}
private String addMethod() {
return "Add: ";
}
}
重新编译类后运行会得到以下的报错:
20: User: wyw
21: User: wyw
22: User: wyw
23: User: wyw
java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method
at java.instrument/sun.instrument.InstrumentationImpl.redefineClasses0(Native Method)
at java.instrument/sun.instrument.InstrumentationImpl.redefineClasses(InstrumentationImpl.java:193)
at com.wyw.Agent.lambda$premain$0(Agent.java:28)
at java.base/java.lang.Thread.run(Thread.java:834)
24: User: wyw
25: User: wyw
总结
通常我们无法通过热更新新增或者删除一个方法,但是我们可以修改方法内部的实现。
案例只是简单展示了相关的代码逻辑,固定时间固定类去进行热更新替换。
投射到具体的工程中,我们就需要根据这种逻辑因地制宜去实现我们的逻辑,或者说制定某种规范。
这里再给出一些我的思考。
检查的时机
在这里我是固定时间间隔检查一次,但是工程中,应该修改为每隔一段时间进行检查。
判断哪些类需要更新
在这里我直接写死。其他的做法还有:
- 用一个文件来存储需要热更的路径,读取这个文件。
- 储存当前类的修改时间,发现时间变化时进行更新。