使用JAVA虚拟机提供的agentmain实现热更新,主要涉及到的模块:java工具包tools.jar
- 需要定义更新引擎
- 定义更新执行器
- 待更新的服务程序
1、快速开始
项目结构大致如下:
使用maven构建一个更新引擎,需要使用到maven编译jar插件,主要maven配置如下:
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifestFile>META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
上述文件中只要定义了maven在编译打包jar文件时,指定一个配置文件,这个配置文件是agentmain执行的配置,该配置信息如下:
Manifest-Version: 1.0
Can-Redefine-Classes: true
Agent-Class: xin.spring.hotload.ServerAgent
Can-Retransform-Classes: true
上述文件主要配置说明:
Manifest-Version:指定版本信息
Can-Redefine-Classes:指定类是否可以重定义
Can-Retransform-Classes:指定类是否可以宠你想你转换定义
Agent-Class:更新引擎程序类
1、编写引擎类:ServerAgent
package xin.spring.hotload;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* 服务器代理
*
* @author spring
* @date 2023/03/09
*/
public class ServerAgent {
// 写一个日志记录格式方法
public static void print(String str) {
long time = System.currentTimeMillis();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = sdf.format(time);
System.out.println(date + "----" + str);
}
// 读取class文件字节码
public static byte[] fileToBytes(File file) throws IOException {
FileInputStream in = new FileInputStream(file);
byte[] bytes = new byte[in.available()];
in.read(bytes);
in.close();
return bytes;
}
// 执行热更新
public static void agentmain(String args, Instrumentation inst) throws Exception {
long startTime = System.currentTimeMillis();
print(Thread.currentThread().getName() + ":agent 启动成功,开始重定义对象....");
print("args:" + args);
String[] classPathArr = args.split(",");
Class[] allClass = inst.getAllLoadedClasses();
// print("allClass:" + Arrays.toString(allClass));
Map<String, String> classMap = new HashMap<String, String>(16);
String className;
String filePath;
for (int i = 0; i < classPathArr.length; i++) {
String classPath = classPathArr[i];
print("classpath:" + classPath);
String[] arr = classPath.split("/");
className = arr[arr.length - 1];
filePath = classPath.replaceAll("\\.", "/") + ".class";
classMap.put(className, filePath);
print("targetPath:" + filePath);
}
print(classMap.keySet().toString());
try {
boolean isSuccess = false;
for (int i = 0; i < allClass.length; i++) {
Class c = allClass[i];
className = c.getName();
print("className:" + className);
filePath = classMap.get(className);
if (filePath != null) {
print("正在热更新class:" + className);
File file = new File(filePath);
try {
byte[] bytes = fileToBytes(file);
print("文件大小:" + bytes.length);
ClassDefinition classDefinition = new ClassDefinition(c, bytes);
inst.redefineClasses(new ClassDefinition[]{classDefinition});
isSuccess = true;
} catch (IOException var18) {
isSuccess = false;
var18.printStackTrace();
break;
}
}
}
long endTime = System.currentTimeMillis();
if (isSuccess) {
print(args + "热更新成功,runtime(" + (endTime - startTime) + ")....finish");
} else {
print(args + "热更新失败,runtime(" + (endTime - startTime) + ")....failed");
}
} catch (Exception var19) {
var19.printStackTrace();
}
}
}
2、定义一个主更新程序
该程序用于执行引擎服务
package xin.spring.hotload;
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;
/**
* 热更新服务器
*
* @author spring
* @date 2023/03/09
*/
public class HotUpdateServer {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
if (args != null && args.length >= 3) {
String agentJar = args[0];
String[] pidArr = args[1].split(",");
for (int i = 0; i < pidArr.length; i++) {
String pid = pidArr[i];
VirtualMachine vm = VirtualMachine.attach(pid);
System.out.println("正在热更新的pid是:" + pid);
vm.loadAgent(agentJar, args[2]);
}
} else {
System.out.println("至少需要AgentJar包路径和一个进程id!!!");
}
}
}
3、编写一个简单的程序用于热更新
使用maven构建以个game-module,项目结构大致如下:
热更新前的代码
package xin.spring.game;
/**
* 游戏逻辑
*
* @author spring
* @date 2023/03/09
*/
public class GameLogic {
private String name;
public GameLogic(String name) {
this.name = name;
}
public void play() {
System.out.println(name + " is playing!!");
}
}
热更新修改的代码
package xin.spring.game;
/**
* 游戏逻辑
*
* @author spring
* @date 2023/03/09
*/
public class GameLogic {
private String name;
public GameLogic(String name) {
this.name = name;
}
public void play() {
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:MM:SS");
System.out.println("["+format.format(date)+"]" + name + " is playing!!");
}
}
执行游戏主程序:
import java.util.concurrent.TimeUnit;
/**
* @author spring
*/
public class GameServerMain {
public static void main(String[] args) throws InterruptedException {
String name = "spring";
if (args.length > 0) {
name = args[0];
}
GameLogic gameLogic = new GameLogic(name);
while (true) {
gameLogic.play();
TimeUnit.SECONDS.sleep(1);
}
}
}
将我们的游戏打包成jar文件
4、执行程序,实现热更新
1、启动game-module游戏
java -Dfile.encoding=UTF-8 -classpath C:\dev-tools\java\jdk1.8.0_311\jre\lib\charsets.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\deploy.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\access-bridge-64.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\cldrdata.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\dnsns.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\jaccess.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\jfxrt.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\localedata.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\nashorn.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\sunec.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\sunjce_provider.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\sunmscapi.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\sunpkcs11.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\zipfs.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\javaws.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\jce.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\jfr.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\jfxswt.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\jsse.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\management-agent.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\plugin.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\resources.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\rt.jar;D:\ubuntu-os\hotJava\game-module-1.0-SNAPSHOT.jar xin.spring.game.GameServerMain spring
2、将game-module按照更新的代码重新编译打包
3、将打包后的game-module解压到指定位置:D:/ubuntu-os/hotJava/xin.spring.game.GameLogic
jar -xvf D:\ubuntu-os\hotJava\game-module-1.0-SNAPSHOT.jar
4、使用jps查看当前执行game-module程序pid
5、执行GameServerMain 热更程序
java -Dfile.encoding=UTF-8 -classpath C:\dev-tools\java\jdk1.8.0_311\jre\lib\charsets.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\deploy.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\access-bridge-64.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\cldrdata.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\dnsns.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\jaccess.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\jfxrt.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\localedata.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\nashorn.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\sunec.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\sunjce_provider.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\sunmscapi.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\sunpkcs11.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\ext\zipfs.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\javaws.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\jce.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\jfr.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\jfxswt.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\jsse.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\management-agent.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\plugin.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\resources.jar;C:\dev-tools\java\jdk1.8.0_311\jre\lib\rt.jar; xin.spring.hotload.HotUpdateServer
{更新引擎D:\ubuntu-os\hotJava\hot-load-agent-1.0-SNAPSHOT.jar} {JAVAPID} {更新的类D:/ubuntu-os/hotJava/xin.spring.game.GameLogic}
注意:xin.spring.hotload.HotUpdateServer后面的三个参数分别是:执行引擎jar、要更新的java程序pid、待更新的类项目目录+类全名
执行结果:
更新之前:
更新中:
更新后的执行结果: