前言

让我们继续探秘 Java 热部署。在前文 探秘 Java 热部署二(Java agent premain)中,我们介绍了 Java agent premain。通过在main方法之前通过类似 AOP 的方式添加 premain 方法,我们可以在类加载之前做修改字节码的操作,无论是第一次加载,还是每次新的 ClassLoader 加载,都会经过 ClassFileTransformer 的 transform 方法,也就是说,都可以在这个方法中修改字节码,虽然他的方法名是 premain ,但是我们确实可以利用这个特性做这个事情。

在文章的最后,我们也提到了,虽然相比较在自定义类中修改字节码,premain 没有什么侵入性,对业务透明,但是美中不足的是,他还需要在启动的时候增加参数。

我们还提到了,premain 虽然可以热部署,但是还需要重新创建类加载器,虽然,这的确也符合 JVM 关于类的唯一性的定义。但是,有一种情况,如果使用的是系统类加载器,我们也无法创建新的ClassLoader对象。那么我们也就无法重新加载类了,怎么办呢?还好 Java 6 为我们提供了一种方法,也就是今天的主角 agentmain。

1. 什么是 agentmain?

和 premain 师出同门,我们知道,premain 只能在类加载之前修改字节码,类加载之后无能为力,只能通过重新创建ClassLoader 这种方式重新加载。而 agentmain 就是为了弥补这种缺点而诞生的。简而言之,agentmain 可以在类加载之后再次加载一个类,也就是重定义,你就可以通过在重定义的时候进行修改类了,甚至不需要创建新的类加载器,JVM 已经在内部对类进行了重定义(重定义的过程相当复杂)。

但是这种方式对类的修改是由限制的,对比原来的老类,由如下要求:

1.父类是同一个;

2. 实习那的接口数也要相同;

3. 类访问符必须一致;

4. 字段数和字段名必须一致;

5. 新增的方法必须是 private static/final 的;

6. 可以删除修改方法;

可以看的出来,相比较重新创建类加载器,限制还是挺多的,最重要的字段是无法修改的。因此,使用的时候要注意。

但是,agentmain 还有一个强大的特点,就是目标程序什么都不需要管,就能够被代理。还记得 premain 是如何使用的吗?需要在目标应用启动的时候增加 -javaagent 参数。虽说没有侵入性,但相比 agentmain 而言,还是有侵入性的,毕竟 agentmain 什么都不要。目标程序独立运行,什么都不用管。

那我们就来试试吧!

2. 如何使用?

agentmain 使用步骤如下:

  1. 定义一个MANIFEST.MF 文件,文件中必须包含 Agent-Class;
  2. 创建一个 Agent-Class 指定的类,该类必须包含 agentmain 方法(参数和 premian 相同)。
  3. 将MANIFEST.MF 和 Agent 类打成 jar 包;
  4. 将 jar 包载入目标虚拟机。目标虚拟机将会自动执行 agentmain 方法执行方法逻辑,同时,ClassFileTransformer 也会长期有效,在每一个类加载器加载 Class 的时候都会拦截。

让我们按着步骤来一遍吧!

  1. 定义一个MANIFEST.MF 文件,文件中必须包含 Agent-Class;
Manifest-Version: 1.0
Can-Redefine-Classes: true
Agent-Class: cn.think.in.java.clazz.loader.asm.agent.AgentMainTraceAgent
Can-Retransform-Classes: true
  1. 创建一个 Agent-Class 指定的类,该类必须包含 agentmain 方法(参数和 premian 相同)。
public class AgentMainTraceAgent {

  public static void agentmain(String agentArgs, Instrumentation inst)
throws UnmodifiableClassException {
System.out.println("Agent Main called");
System.out.println("agentArgs : " + agentArgs);
inst.addTransformer(new ClassFileTransformer() { @Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("agentmain load Class :" + className);
return classfileBuffer;
}
}, true);
inst.retransformClasses(Account.class);
}

上面的类逻辑很简单,打印 Agent Main called,并打印参数,然后添加一个类转换器,转换器中的逻辑只是打印字符串,为了简单起见,并没有修改字节码(各位可自行使用ASM 等类库修改)。最后一点很重要,执行了 inst.retransformClasses(Account.class); 这段代码的意思是,重新转换目标类,也就是 Account 类。也就是说,你需要重新定义哪个类,需要指定,否则 JVM 不可能知道。还有一个类似的方法 redefineClasses ,注意,这个方法是在类加载前使用的。类加载后需要使用 retransformClasses 方法。

  1. 将MANIFEST.MF 和 Agent 类打成 jar 包;

    这个请自行 google。maven 或者 ide 都可以。
  2. 将 jar 包载入目标虚拟机。目标虚拟机将会自动执行 agentmain 方法执行方法逻辑,同时,ClassFileTransformer 也会长期有效,在每一个类加载器加载 Class 的时候都会拦截。

这段代码很重要:

class JVMTIThread {

  public static void main(String[] args)
throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
if (vmd.displayName().endsWith("AccountMain")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("E:\\self\\demo\\out\\artifacts\\test\\test.jar ", "cxs");
System.out.println("ok");
virtualMachine.detach();
}
}
}
}

注意:写这段代码的时候 IDE 可能提示找不到 jar 包,这时候将 jdk/lib/tools.jar 添加的项目的 classpath 中,具体请自行 google。

该main方法步骤如下:

  1. 获取当前系统所有的虚拟机,类似 jps 命令。
  2. 循环所有虚拟机,找到 AccountMain 虚拟机。
  3. 将当前JVM 链接上目标JVM,并加载 loadAgent jar 包且传递参数。
  4. 卸载虚拟机。

如何测试呢?

首先看测试类,也就是AccountMain类:

class AccountMain {

  public static void main(String[] args) throws InterruptedException {
for (;;) {
new Account().operation();
Thread.sleep(5000);
} }
}

当我们一切准备就绪,启动 AccountMain 后,再启动 JVMTIThread 类,结果如下:

可以看到,执行了1遍 operation方法后,我们启动了 attach jvm,随后,agentmain 方法就被执行了,并打印了我们我们传递的字符串参数,同时也进入到了 ClassFileTransformer 方法中,表示重新加载了 Account 类。有点遗憾的是,限于篇幅,我们没有修改字节码。不过楼主还是展示一下楼主修改字节码的结果吧,假设楼主想统计 operation 方法的时长,楼主将使用 ASM 增加一段统计时长的代码:

public class TimeStat {
static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void start() {
threadLocal.set(System.currentTimeMillis());
}
public static void end() {
long time = System.currentTimeMillis() - threadLocal.get();
System.out.print(Thread.currentThread().getStackTrace()[2] + "方法耗费时间: ");
System.out.println(time + "毫秒");
}
}

然后修改 agentmain 方法中ClassFileTransformer 的transform 逻辑,也就是在这里修改字节码。

运行结果如下:

可以看到,首先重定义了 Account 类,又主动加载了 TimeStat 类,然后生效,在 operation 字符串后面打印了方法的耗时。

总结

通过对 agentmain 的使用,我们感受到了他的强大,在目标程序丝毫不改动的,甚至连启动参数都不加的情况下,可以修改类,并且是运行后修改,而且不重新创建类加载器。其主要得益于 JVM 底层的对类的重定义,关于底层代码解释,Jvm 大神寒泉子有篇文章 JVM源码分析之javaagent原理完全解读 ,详细分析了 javaagent 的原理。但 agentmain 有一些功能上的限制,比如字段不能修改增减。所以,使用的时候需要权衡,到底使用哪种方式实现热部署。

说了这么久的热部署,其实就是动态或者说运行时修改类,大的方向说有2种方式:

  1. 使用 agentmain,不需要重新创建类加载器,可直接修改类,但是有很多限制。
  2. 使用 premain 可以在类第一次加载之前修改,加载之后修改需要重新创建类加载器。或者在自定义的类加载器种修改,但这种方式比较耦合。

无论是哪种,都需要字节码修改的库,比如ASM,javassist ,cglib 等,很多。总之,通过java.lang.instrument 包配合字节码库,可以很方便的动态修改类,或者进行热部署。

good luck!!!!!

参考: JVM源码分析之javaagent原理完全解读

《实战Java 虚拟机》

javaagent

Instrumentation 新功能

探秘 Java 热部署三(Java agent agentmain)的更多相关文章

  1. JAVA热部署,通过agent进行代码增量热替换!!!

    在前说明:好久没有更新博客了,这一年在公司做了好多事情,包括代码分析和热部署替换等黑科技,一直没有时间来进行落地写出一些一文章来,甚是可惜,趁着中午睡觉的时间补一篇介绍性的文章吧. 首先热部署的场景是 ...

  2. 探秘 Java 热部署二(Java agent premain)

    # 前言 在前文 探秘 Java 热部署 中,我们通过在死循环中重复加载 ClassLoader 和 Class 文件实现了热部署的功能,但我们也指出了缺点-----不够灵活.需要手动修改文件等操作. ...

  3. 揭秘Java热部署原理及JRebel(Hotcode)的实现原理

    基础知识:class卸载.热替换和Tomcat的热部署的分析HotSwap:HotSwap和JRebel原理成熟的热部署技术实现原理:深入探索 Java 热部署 java的热部署和热加载

  4. 探秘 Java 热部署

    # 前言 在之前的 深入浅出 JVM ClassLoader 一文中,我们说可以通过修改默认的类加载器实现热部署,但在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能 ...

  5. 深入探索 Java 热部署

    在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作.对于某些大型的应用来 ...

  6. 深入探索 Java 热部署--转

    在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作.本文将探索如何在不破 ...

  7. Java 热部署深入探索

    简介 在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作.对于某些大型的 ...

  8. 探索 Java 热部署

    在 JAVA 开发领域,热部署一直是一个难以解决的问题,目前的 JAVA 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作.对于某些大型的应用来 ...

  9. springboot热部署(一)——Java热部署与热加载原理

    一.概述 在应用运行的时升级软件,无需重新启动的方式有两种,热部署和热加载. 对于Java应用程序来说, 热部署就是在服务器运行时重新部署项目,——生产环境 热加载即在在运行时重新加载class,从而 ...

随机推荐

  1. 小白的CTF学习之路2——二进制数据基础与运算(上)

    今天的第二更,被我拖到了傍晚,嘿嘿,二进制这方面让本就数学不好的我很头疼,所以研究了一段时间 在学习之前我们先了解几个问题: 32位是几个字节? 01011100对于十进制是多少? 00001111向 ...

  2. IntelliJ IDEA使用心得之Maven项目篇(转)

    IntelliJ IDEA使用心得之Maven项目篇   今天和大家分享下,在IDEA中打开Maven项目的方法. 对于新版的IDEA可以直接打开Maven项目,但是对于旧版的IDEA需要使用Mave ...

  3. PowerShell工作流学习-1-嵌套工作流和嵌套函数

    关键点: a)嵌套深度没有任何语法限制,但是嵌套三个层次的工作流不支持任何通用参数,包括工作流通用参数 b)嵌套工作流可以调用当前范围和任何父范围内的工作流和函数 c)工作流不允许递归调用,脚本和函数 ...

  4. 设置navigation的title

      onReady(){         wx.setNavigationBarTitle({           title: this.data.title         });     } 

  5. 《mysql必知必会》学习_第19章_20180809_欢

    第19章 插入数据 P132 insert into customers VALUES(NULL,'Pep E.Lapew','100 Main Street',,Los Angeles','CA', ...

  6. 利用蒙特卡洛(Monte Carlo)方法计算π值[ 转载]

    部分转载自:https://blog.csdn.net/daniel960601/article/details/79121055 圆周率π是一个无理数,没有任何一个精确公式能够计算π值,π的计算只能 ...

  7. 利用Go2Shell 实现 Mac Finder 直接shell端打开当前文件夹

    Finder 窗口 ,点击下图所示的按钮(红色框内),即可打开Shell Terminal. 打开后,如图 用法 安装go2shell后,打开finder的application文件夹,找到go2sh ...

  8. wpf使用FFMEPG录制屏幕

    Simple function of recording screen based on ffmpeg Using WPF环境 Visual Studio 2017,dotNet Framework ...

  9. [转]kaldi中的在线识别----Online Recognizers

    转自: http://blog.csdn.net/wbgxx333/article/details/24932533 本文是kaldi学习联盟中@冒顿翻译的,下面是@冒顿的翻译结果,在这里感谢@冒顿的 ...

  10. git简易使用

    git的安装以及GitHub的注册这里就不说了,这里直接从上传开始. 1. 登录github进入settings 2. 添加SSH KEY,添加方式查看第3步 3. 由于本地Git仓库和Github仓 ...