手把手教你实现热更新功能,带你了解 Arthas 热更新背后的原理
一、前言
一天下午正在摸鱼的时候,测试小姐姐走了过来求助,说是需要改动测试环境 mock 应用。但是这个应用一时半会又找不到源代码存在何处。但是测试小姐姐的活还是一定要帮,突然想起了 Arthas 可以热更新应用代码,按照网上的步骤,反编译应用代码,加上需要改动的逻辑,最后热更新成功。对此,测试小姐姐很满意,并表示下次会少提 Bug。
嘿嘿,以前一直对热更新背后原理很好奇,借着这个机会,研究一下热更新的原理。
二、Arthas 热更新
我们先来看下 Arthas 是如何热更新的。
假设我们现在有一个 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 非常简单,内部只有两个主要的类,VirtualMachine
与 VirtualMachineDescriptor
。
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 热更新背后的原理的更多相关文章
- 搭建带热更新功能的本地开发node server
引言 使用webpack有一段时间了,对其中的热更新的大概理解是:对某个模块做了修改,页面只做局部更新而不需要刷新整个页面来进行更新.这样就能节省因为整个页面刷新所产生开销的时间,模块热加载加快了开发 ...
- Delphi - 手把手教你基于D7+Access常用管理系统架构的设计与实现 (更新中)
前言 从事软件开发工作好多年了,学的越深入越觉得自己无知,所以还是要对知识保持敬畏之心,活到老,学到老! 健身和代码一样都不能少,身体是革命的本钱,特别是我们这种高危工种,所以小伙伴们运动起来!有没有 ...
- 为我们的SSR程序添加热更新功能
前沿 通过上一篇文章 通过vue-cli3构建一个SSR应用程序 我们知道了什么是SSR,以及如何通过vue-cli3构建一个SSR应用程序.但是最后遗留了一些问题没有处理,就是没有添加开发时的热更新 ...
- 手把手教你把 Git 子模块更新到主项目
本文以 skywalking-rocketbot-ui子模块合并到 skywalking 为例,手把手教你如何把 Git 子模块更新到主项目中去. 首先,把fork的skywalking项目克隆到本地 ...
- 手把手教你用vue-cli搭建vue项目
手把手教你用vue-cli搭建vue项目 本篇主要是利用vue-cli来搭建vue项目,其中前提是node和npm已经安装好,文章结尾将会简单提到一个简单的例子.使用vue-cli搭建项目最开始我也是 ...
- 手把手教你学Dapr - 4. 服务调用
上一篇:手把手教你学Dapr - 3. 使用Dapr运行第一个.Net程序 介绍 通过使用服务调用,您的应用程序可以使用标准的gRPC或HTTP协议与其他应用程序可靠.安全地通信. 为什么不直接用Ht ...
- 手把手教你做个人 app
我们都知道,开发一个app很大程度依赖服务端:服务端提供接口数据,然后我们展示:另外,开发一个app,还需要美工协助切图.没了接口,没了美工,app似乎只能做成单机版或工具类app,真的是这样的吗?先 ...
- 推荐!手把手教你使用Git
推荐!手把手教你使用Git 原文出处: 涂根华的博客 http://blog.jobbole.com/78960/ 一:Git是什么? Git是目前世界上最先进的分布式版本控制系统. 二:SVN与 ...
- 30分钟手把手教你学webpack实战
30分钟手把手教你学webpack实战 阅读目录 一:什么是webpack? 他有什么优点? 二:如何安装和配置 三:理解webpack加载器 四:理解less-loader加载器的使用 五:理解ba ...
随机推荐
- tf.split
tf.split(dimension, num_split, input):dimension的意思就是输入张量的哪一个维度,如果是0就表示对第0维度进行切割.num_split就是切割的数量,如果是 ...
- if循环判断
if循环判断 if-else循环的语法格式 if 逻辑判断句: 代码块 # 缩进表示所属关系 else 逻辑判断句: 代码块 if 和elif同时使用来做多层判断 if 逻辑判断式: 代码块 ...
- tp5底层源码分析之------tp5.1类的自动加载机制
tp框架作为国内主流框架,目前已经发布了6.0版本,相当于3.*版本是进行了重构,今天我们从源码的角度来研究下tp5.1自动加载的实现 作为单入口框架,从入口文件看起,入口文件在public/下,那么 ...
- 微软并发Key-Value存储库FASTER介绍
微软支持并发的Key-Value 存储库有C++与C#两个版本.号称迄今为止最快的并发键值存储.下面是C#版本翻译: FASTER C#可在.NET Framework和.NET Core中运行,并且 ...
- Redis(十五)Redis 的一些常用技术(Spring 环境下)
一.Redis 事务与锁机制 1.Redis的基础事务 在Redis中开启事务的命令是 multi 命令, 而执行事务的命令是 exec 命令.multi 到 exec 命令之间的 Redis 命令将 ...
- fenby C语言 P26
指针 地址 类型+“*”+变量名=指针变量 int *p1;//指向整型变量的指针变量p1 float *p2;//指向浮点型变量的指针变量p2 char *p3;//指向字符型变量的指针变量p3 “ ...
- 设计模式C++描述----02.模板(Template)模式
一. 问题 在面向对象系统的分析与设计过程中经常会遇到这样一种情况:对于某一个业务逻辑(算法实现)在不同的对象中有不同的细节实现,但是逻辑(算法)的框架(或通用的应用算法)是相同的.Template提 ...
- Spring使用@Async注解
本文讲述@Async注解,在Spring体系中的应用.本文仅说明@Async注解的应用规则,对于原理,调用逻辑,源码分析,暂不介绍.对于异步方法调用,从Spring3开始提供了@Async注解,该注解 ...
- 盘点飞思卡尔i.MX多媒体处理器前世今生 (转)
现如今,移动处理器领域,大家关注最多的是德州仪器.高通.展讯.MTK,甚至包括Intel,但是请别忘记飞思卡尔,他的i.MX处理器已经发展到第六代. 那么我们今天就来盘点下i.MX的前世今生吧. i. ...
- tomcat的虚拟路径的配置
在一些项目中有时候需要把一些文件(例如图片.视频)存储在硬盘上的,如果是把文件在放在硬盘上的话,怎么才能访问到这些文件昵. 好了.下面就为大家讲讲如何利用tomcat 虚拟路径访问硬盘上的文件.总共有 ...