JAVA热更新

引言

知识储备先看这篇文章:JAVA Instrument

在这个案例中我们会利用Instrument机制实现一个简单的热更新案例。

总体来说,步骤如下:

  1. 创建一个带premain方法的jar包。这个方法定时检测某个文件然后进行热更新。
  2. 命令行启动业务类时使用参数-javaagent,例如java -javaagent:jarpath[=选项] Main

网上有很多案例都是用Mavenjar包的,但是这里我讲的是纯命令行的做法。

代码实现

结构如下

在这里插入图片描述
这个是编译后的结构,这里先从逻辑实现说起。

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. 编写业务类

主启动类:
声明一个名为wywUser,然后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

扫描二维码关注公众号,回复: 15708193 查看本文章
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

总结

通常我们无法通过热更新新增或者删除一个方法,但是我们可以修改方法内部的实现。
案例只是简单展示了相关的代码逻辑,固定时间固定类去进行热更新替换。
投射到具体的工程中,我们就需要根据这种逻辑因地制宜去实现我们的逻辑,或者说制定某种规范。

这里再给出一些我的思考。

检查的时机

在这里我是固定时间间隔检查一次,但是工程中,应该修改为每隔一段时间进行检查。

判断哪些类需要更新

在这里我直接写死。其他的做法还有:

  • 用一个文件来存储需要热更的路径,读取这个文件。
  • 储存当前类的修改时间,发现时间变化时进行更新。

猜你喜欢

转载自blog.csdn.net/sayWhat_sayHello/article/details/121083133