探秘 Java 热部署二(Java agent premain)
# 前言
在前文 探秘 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个要求:
- 这个 jar 包的MANIFEST.MF 文件必须指定 Premain-Class 项。
- 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 常用方法如下:
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。void redefineClasses(ClassDefinition... definitions) hrows ClassNotFoundException, UnmodifiableClassException;
在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。boolean removeTransformer(ClassFileTransformer transformer);
删除一个类转换器void retransformClasses(Class<?>... classes) throws UnmodifiableClassException
在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
3. 如何使用?
使用 javaagent 需要几个步骤:
- 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
- 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
- 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
- 使用参数 -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
- 创建一个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);
}
}
将 premain 的类和 MANIFEST.MF 文件打成 jar 包 .
使用 IDEA 的 build ,当然你也可以使用 maven。具体请 google。使用参数 -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)的更多相关文章
- JAVA热部署,通过agent进行代码增量热替换!!!
在前说明:好久没有更新博客了,这一年在公司做了好多事情,包括代码分析和热部署替换等黑科技,一直没有时间来进行落地写出一些一文章来,甚是可惜,趁着中午睡觉的时间补一篇介绍性的文章吧. 首先热部署的场景是 ...
- 探秘 Java 热部署三(Java agent agentmain)
前言 让我们继续探秘 Java 热部署.在前文 探秘 Java 热部署二(Java agent premain)中,我们介绍了 Java agent premain.通过在main方法之前通过类似 A ...
- 揭秘Java热部署原理及JRebel(Hotcode)的实现原理
基础知识:class卸载.热替换和Tomcat的热部署的分析HotSwap:HotSwap和JRebel原理成熟的热部署技术实现原理:深入探索 Java 热部署 java的热部署和热加载
- 探秘 Java 热部署
# 前言 在之前的 深入浅出 JVM ClassLoader 一文中,我们说可以通过修改默认的类加载器实现热部署,但在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能 ...
- 深入探索 Java 热部署
在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作.对于某些大型的应用来 ...
- 深入探索 Java 热部署--转
在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作.本文将探索如何在不破 ...
- Java 热部署深入探索
简介 在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作.对于某些大型的 ...
- 探索 Java 热部署
在 JAVA 开发领域,热部署一直是一个难以解决的问题,目前的 JAVA 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作.对于某些大型的应用来 ...
- springboot热部署(一)——Java热部署与热加载原理
一.概述 在应用运行的时升级软件,无需重新启动的方式有两种,热部署和热加载. 对于Java应用程序来说, 热部署就是在服务器运行时重新部署项目,——生产环境 热加载即在在运行时重新加载class,从而 ...
随机推荐
- ----关于posotion的sticky与fixed的区别----
sticky 出现在正常流当中,不能设置定位,随页面滚动 (sticky = relative + fixed) fixed 不出现在正常流当中,能设置定位,随页面滚动 eg: html: <p ...
- Zookeeper Client基础操作和Java调用
## Zookeeper > Zookeeper目前用来做数据同步,再各个服务之前同步关键信息 i.客户端操作 1. 创建 create [-s] [-e] path data acl -s 为 ...
- android开发笔记(3)
在android环境开发过程中,我最陌生的是虚拟机的创建以及使用,下面是虚拟机的创建以及使用过程. 创建虚拟机: 点击创建虚拟机的按钮,create->填写虚拟机相关的信息,确认之后创建. 创建 ...
- 64位版本为什么叫amd64,而不是intel64
64位版本为什么叫amd64,而不是intel64? 首先了解下常见的几个架构: X86是一个指令集,是刚有个人电脑时候的什么8086,286,386的那个兼容的指令集. “x86-64”,有时会 ...
- 《Linux就该这么学》第二天课程
秦时明月经典语录:很多人被命运安排,而我安排命运.——卫庄 今天介绍了VM 虚拟机的安装以及Linux系统的安装,还讲解了Linux内核 RPM:降低软件的安装难度 源代码+安装规则→将程序源代码与安 ...
- Linux学习---GCC编译常见错误
预处理错误: No such file or directory 出错原因:①包含错误:eg #include <abc.h> //abc.h为用户自行编写文件 解决方法:⑴应改为#in ...
- eclipse怎么删除多余的tomcat server(2)
首先你的Server要是可用状态,就是说当前这个tomcat处于可用状态才能点击那个Create Launch Configuration
- 2.3.7synchronized代码块有volatile同步的功能
关键字synchronized可以使多个线程访问同一个资源具有同步性,而且他还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能. package com.cky.thread; /** * ...
- 设计模式之观察者模式(c++)
Observer 模式应该可以说是应用最多.影响最广的模式之一,因为 Observer 的一个实例 Model/View/Control( MVC) 结构在系统开发架构设计中有着很重要的地位和意义, ...
- 如何减少SQL Server中的PREEMPTIVE_OS_WRITEFILEGATHER等待类型
在数据库大小分配期间,我正在等待类型PREEMPTIVE_OS_WRITEFILEGATHER.昨天,我将数据库大小配置为供应商建议的值.我们需要将数据库大小设置为700GB,保留150 GB的日志文 ...