手把手教你实现热更新功能,带你了解 Arthas 热更新背后的原理

简介: 一天下午正在摸鱼的时候,测试小姐姐走了过来求助,说是需要改动测试环境 mock 应用。但是这个应用一时半会又找不到源代码存在何处。但是测试小姐姐的活还是一定要帮,突然想起了 Arthas 可以热更新应用代码,按照网上的步骤,反编译应用代码,加上需要改动的逻辑,最后热更新成功。对此,测试小姐姐很满意,并表示下次会少提 Bug。嘿嘿,以前一直对热更新背后原理很好奇,借着这个机会,研究一下热更新的原理。

一、前言

一天下午正在摸鱼的时候,测试小姐姐走了过来求助,说是需要改动测试环境 mock 应用。但是这个应用一时半会又找不到源代码存在何处。但是测试小姐姐的活还是一定要帮,突然想起了 Arthas 可以热更新应用代码,按照网上的步骤,反编译应用代码,加上需要改动的逻辑,最后热更新成功。对此,测试小姐姐很满意,并表示下次会少提 Bug。

嘿嘿,以前一直对热更新背后原理很好奇,借着这个机会,研究一下热更新的原理。

二、Arthas 热更新

我们先来看下 Arthas 是如何热更新的。

详情参考:阿里巴巴Arthas实践--jad/mc/redefine线上热更新一条龙

假设我们现在有一个 HelloService 类,逻辑如下,现在我们使用 Arthas 热更新代码,让其输出 hello arthas

publicclass HelloService {
    public static void main(String[] args) throws InterruptedException {
        while (true){
            TimeUnit.SECONDS.sleep(1);
            hello();
        }
    }
    public static void hello(){
        System.out.println("hello world");
    }
}

2.1、jad 反编译代码

首先运行 jad 命令反编译 class 文件获取源代码,运行命令如下:。

jad --source-only com.andyxh.HelloService > /tmp/HelloService.java

2.2、修改反编译之后的代码

拿到源代码之后,使用 VIM 等文本编辑工具编辑源代码,加入需要改动的逻辑。

2.3、查找 ClassLoader

然后使用 sc 命令查找加载修改类的 ClassLoader,运行命令如下:

$ sc -d  com.andyxh.HelloService | grep classLoaderHash
 classLoaderHash   4f8e5cde

这里运行之后将会得到  ClassLoader 哈希值。

2.4、 mc 内存编译源代码

使用 mc 命令编译上一步修改保存的源代码,生成最终 class 文件。

$ mc -c 4f8e5cde  /tmp/HelloService.java  -d /tmp
Memory compiler output:
/tmp/com/andyxh/HelloService.class
Affect(row-cnt:1) cost in 463 ms.

2.5、redefine 热更新代码

运行 redefine 命令:

$ redefine /tmp/com/andyxh/HelloService.class
redefine success, size: 1

热更新成功之后,程序输出结果如下:

33.jpg

一般情况下,我们本地将会有源代码,上面的步骤我们可以进一步省略,我们可以先在自己 IDE 上改动代码,编译生成 class 文件。这样我们只需要运行 redefine 命令即可。也就是说实际上起到作用只是  redefine

三、 Instrumentation 与 attach 机制

Arthas 热更新功能看起来很神奇,实际上离不开 JDK 一些 API,分别为 instrument API 与 attach API。

3.1 Instrumentation

Java Instrumentation 是 JDK5 之后提供接口。使用这组接口,我们可以获取到正在运行 JVM 相关信息,使用这些信息我们构建相关监控程序检测 JVM。另外, 最重要我们可以替换修改类的,这样就实现了热更新。

Instrumentation 存在两种使用方式,一种为 pre-main 方式,这种方式需要在虚拟机参数指定 Instrumentation 程序,然后程序启动之前将会完成修改或替换类。使用方式如下:

java -javaagent:jar Instrumentation_jar -jar xxx.jar

有没有觉得这种启动方式很熟悉,仔细观察一下 IDEA 运行输出窗口。

34.png

另外很多应用监控工具,如:zipkin、pinpoint、skywalking。

这种方式只能在应用启动之前生效,存在一定的局限性。

JDK6 针对这种情况作出了改进,增加 agent-main 方式。我们可以在应用启动之后,再运行 Instrumentation 程序。启动之后,只有连接上相应的应用,我们才能做出相应改动,这里我们就需要使用 Java 提供 attach API。

3.2 Attach API

Attach API 位于 tools.jar 包,可以用来连接目标 JVM。Attach API 非常简单,内部只有两个主要的类,VirtualMachineVirtualMachineDescriptor

VirtualMachine 代表一个 JVM 实例, 使用它提供 attach 方法,我们就可以连接上目标 JVM。

VirtualMachine vm = VirtualMachine.attach(pid);

VirtualMachineDescriptor 则是一个描述虚拟机的容器类,通过该实例我们可以获取到 JVM PID(进程 ID),该实例主要通过  VirtualMachine#list 方法获取。

for (VirtualMachineDescriptor descriptor : VirtualMachine.list()){
            System.out.println(descriptor.id());
        }

介绍完热更新涉及的相关原理,接下去使用上面 API 实现热更新功能。

四、实现热更新功能

这里我们使用 Instrumentation agent-main 方式。

4.1、实现 agent-main

首先需要编写一个类,包含以下两个方法:

public static void agentmain (String agentArgs, Instrumentation inst);          [1]
public static void agentmain (String agentArgs);            [2]

上面的方法只需要实现一个即可。若两个都实现,  [1] 优先级大于 [2],将会被优先执行。

接着读取外部传入 class 文件,调用 Instrumentation#redefineClasses,这个方法将会使用新 class 替换当前正在运行的 class,这样我们就完成了类的修改。

publicclass AgentMain {
    /**
     *
     * @param agentArgs 外部传入的参数,类似于 main 函数 args
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        // 从 agentArgs 获取外部参数
        System.out.println("开始热更新代码");
        // 这里将会传入 class 文件路径
        String path = agentArgs;
        try {
            // 读取 class 文件字节码
            RandomAccessFile f = new RandomAccessFile(path, "r");
            finalbyte[] bytes = newbyte[(int) f.length()];
            f.readFully(bytes);
            // 使用 asm 框架获取类名
            final String clazzName = readClassName(bytes);
            // inst.getAllLoadedClasses 方法将会获取所有已加载的 class
            for (Class clazz : inst.getAllLoadedClasses()) {
                // 匹配需要替换 class
                if (clazz.getName().equals(clazzName)) {
                    ClassDefinition definition = new ClassDefinition(clazz, bytes);
                    // 使用指定的 class 替换当前系统正在使用 class
                    inst.redefineClasses(definition);
                }
            }
        } catch (UnmodifiableClassException | IOException | ClassNotFoundException e) {
            System.out.println("热更新数据失败");
        }
    }
    /**
     * 使用 asm 读取类名
     *
     * @param bytes
     * @return
     */
    private static String readClassName(final byte[] bytes) {
        returnnew ClassReader(bytes).getClassName().replace("/", ".");
    }
}

完成代码之后,我们还需要往 jar 包 manifest 写入以下属性。

## 指定 agent-main 全名
Agent-Class: com.andyxh.AgentMain
## 设置权限,默认为 false,没有权限替换 class
Can-Redefine-Classes: true

我们使用 maven-assembly-plugin,将上面的属性写入文件中。

<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <!--指定最后产生 jar 名字-->
        <finalName>hotswap-jdk</finalName>
        <appendAssemblyId>false</appendAssemblyId>
        <descriptorRefs>
            <!--将工程依赖 jar 一块打包-->
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifestEntries>
                <!--指定 class 名字-->
                <Agent-Class>
                    com.andyxh.AgentMain
                </Agent-Class>
                <Can-Redefine-Classes>
                    true
                </Can-Redefine-Classes>
            </manifestEntries>
            <manifest>
                <!--指定 mian 类名字,下面将会使用到-->
                <mainClass>com.andyxh.JvmAttachMain</mainClass>
            </manifest>
        </archive>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id><!-- this is used for inheritance merges -->
            <phase>package</phase><!-- bind to the packaging phase -->
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

到这里我们就完成热更新主要代码,接着使用 Attach API,连接目标虚拟机,触发热更新的代码。

publicclass JvmAttachMain {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        // 输入参数,第一个参数为需要 Attach jvm pid 第二参数为 class 路径
        if(args==null||args.length<2){
            System.out.println("请输入必要参数,第一个参数为 pid,第二参数为 class 绝对路径");
            return;
        }
        String pid=args[0];
        String classPath=args[1];
        System.out.println("当前需要热更新 jvm pid 为 "+pid);
        System.out.println("更换 class 绝对路径为 "+classPath);
        // 获取当前 jar 路径
        URL jarUrl=JvmAttachMain.class.getProtectionDomain().getCodeSource().getLocation();
        String jarPath=jarUrl.getPath();
        System.out.println("当前热更新工具 jar 路径为 "+jarPath);
        VirtualMachine vm = VirtualMachine.attach(pid);//7997是待绑定的jvm进程的pid号
        // 运行最终 AgentMain 中方法
        vm.loadAgent(jarPath, classPath);
    }
}

在这个启动类,我们最终调用 VirtualMachine#loadAgent,JVM 将会使用上面 AgentMain 方法使用传入 class 文件替换正在运行 class。

4.2、运行

这里我们继续开头使用的例子,不过这里加入一个方法获取 JVM 运行进程 ID。

publicclass HelloService {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(getPid());
        while (true){
            TimeUnit.SECONDS.sleep(1);
            hello();
        }
    }
    public static void hello(){
        System.out.println("hello world");
    }
    /**
     * 获取当前运行 JVM PID
     * @return
     */
    private static String getPid() {
        // get name representing the running Java virtual machine.
        String name = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println(name);
        // get pid
        return name.split("@")[0];
    }
}

首先运行 HelloService,获取当前 PID,接着复制  HelloService 代码到另一个工程,修改 hello 方法输出 hello agent,重新编译生成新的 class 文件。

最后在命令行运行生成的 jar 包。

34.jpg

HelloService 输出效果如下所示:

353.jpg

源代码地址:https://github.com/9526xu/hotswap-example

4.3、调试技巧

普通的应用我们可以在 IDE 直接使用 Debug 模式调试程序,但是上面的程序无法直接使用 Debug。刚开始运行的程序碰到很多问题,无奈之下,只能选择最原始的办法,打印错误日志。后来查看 arthas 的文档,发现上面一篇文章介绍使用 IDEA  Remote Debug 模式调试程序。

首先我们需要在 HelloService  JVM 参数加入以下参数:

-Xrunjdwp:transport=dt_socket,server=y,address=8001

此时程序将会被阻塞,直到远程调试程序连接上 8001 端口,输出如下:

35.jpg

然后在 Agent-main 这个工程增加一个 remote 调试。

36.jpg

37.jpg

图中参数如下:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8001

Agent-main 工程打上断点,运行远程调试, HelloService  程序将会被启动。

最后在命令行窗口运行 Agent-main 程序,远程调试将会暂停到相应断点处,接下来调试就跟普通 Debug 模式一样,不再叙述。

4.4、相关问题

由于 Attach API 位于 tools.jar 中,而在 JDK8 之前 tools.jar 与我们常用JDK jar 包并不在同一个位置,所以编译与运行过程可能找不到该 jar 包,从而导致报错。

如果 maven 编译与运行都使用 JDK9 之后,不用担心下面问题。

maven 编译问题

maven 编译过程可能发生如下错误。

38.jpg

解决办法为在 pom 下加入 tools.jar 。

<dependency>
            <groupId>jdk.tools</groupId>
            <artifactId>jdk.tools</artifactId>
            <scope>system</scope>
            <version>1.6</version>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>

或者使用下面依赖。

<dependency>
            <groupId>com.github.olivergondza</groupId>
            <artifactId>maven-jdk-tools-wrapper</artifactId>
            <version>0.1</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>

程序运行过程 tools.jar 找不到

运行程序时抛出 java.lang.NoClassDefFoundError,主要原因还是系统未找到 tools.jar 导致。

39.jpg

在运行参数加入 -Xbootclasspath/a:${java_home}/lib/tools.jar,完整运行命令如下:

40.jpg

4.5、热更新存在一些限制

并不是所有改动热更新都将会成功,当前使用 Instrumentation#redefineClasses 还是存在一些限制。我们仅只能修改方法内部逻辑,属性值等,不能添加,删除方法或字段,也不能更改方法的签名或继承关系。

五、彩蛋

写完热更新代码,收到一封系统邮件提示 xxx bug 待修复。恩,说好的少提 Bug 呢 o(╥﹏╥)o。


相关文章
|
Arthas Java 测试技术
记一次使用 Arthas 热更新线上代码
引用参考第二条 - Arthas提醒您:诊断千万条,规范第一条,热更不规范,同事两行泪
1540 0
记一次使用 Arthas 热更新线上代码
|
Arthas 存储 运维
记Arthas实现一次CPU排查与代码热更新
线上代码经常会出现CPU占用过高的情况,按以往经验我会使用top指令,进一步借助于jstack去查看具体信息从而进行问题排查,但基本上都逃不过需要重新发包的局面,即使是一个增量包,应用也需要短暂停启。后来运维大兄弟让我试一下Arthas,说是可以进行代码的热更新操作,正好来试一下。
440 0
|
Arthas 存储 运维
60-微服务技术栈(高级):在线检测工具Arthas(实现CPU排查与代码热更新)
线上代码经常会出现CPU占用过高的情况,按以往经验我会使用top指令,进一步借助于jstack去查看具体信息从而进行问题排查,但基本上都逃不过需要重新发包的局面,即使是一个增量包,应用也需要短暂停启。后来运维大兄弟让我试一下Arthas,说是可以进行代码的热更新操作,正好来试一下。
386 0
|
Arthas 小程序 Java
Arthas,热更新线上代码神器
日常开发过程中,可能会遇到小问题,本地代码改完后,总得重新打包部署,线上代码才会生效,这样太麻烦了,这个时候就可以考虑使用Arthas(阿尔萨斯)工具来热更新线上代码,先使线上的功能能使用,后面有大的迭代版本的时候再打包部署。笔者使用了这个工具,发现这个操作实在是骚的很,太好用了,在此做下笔记,以供后续参考。
787 0
Arthas,热更新线上代码神器
|
Arthas 监控 IDE
谈谈阿里arthas背后的原理
谈谈阿里arthas背后的原理
谈谈阿里arthas背后的原理
|
Arthas 监控 Java
Java线上问题排查神器Arthas快速上手与原理浅谈
当你兴冲冲地开始运行自己的Java项目时,你是否遇到过如下问题: 程序在稳定运行了,可是实现的功能点了没反应。 为了修复Bug而上线的新版本,上线后发现Bug依然在,却想不通哪里有问题? 想到可能出现问题的地方,却发现那里没打日志,没法在运行中看到问题,只能加了日志输出重新打包——部署——上线 程序功能正常了,可是为啥响应时间这么慢,在哪里出现了问题? 程序不但稳定运行,而且功能完美,但跑了几天或者几周过后,发现响应速度变慢了,是不是内存泄漏了?
631 0
|
Arthas 监控 Cloud Native
Java 线上问题排查神器 Arthas 快速上手与原理浅谈
相信我,Arhas 绝对是你提升效率的利器,适合各种阶段的开发者,尤其适合我这种刚入门的新人(天天上班写 Bug 的人)。你不应该有这种东西是高阶程序员才应该去使用的思想,放心大胆的去用吧!
13492 5
Java 线上问题排查神器 Arthas 快速上手与原理浅谈
|
Arthas 存储 运维
记 Arthas 实现一次 CPU 排查与代码热更新
线上代码经常会出现 CPU 占用过高的情况,按以往经验我会使用 top 指令,进一步借助于 jstack 去查看具体信息从而进行问题排查,但基本上都逃不过需要重新发包的局面,及时是一个增量包,应用也需要短暂停启。后来运维大兄弟让我试一下 Arthas,说是可以进行代码的热更新操作,正好来试一下。
记 Arthas 实现一次 CPU 排查与代码热更新
|
5月前
|
Arthas 监控 Java
Java 诊断利器 Arthas使用
Java 诊断利器 Arthas使用
187 0
|
5月前
|
Arthas 监控 Java
Arthas 可以用于监控和诊断在 Windows 系统下部署的 Tomcat 服务
Arthas 可以用于监控和诊断在 Windows 系统下部署的 Tomcat 服务
753 2