文章来源:https://studyidea.cn/java-hotswap

一、前言

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

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

二、Arthas 热更新

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

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

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

public class 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

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

一般情况下,我们本地将会有源代码,上面的步骤我们可以进一步省略,我们可以先在自己 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 运行输出窗口。

另外很多应用监控工具,如: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,这样我们就完成了类的修改。

public class 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");
final byte[] bytes = new byte[(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) {
return new 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,连接目标虚拟机,触发热更新的代码。

public class 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。

public class 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 包。

HelloService 输出效果如下所示:

源代码地址: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 端口,输出如下:

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

图中参数如下:

-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 编译过程可能发生如下错误。

解决办法为在 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 导致。

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

4.5、热更新存在一些限制

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

五、彩蛋

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

六、帮助

1.深入探索 Java 热部署

2.Instrumentation 新功能

欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn

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

  1. 搭建带热更新功能的本地开发node server

    引言 使用webpack有一段时间了,对其中的热更新的大概理解是:对某个模块做了修改,页面只做局部更新而不需要刷新整个页面来进行更新.这样就能节省因为整个页面刷新所产生开销的时间,模块热加载加快了开发 ...

  2. Delphi - 手把手教你基于D7+Access常用管理系统架构的设计与实现 (更新中)

    前言 从事软件开发工作好多年了,学的越深入越觉得自己无知,所以还是要对知识保持敬畏之心,活到老,学到老! 健身和代码一样都不能少,身体是革命的本钱,特别是我们这种高危工种,所以小伙伴们运动起来!有没有 ...

  3. 为我们的SSR程序添加热更新功能

    前沿 通过上一篇文章 通过vue-cli3构建一个SSR应用程序 我们知道了什么是SSR,以及如何通过vue-cli3构建一个SSR应用程序.但是最后遗留了一些问题没有处理,就是没有添加开发时的热更新 ...

  4. 手把手教你把 Git 子模块更新到主项目

    本文以 skywalking-rocketbot-ui子模块合并到 skywalking 为例,手把手教你如何把 Git 子模块更新到主项目中去. 首先,把fork的skywalking项目克隆到本地 ...

  5. 手把手教你用vue-cli搭建vue项目

    手把手教你用vue-cli搭建vue项目 本篇主要是利用vue-cli来搭建vue项目,其中前提是node和npm已经安装好,文章结尾将会简单提到一个简单的例子.使用vue-cli搭建项目最开始我也是 ...

  6. 手把手教你学Dapr - 4. 服务调用

    上一篇:手把手教你学Dapr - 3. 使用Dapr运行第一个.Net程序 介绍 通过使用服务调用,您的应用程序可以使用标准的gRPC或HTTP协议与其他应用程序可靠.安全地通信. 为什么不直接用Ht ...

  7. 手把手教你做个人 app

    我们都知道,开发一个app很大程度依赖服务端:服务端提供接口数据,然后我们展示:另外,开发一个app,还需要美工协助切图.没了接口,没了美工,app似乎只能做成单机版或工具类app,真的是这样的吗?先 ...

  8. 推荐!手把手教你使用Git

    推荐!手把手教你使用Git 原文出处: 涂根华的博客   http://blog.jobbole.com/78960/ 一:Git是什么? Git是目前世界上最先进的分布式版本控制系统. 二:SVN与 ...

  9. 30分钟手把手教你学webpack实战

    30分钟手把手教你学webpack实战 阅读目录 一:什么是webpack? 他有什么优点? 二:如何安装和配置 三:理解webpack加载器 四:理解less-loader加载器的使用 五:理解ba ...

随机推荐

  1. 算数运算符and数据类型转换

    一元(单目)运算符有且只有一个运算参数,二元(双目)运算符有且只有两个运算参数. 二元运算符:+(加).-(减).*(乘)./(求商).%(求余) 一元运算符:+(正),-(负),++(自增),--( ...

  2. [JZOJ5773]【NOIP2008模拟】简单数学题

    Description       话说, 小X是个数学大佬,他喜欢做数学题.有一天,小X想考一考小Y.他问了小Y一道数学题.题目如下:      对于一个正整数N,存在一个正整数T(0<T&l ...

  3. [LUOGU1868] 饥饿的奶牛 - dp二分

    题目描述 有一条奶牛冲出了围栏,来到了一处圣地(对于奶牛来说),上面用牛语写着一段文字. 现用汉语翻译为: 有N个区间,每个区间x,y表示提供的x~y共y-x+1堆优质牧草.你可以选择任意区间但不能有 ...

  4. 怎样快速找到某一行代码的git提交记录

    利用notepad++提高问题分析效率,以及快速找到某一行代码的git提交记录 1. 全目录搜索/替换 Notepad++是一款强大的文本编辑工具,当知道大概的关键词但不知道在哪个日志时可以使用not ...

  5. OpenvSwitch系列之ovs-ofctl命令使用

    Open vSwitch系列之一 Open vSwitch诞生 Open vSwitch系列之二 安装指定版本ovs Open vSwitch系列之三 ovs-vsctl 命令使用 OpenvSwit ...

  6. Mobius反演学习

    这篇文章参考了许多资料和自己的理解. 先放理论基础. 最大公约数:小学学过,这里只提一些重要的公式: $·$若$a=b$,则$\gcd(a,b)=a=b$: $·$若$\gcd(a,b)=d$,则$\ ...

  7. 第3次作业-MOOC学习笔记:Python网络爬虫与信息提取

    1.注册中国大学MOOC 2.选择北京理工大学嵩天老师的<Python网络爬虫与信息提取>MOOC课程 3.学习完成第0周至第4周的课程内容,并完成各周作业 4.提供图片或网站显示的学习进 ...

  8. Java基础(42)AbstractSet类

    AbstractSet类的子类有HashSet(其子类是LinkedHashSet).EnumSet.TreeSet 1.HashSet public class HashSet<E> e ...

  9. OptimalSolution(1)--递归和动态规划(1)斐波那契系列问题的递归和动态规划

    一.斐波那契数列 斐波那契数列就是:当n=0时,F(n)=0:当n=1时,F(n)=1:当n>1时,F(n) = F(n-1)+F(n-2). 根据斐波那契数列的定义,斐波那契数列为(从n=1开 ...

  10. OC-音乐播放器-锁屏处理

    QQ音乐播放的过程中,锁屏状态下的效果如下: 也就是说,QQ音乐播放过程中,添加锁屏远程事件的监听. 本文只记录本人知道的小知识点,不提供完整的代码. 实现的原理: (1)获取锁屏歌曲信息中心:MPN ...