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方式來避免。