使用JAVA虚拟机提供的agentmain实现热更新,主要涉及到的模块:java工具包tools.jar

  • 需要定义更新引擎
  • 定义更新执行器
  • 待更新的服务程序

1、快速开始

项目结构大致如下:

Properties java 更新某个配置 java配置热更新_java

使用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,项目结构大致如下:

Properties java 更新某个配置 java配置热更新_java_02

热更新前的代码

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、待更新的类项目目录+类全名

执行结果:

更新之前:

Properties java 更新某个配置 java配置热更新_java_03

更新中:

Properties java 更新某个配置 java配置热更新_java_04


更新后的执行结果:

Properties java 更新某个配置 java配置热更新_java_05