前景提要
Java 热更新 讲解了如何手动替换class文件, 然后通过监听文件是否被修改, 重复创建新的ClassLoader实现了热更新的功能.
这种方式的热更新是jvm原生支持的方式, 但是缺点也很明显:
-
不够灵活, 需要手动修改文件等操作
-
重复创建类加载器, 并且卸载困难, 会增加系统负担
-
使用起来具有代码侵入性, 需要对代码进行一定改造
javaagent是什么?
顾名思义, javaagent就是一个可以作为java代理的工具, 简单来说就是一个可供用于编写的java切面, 它的主要功能就是为用户提供了在 JVM 将字节码文件读入内存之后,JVM 使用对应的字节流在 Java 堆中生成一个 Class 对象之前,用户可以对其字节码进行修改的能力,从而 JVM 也将会使用用户修改过之后的字节码进行新的Class 对象的创建(打破了一个类只能加载一次的规则)。
javaagent的使用对于你自身的代码是无侵入性的.
从功能上来看, 它完美的解决了我们自定义类加载器实现热更新的缺点
javaagent的使用
javaagent根据加载时机的不同分为两种
-
通过命令行的形式, 启动java 项目时就声明使用javaagent
-
对于一个正在运行的项目, 使用javaagent, 此时的javaagent就类似一个可插拔的工具, 需要时才启动.
jvm启动前使用javaagent---premain模式
使用方法
javaagent本身作为java命令的一个参数, 可以在本身的项目启动前, 额外指定一个jar包, 该jar包包含了你期望通过javaagent实现的逻辑
具体的命令行demo如下:
一个java程序中-javaagent参数的个数是没有限制的,所以可以添加任意多个javaagent。所有的java agent会按照你定义的顺序执行
java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar
-
agent1.jar, agent2.jar是你按照要求打包生成的agent jar包 包含你期望在类加载前进行的处理
-
MyProgram.jar 自己业务的jar包
premain模式 agent jar包的实现流程
1. 定义MANIFEST.MF文件
首先需要一份配置文件, 通常包含以下配置
Manifest-Version: 1.0
Premain-Class: javaagent.PreMainTraceAgent
参数名 | 含义 |
---|---|
Manifest-Version | MANIFEST.MF的版本 |
Premain-Class | 包含premain方法的类全名 |
2. premain方法
创建一个类, 其中包含名为premain的静态方法即可, 无需继承, 主要修改的逻辑包含在transform方法中
package javaagent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class PreMainTraceAgent {
public static void premain(String agentArgs, Instrumentation inst) {
// agentArgs 外部参数
// inst Instrumentation的实例
System.out.println("agentArgs : " + agentArgs);
// 添加类转换器, 类似注册一个拦截器
// 类在第一次加载的时候发出 ClassFileLoad 事件, 会被拦截器拦截
inst.addTransformer(new DefineTransformer(), true);
}
// 每次类加载时, 都会触发ClassFileTransformer中的transform方法
// 因此就可以在这个方法中在类加载前, 对它进行拦截并处理
static class DefineTransformer implements ClassFileTransformer{
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 转换方法里可以实现对字节码的修改, 具体的修改可以采用ASM等方式
// demo只是单纯的打印字符串, 表示拦截成功
System.out.println("premain load Class:" + className);
return classfileBuffer;
}
}
}
3. 打包成jar
采用maven或者gradle打包即可 注意检查MANIFEST.MF文件打包后是否正确
项目结构如下
----src
--------main
--------|------java
--------|----------javaagent
--------|------------PreMainTraceAgent
--------|resources
-----------META-INF
--------------MANIFEST.MF
premain模式的运行原理
1.创建并初始化 JPLISAgent
2.MANIFEST.MF 文件的参数,并根据这些参数来设置 JPLISAgent 里的一些内容
3.监听 VMInit 事件,在 JVM 初始化完成之后做下面的事情:
(1)创建 InstrumentationImpl 对象 ;
(2)监听 ClassFileLoadHook 事件 ;
(3)调用 InstrumentationImpl 的loadClassAndCallPremain方法,在这个方法里会去调用 javaagent 中 MANIFEST.MF 里指定的Premain-Class 类的 premain 方法
优缺点
优点
-
类似切面, 能够成功拦截部分系统类和用户类, 在类第一次加载前对字节码进行修改, 没有侵入性, 对业务透明
-
自定义ClassFileTransformer里的transform方法, 在每次classLoader加载类的时候都会拦截触发, 也就是说你只要能够让classLoader重新加载类, 这部分逻辑都会生效, 可以做一些文章
缺点
-
jvm中的类只会被类加载器加载一次, 因此正常情况下transform方法对于一个类,同一个类加载器, 只会执行一次, 如果不重新定义类加载器加载类的话, 无法实现热更新功能.
-
对于已经正在运行的java项目, 没办法使用javaagent的功能
jvm启动后使用javaagent---agentmain模式
在premain模式的基础上, java升级提供了agentmain模式.
简而言之,agentmain 可以在类加载之后再次加载一个类,也就是重定义,你就可以通过在重定义的时候进行修改类了,甚至不需要创建新的类加载器,JVM 已经在内部对类进行了重定义(重定义的过程相当复杂)。
agentmain可以直接对一个正在运行的java程序起作用, 因此通过attach工具启动一个包含agentmain的jar包载入正在运行的java程序即可
agentmain模式 agent jar包的实现流程
1. 定义MANIFEST.MF文件
首先需要一份配置文件, 通常包含以下配置
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: cn.think.in.java.clazz.loader.asm.agent.PreMainTraceAgent
参数名 | 含义 |
---|---|
Manifest-Version | MANIFEST.MF的版本 |
Can-Redefine-Classes | true表示能重定义此代理所需的类,默认值为 false(可选) |
Can-Retransform-Classes | true 表示能重转换此代理所需的类,默认值为 false (可选) |
Agent-Class | 包含agenmain方法的类全名 |
注意在jvm前启动前使用javaagent的时候, 必须要定义Can-Redefine-Classes和Can-Retransform-Classes为true, 表示允许对一个类的二进制流在读取后进行重定义(改变字节码), 然后再进行相应类的加载流程.
2. agentmain方法
创建一个类, 其中包含名为agentmain的静态方法即可, 无需继承, 主要修改的逻辑包含在transform方法中
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);
}
通过执行retransformClasses方法, 使得正在运行的java程序中的某个类, 重新进行类加载的过程, 也就会被载入的agentmain方法拦截, 执行相应的逻辑
inst.retransformClasses(Account.class);
这段代码的意思是,重新转换目标类,也就是 Account 类。
也就是说,你需要重新定义哪个类,需要指定,否则 JVM 不可能知道。
3. 打包成jar包
采用maven或者gradle打包即可 注意检查MANIFEST.MF文件打包后是否正确
4. 通过attach工具加载
通过VirtualMachine类的attach(pid)方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。
// 列出所有VM实例
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// attach目标VM
VirtualMachine.attach(descriptor.id());
// 目标VM加载Agent
VirtualMachine#loadAgent("代理Jar路径","命令参数");
agentmain模式的运行原理
1.创建并初始化JPLISAgent
2.解析MANIFEST.MF 里的参数,并根据这些参数来设置 JPLISAgent 里的一些内容
3.监听 VMInit 事件,在 JVM 初始化完成之后做下面的事情:
(1)创建 InstrumentationImpl 对象 ;
(2)监听 ClassFileLoadHook 事件 ;
(3)调用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在这个方法里会去调用javaagent里 MANIFEST.MF 里指定的Agent-Class类的agentmain方法。
优缺点
优点
-
能够对运行中对java程序直接加载java agent, 无需在启动时指定
-
在不重新定义类加载器的情况下, 对于已经加载的类重新加载
-
没有侵入性, 对业务透明
-
比较完美的实现热更新的功能
缺点
- 因为涉及到对类对重新加载, 因此对于类字节码在修改时是有一定要求, 要求如下
1.父类是同一个;
2. 实习那的接口数也要相同;
3. 类访问符必须一致;
4. 字段数和字段名必须一致;
5. 新增的方法必须是 private static/final 的;
6. 可以删除修改方法;
premain模式和agentmain模式的对比
premain和agentmain两种方式最终的目的都是为了回调Instrumentation实例并激活sun.instrument.InstrumentationImpl#transform()(InstrumentationImpl是Instrumentation的实现类)从而回调注册到Instrumentation中的ClassFileTransformer实现字节码修改,本质功能上没有很大区别。两者的非本质功能的区别如下:
-
premain方式是JDK1.5引入的,agentmain方式是JDK1.6引入的,JDK1.6之后可以自行选择使用premain或者agentmain。
-
premain需要通过命令行使用外部代理jar包,即-javaagent:代理jar包路径;agentmain则可以通过attach机制直接附着到目标VM中加载代理,也就是使用agentmain方式下,操作attach的程序和被代理的程序可以是完全不同的两个程序。
-
premain方式回调到ClassFileTransformer中的类是虚拟机加载的所有类,这个是由于代理加载的顺序比较靠前决定的,在开发者逻辑看来就是:所有类首次加载并且进入程序main()方法之前,premain方法会被激活,然后所有被加载的类都会执行ClassFileTransformer列表中的回调。
-
agentmain方式由于是采用attach机制,被代理的目标程序VM有可能很早之前已经启动,当然其所有类已经被加载完成,这个时候需要借助Instrumentation#retransformClasses(Class<?>...classes)让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调。
-
通过premain方式的代理Jar包进行了更新的话,需要重启服务器,而agentmain方式的Jar包如果进行了更新的话,需要重新attach,但是agentmain重新attach还会导致重复的字节码插入问题,不过也有Hotswap和DCE VM方式来避免。