Java 热更新 讲解了如何手动替换class文件, 而后经过监听文件是否被修改, 重复建立新的ClassLoader实现了热更新的功能.java
这种方式的热更新是jvm原生支持的方式, 可是缺点也很明显:服务器
不够灵活, 须要手动修改文件等操做jvm
重复建立类加载器, 而且卸载困难, 会增长系统负担maven
使用起来具备代码侵入性, 须要对代码进行必定改造ide
顾名思义, javaagent就是一个能够做为java代理的工具, 简单来讲就是一个可供用于编写的java切面, 它的主要功能就是为用户提供了在 JVM 将字节码文件读入内存以后,JVM 使用对应的字节流在 Java 堆中生成一个 Class 对象以前,用户能够对其字节码进行修改的能力,从而 JVM 也将会使用用户修改过以后的字节码进行新的Class 对象的建立(打破了一个类只能加载一次的规则)。工具
javaagent的使用对于你自身的代码是无侵入性的.post
从功能上来看, 它完美的解决了咱们自定义类加载器实现热更新的缺点复制代码
javaagent根据加载时机的不一样分为两种gradle
经过命令行的形式, 启动java 项目时就声明使用javaagent命令行
对于一个正在运行的项目, 使用javaagent, 此时的javaagent就相似一个可插拔的工具, 须要时才启动.代理
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包
首先须要一份配置文件, 一般包含如下配置
Manifest-Version: 1.0 Premain-Class: javaagent.PreMainTraceAgent复制代码
参数名 | 含义 |
---|---|
Manifest-Version | MANIFEST.MF的版本 |
Premain-Class | 包含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); } 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; } } }复制代码
采用maven或者gradle打包便可 注意检查MANIFEST.MF文件打包后是否正确
项目结构以下
----src --------main --------|------java --------|----------javaagent --------|------------PreMainTraceAgent --------|resources -----------META-INF --------------MANIFEST.MF复制代码
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的功能
在premain模式的基础上, java升级提供了agentmain模式.
简而言之,agentmain 能够在类加载以后再次加载一个类,也就是重定义,你就能够经过在重定义的时候进行修改类了,甚至不须要建立新的类加载器,JVM 已经在内部对类进行了重定义(重定义的过程至关复杂)。
agentmain能够直接对一个正在运行的java程序起做用, 所以经过attach工具启动一个包含agentmain的jar包载入正在运行的java程序便可
首先须要一份配置文件, 一般包含如下配置
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, 表示容许对一个类的二进制流在读取后进行重定义(改变字节码), 而后再进行相应类的加载流程.
建立一个类, 其中包含名为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 不可能知道。复制代码
采用maven或者gradle打包便可 注意检查MANIFEST.MF文件打包后是否正确
经过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路径","命令参数");复制代码
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两种方式最终的目的都是为了回调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方式来避免。