# 前言

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

如果有那么一种功能,当你需要重新加载类并修改类的时候,有那么一个转换器自动帮你修改已有的 Class 文件变成你设定的 Class 文件,那么就不需要手动修改编译了。

也许你第一想到的就是在自定义类加载器中做文章,比如在 loadClass 中,得到字节码之后,通过 ASM 或者 javassist 修改字节码,然后再调用 defineClass 方法。

确实可行,但是这种侵入性太大。如果JVM 在底层提供一种类似 “类转换器” 的东西,是不是侵入性就不大了呢?

实际上,JVM 确实给我们提供了一个工具,那就是今天的主角------java agent。

1. 什么是 Java agent

在 JDK 1.5 中,Java 引入了 java.lang.Instrument 包,该包提供了一些工具帮助开发人员在 Java 程序运行时,动态修改系统中的 Class 类型。其中,使用该软件包的一个关键组件就是 Java agent。从名字上看,似乎是个 Java 代理之类的,而实际上,他的功能更像是一个Class 类型的转换器,他可以在运行时接受重新外部请求,对Class 类型进行修改。

如果在命令行执行 java 命令,会出现一些命令帮助,其中就有 java agent的选项:

2. Java agent 详细介绍

参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:

  1. 这个 jar 包的MANIFEST.MF 文件必须指定 Premain-Class 项。
  2. Premain-Class 指定的那个类必须实现 premain()方法。

重点就在 premain 方法,也就是我们今天的标题。从字面上理解,就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行 -javaagent 所指定 jar 包内 Premain-Class 这个类的 premain 方法,其中,该方法可以签名如下:

1.public static void premain(String agentArgs, Instrumentation inst)

2.public static void premain(String agentArgs)

JVM 会优先加载 1 签名的方法,加载成功忽略 2,如果1 没有,加载 2 方法。这个逻辑在sun.instrument.InstrumentationImpl 类中:

参数 agentArgs 时通过命令行传给 Java Agent 的参数, inst 是 Java Class 字节码转换的工具,Instrumentation 常用方法如下:

  1. void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。

  2. void redefineClasses(ClassDefinition... definitions) hrows ClassNotFoundException, UnmodifiableClassException;

    在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。

  3. boolean removeTransformer(ClassFileTransformer transformer);

    删除一个类转换器

  4. void retransformClasses(Class<?>... classes) throws UnmodifiableClassException

    在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。

3. 如何使用?

使用 javaagent 需要几个步骤:

  1. 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
  2. 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
  3. 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
  4. 使用参数 -javaagent:/jar包路径=[agentArgs 参数] 启动要代理的方法。

在执行以上步骤后,JVM 会先执行 premain 方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。当然,遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。

也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,注意:是类加载之前。也就是说,我们可以在这个缝隙中做很多文章,比如修改字节码。

让我们来试试:

1 首先定义一个 MANIFEST.MF 文件:

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: cn.think.in.java.clazz.loader.asm.agent.PreMainTraceAgent
  1. 创建一个Premain-Class 指定的类,类中包含 premain 方法:
public class PreMainTraceAgent {

  public static void premain(String agentArgs, Instrumentation inst) {
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("premain load Class :" + className);
return classfileBuffer;
}
}, true);
}
}
  1. 将 premain 的类和 MANIFEST.MF 文件打成 jar 包 .

    使用 IDEA 的 build ,当然你也可以使用 maven。具体请 google。

  2. 使用参数 -javaagent:/jar包路径=[agentArgs 参数] 启动要代理的方法。

    我们当然需要一个测试类:


class AccountMain { public static void main(String[] args)
throws ClassNotFoundException, InterruptedException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
while (true) {
Account account = new Account();
account.operation();
}
} } class Account {
public void operation() {
System.out.println("operation....");
}
}

VM 参数 -javaagent:/jar包路径=[agentArgs 参数] 。

运行结果:

可以看到,我们的 premain 方法确实在 main 方法之前被调用了,并且是在类加载的时候被调用的,而我们重写的 transform 方法其中的 classfileBuffer 参数就是即将被载入虚拟机的字节码,因此,我们可以使用各种字节码库进行修改。具体修改,这里就暂时不表,以后有机会好好写写。

4. 和热部署有什么关系?

说了这么多,Java agent 确实可以在 main 方法之前并加载字节码的同时进行代理,有点类似 AOP。但到底和热部署有什么关系呢?

回忆刚开始我们说的,我们如果自己自定义一个类加载器,那么就可以在重新加载类(新的类加载器)的时候对字节码进行修改,但是对业务代码侵入性较大,如果在底层,也就是 JVM 层面,在加载字节码的时候回调某个方法,在该方法中修改字节码,岂不是达到了我们的目的?

我们再看看 premain 方法:

  public static void premain(String agentArgs, Instrumentation inst) {
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("premain load Class :" + className);
return classfileBuffer;
}
}, true);
}

该方法中的 Instrumentation 添加了一个类转换器,该转换器是长期有效的,当该转换器被添加之后,只要有类加载的活动,都会被拦截。假设,我们的业务是当某个类需要修改,我们就重新加载(重新创建类加载器的前提)原来的字节码,加载之后,注意:加载之后对该字节码进行修改。而这些操作对业务代码来说,完全是透明的,基本没有侵入性(加入了 VM 参数)。

总结

通过上面的步骤,我们将 探秘 Java 热部署 的代码进行了优化,原本是直接手动修改字节码,现在通过加载原来的字节码,在原来的字节码基础上进行修改,再重新加载,完成了一次热部署。

而着一切得益于 java agent,虽然使用自定义的类加载也可以做到,但是似乎显得不是很优雅。使用 java agent 能让修改字节码这个动作化于无形,对业务透明,减少侵入性。

实际上,premain 还是有缺点的。什么缺点?竟然还要再命令行加参数?能不能不加参数?可以! java 1.6 已经为我们准备了这个工具。也就是 agentmain ,可以不加任何参数,就可以修改一个类,甚至不需要重新创建类加载器!神奇吗?我们说,一个类加载器只能加载一个类,要想修改一个类,必须重新创建一个新的类加载器。但是,JVM 为我们做了很多,他在底层直接修改了类定义。使得我们不必重新创建类加载器了。具体我们将在下篇文章中详细介绍。

good luck!!!!!

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

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

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

  2. 探秘 Java 热部署三(Java agent agentmain)

    前言 让我们继续探秘 Java 热部署.在前文 探秘 Java 热部署二(Java agent premain)中,我们介绍了 Java agent premain.通过在main方法之前通过类似 A ...

  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. sqlserver删除临时表中的数据

    select distinct * into #tmptable from tablename drop table tablename select * into tablename from #t ...

  2. idea执行mapreduce报错 Could not locate Hadoop executable: C:\hadoop-3.1.1\bin\winutils.exe

    window执行mapreduce报错 Exception in thread "main" java.lang.RuntimeException: java.io.FileNot ...

  3. Testng用例失败重新运行

    Testng用例失败重新运行   在ui测试用例的运行过程中,发现有很多不确定的因素会导致用例失败,比如网络原因,比如屏幕滑动失败等.想到需要让测试用例,在失败后重新运行来提高测试成功率. 在gith ...

  4. mysql数据库到底是什么?!

    MySql是MySql.AB公司开发的,采用客户/服务器模型的开放源码关系型SQL数据库管理系统,它可以在多种操作系统上运行. 客户端/服务器:C/S.需要给不同系统安装不同的软件,是专用的协议,比较 ...

  5. 2017-2018-1 20155326信息安全系统设计基础》嵌入式C语言课上考试补交

    2017-2018-1 20155326信息安全系统设计基础>嵌入式C语言课上考试补交 PPT上的例子 已知位运算规则为: &0 --> 清零 &1 --> 不变 | ...

  6. C#使用iTextSharp+ZXing.Net+FreeSpire.PDF生成和打印pdf文档

    项目需求(Winform)可以批量打印某个模板,经过百度和摸索,使用iTextSharp+ZXing.Net+FreeSpire.PDF三个类库实现了生成pdf.生成条形码和打印pdf功能. 首先在项 ...

  7. Codeforces gym101612 E.Equal Numbers(贪心)

    传送:http://codeforces.com/gym/101612 题意:给出一个大小为n的序列a[i],每次选其中一个数乘以一个正整数,问进行k步操作后最少剩下多少种数字,输出0≤k≤n,所有的 ...

  8. 简介 - RESTful

    RESTful REST(Representational State Transfer,表现层状态转化),可以简单理解为"资源在网络中以某种表现形式进行状态转移" Resourc ...

  9. Testing - 软件测试知识梳理 - 比较质量保证(QA)与质量控制(QC)

    QA QC QM 概念 Quality Assurance (质量保证) Quality Control (质量控制) Quality Manage (质量管理) 定义 为达到质量要求所采取的作业技术 ...

  10. 聊聊我面试过的一个最奇葩的 Java 程序猿!

    上周我聊了聊最让我反感的 10 种程序猿,无奈一个小时就进行了删除,详细原因就不说了,容易招黑. 今天聊的我面试过的最奇葩的一个程序猿,绝对是奇葩中的奇葩,简直是程序猿中的另类,最让我反感的程序猿又添 ...