introduction
Read this article first for knowledge reserve: JAVA Instrument
In this case, we will use Instrument
the mechanism to implement a simple hot update case.
Overall, the steps are as follows:
- Create a package with
premain
methodsjar
. This method regularly detects a file and performs hot updates. - Parameters are used when starting a business class from the command line
-javaagent
, egjava -javaagent:jarpath[=选项] Main
.
There are many cases on the Internet that use Maven
packagingjar
, but here I am talking about the pure command line approach.
Code
The structure is as follows
This is the compiled structure, let's start with the logic implementation.
1. Write a premain
proxy class with methods
First declare a premain
class with methods. We set the class name as Agent
: this class first starts a thread, and this thread will replace the class
after 25 seconds . Among them, reading the file into bytes and then replacing is relevant knowledge.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. Write business class
Main startup class:
Declare a named class wyw
, User
and then print the properties of this class within 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());
}
}
}
The User class is defined as follows:
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;
}
}
Create jar
package, compile class
We first need to javac
compile the above implementation with:
PS C:\Users\QTZ\IdeaProjects\hotfix\src> javac .\com\wyw\User.java .\com\wyw\Agent.java .\com\wyw\Main.java
After execution, you can see that several files are generated class
.
Then to create this jar
package, you need to declare a list for packaging, in which you need to specify Premain-Class
the path, this list is named MANIFEST.MF
:
Manifest-Version: 1.0
Premain-Class: com.wyw.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Then on the command line:
PS C:\Users\QTZ\IdeaProjects\hotfix\src> jar -c -f agent.jar -m .\com\wyw\MANIFEST.MF
A will be generated after execution agent.jar
.
After these two basic steps are done, we start the class:
PS C:\Users\QTZ\IdeaProjects\hotfix\src> java -javaagent:agent.jar Main
The console will then produce output similar to this:
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
In the code logic of the case, the class will be replaced after 25 seconds, which means that we need to modify User
the class and compile it during the period.
For example, we will User
modify it to:
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;
}
}
Then compile it:
PS C:\Users\QTZ\IdeaProjects\hotfix\src> javac .\com\wyw\User.java
You can see the console output as:
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
That is, the replacement is successful.
Note that we cannot add or delete a method through hot update
Replace the original User
with the following method:
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: ";
}
}
After recompiling the class, the following error will be reported when running:
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
Summarize
Usually we cannot add or delete a method through hot update, but we can modify the internal implementation of the method.
The case simply shows the relevant code logic, and fixed time and fixed classes are used for hot update and replacement.
Projected into a specific project, we need to implement our logic according to local conditions according to this logic, or formulate some kind of specification.
Here are some more of my thoughts.
when to check
Here I check at regular intervals, but in engineering, it should be modified to check at regular intervals.
Determine which classes need to be updated
Here I write directly to death. Other approaches include:
- Use a file to store the paths that need to be hot-updated, and read this file.
- Store the modification time of the current class and update it when the time changes.