參考資料
instrument 規(guī)范
https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html?is-external=true
Class VirtualMachine
Interface ClassFileTransformer
https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/ClassFileTransformer.html
java agent 實現(xiàn)
代理需要編譯成jar包的方式來運行篙骡,JAR文件manifest中的屬性指定將加載哪個代理類來啟動代理傅物。
有下面兩種方式可以啟動一個agent
一:命令行接口
程序沒有啟動時可以通過在命令行上指定javaagent的方式來啟動代理
-javaagent:jarpath[=options]
#如:
java -javaagent:xxx-agent.jar -cp xxx.jar com.wwh.xxxx
通過命令行的方式可以指定多個代理,并且支持參數(shù)苫费。初始化Java虛擬機(jī)(JVM)之后珍手,將按照指定代理的順序調(diào)用每個premain方法办铡,然后調(diào)用真正的應(yīng)用程序main方法。每個premain方法必須返回琳要,以便繼續(xù)啟動程序寡具。
agent Jar包中的manifest文件必須包含Premain-Class, 指向代理的入口類稚补,這個類中包含了一個公共靜態(tài)的premain方法
premain 方法有兩種簽名童叠,虛擬機(jī)會嘗試先運行下面這個
public static void premain(String agentArgs, Instrumentation inst);
如果沒有會嘗試調(diào)用下面這個
public static void premain(String agentArgs);
當(dāng)使用命令行選項啟動代理時,不會調(diào)用agentmain方法课幕。
代理類將由系統(tǒng)類加載器加載(參見ClassLoader.getSystemClassLoader)厦坛。這是類加載器,它通常加載包含應(yīng)用程序主方法的類乍惊。premain方法將在與應(yīng)用程序main方法相同的安全性和類加載器規(guī)則下運行杜秸。對于代理premain方法可以做什么,沒有建模限制润绎。application main可以做的任何事情撬碟,包括創(chuàng)建線程诞挨,都是合法的。
每個代理都通過agentArgs參數(shù)傳遞其代理選項呢蛤。代理選項作為單個字符串傳遞惶傻,任何額外的解析都應(yīng)該由代理本身執(zhí)行。
二:在虛擬機(jī)啟動之后再啟動代理
程序已經(jīng)啟動后可以通過VirtualMachine 來加載啟動代理其障,如下:
VirtualMachine vm = VirtualMachine.attach("2177");
vm.loadAgent(jar);
vm.detach();
注意:
- 代理JAR的manifest中必須包含屬性 Agent-Class银室。此屬性的值是代理類的名稱。
- 代理類必須實現(xiàn)一個公共靜態(tài)的 agentmain 方法励翼,如下所示蜈敢。
啟動代理時先嘗試運行
public static void agentmain(String agentArgs, Instrumentation inst);
找不到再嘗試運行
public static void agentmain(String agentArgs);
agentmain方法不能阻塞,這個類同用可以擁有 premain 方法抚笔,不過并不會被調(diào)用
參數(shù)通過如下方式指定:
vm.loadAgent(jar, options);
Manifest 屬性說明
代理JAR文件定義了以下清單屬性:
- Premain-Class 此屬性指定代理類扶认,也就是包含premain方法的類。如果該屬性不存在,JVM將中止踩验。注意這是一個類名拒课,而不是文件名或路徑。
- Agent-Class 指定代理類海洼,支持在VM啟動后啟動代理的機(jī)制,包含了agentmain 方法的類,如果該屬性不存在敞葛,則代理將不會啟動。注意這是一個類名与涡,而不是文件名或路徑惹谐。
- Boot-Class-Path 引導(dǎo)類裝入器要搜索的路徑列表
- Can-Redefine-Classes 布爾值( true 或 false ,不區(qū)分大小寫)驼卖。代理是否可以重新定義類氨肌。此屬性是可選的,默認(rèn)為false酌畜。
- Can-Retransform-Classes 布爾值( true 或 false 怎囚,不區(qū)分大小寫)。代理是否可以重新轉(zhuǎn)換類桥胞。此屬性是可選的恳守,默認(rèn)為false。
- Can-Set-Native-Method-Prefix 布爾值( true 或 false 贩虾,不區(qū)分大小寫)催烘。代理是否可以設(shè)置本地方法前綴。此屬性是可選的缎罢,默認(rèn)為false伊群。
代理JAR文件可同時具有清單中的Premain-Class和Agent-Class屬性喳钟。當(dāng)使用-javaagent選項在命令行上啟動代理時,執(zhí)行Premain-Class屬性指定的
代理類在岂,而Agent-Class屬性將被忽略奔则。類似地,如果代理在VM啟動之后再啟動蔽午,則執(zhí)行Agent-Class屬性指定的代理類易茬,而忽略Premain-Class屬性的值。
相關(guān)類說明
幾個關(guān)鍵類和接口
VirtualMachine
表示一個java虛擬機(jī)
VirtualMachine表示已附加到的Java虛擬機(jī)及老。它所附加的Java虛擬機(jī)有時稱為目標(biāo)虛擬機(jī)或目標(biāo)VM抽莱。應(yīng)用程序(通常是managemet控制臺或分析器之類的工具)使用虛擬機(jī)將代理加載到目標(biāo)VM中。例如骄恶,用Java語言編寫的分析器工具可能附加到正在運行的應(yīng)用程序食铐,并加載其分析器代理來分析正在運行的應(yīng)用程序。
通過調(diào)用帶有標(biāo)識目標(biāo)虛擬機(jī)的標(biāo)識符的attach方法來獲得虛擬機(jī)僧鲁。標(biāo)識符依賴于實現(xiàn)虐呻,但在每個Java虛擬機(jī)都在自己的操作系統(tǒng)進(jìn)程中運行的環(huán)境中,標(biāo)識符通常是進(jìn)程標(biāo)識符(或pid)寞秃。另外斟叼,通過使用從list方法返回的虛擬機(jī)描述符列表中獲得的VirtualMachineDescriptor調(diào)用attach方法來獲得虛擬機(jī)實例。一旦獲得對虛擬機(jī)的引用春寿,就使用loadAgent朗涩、loadAgentLibrary和loadAgentPath方法將代理加載到目標(biāo)虛擬機(jī)中。loadAgent方法用于加載用Java語言編寫并部署在JAR文件中的代理绑改。loadAgentLibrary和loadAgentPath方法用于加載部署在動態(tài)庫或靜態(tài)鏈接到VM并使用JVM工具接口的代理谢床。
除了加載代理之外,虛擬機(jī)還提供對目標(biāo)VM中的系統(tǒng)屬性的讀訪問厘线。這在某些環(huán)境中非常有用识腿,比如java。home皆的、os.name或os覆履。arch用于構(gòu)造將加載到目標(biāo)VM的代理的路徑。
一個啟動jmx的例子
// attach to target VM
VirtualMachine vm = VirtualMachine.attach("2177");
// start management agent
Properties props = new Properties();
props.put("com.sun.management.jmxremote.port", "5000");
vm.startManagementAgent(props);
// detach
vm.detach();
虛擬機(jī)對于多個并發(fā)線程的使用是安全的费薄。
ClassFileTransformer
代理提供此接口的實現(xiàn)硝全,以便轉(zhuǎn)換類文件。轉(zhuǎn)換發(fā)生在JVM定義類之前楞抡。
一個代理提供者需要實現(xiàn):ClassFileTransformer 接口伟众,來轉(zhuǎn)變class文件,這個接口有一個方法
byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
此方法的實現(xiàn)可以轉(zhuǎn)換提供的類文件并返回一個新的替換類文件
一旦向addTransformer注冊了一個transformer召廷,每個新的類定義和每個類重定義都會調(diào)用這個transformer凳厢。每個類重新轉(zhuǎn)換時也將調(diào)用具有重新轉(zhuǎn)換能力的轉(zhuǎn)換器账胧。
對新類定義的請求是用 ClassLoader.defineClass 或本地調(diào)用觸發(fā)的。
對類的重定義請求是通過 Instrumentation.redefineClasses 或本地調(diào)用觸發(fā)的先紫。
對類重新轉(zhuǎn)換的請求是通過 Instrumentation.retransformClasses 或本地調(diào)用觸發(fā)的治泥。
在處理請求期間,在類文件字節(jié)被驗證或應(yīng)用之前調(diào)用轉(zhuǎn)換器遮精。
當(dāng)有多個轉(zhuǎn)換器時居夹,轉(zhuǎn)換由鏈接轉(zhuǎn)換調(diào)用組成。也就是說本冲,一個轉(zhuǎn)換調(diào)用返回的字節(jié)數(shù)組將成為下一個調(diào)用的輸入(通過classfileBuffer參數(shù))准脂。
關(guān)于transform輸入的classfileBuffer參數(shù):
如果實現(xiàn)方法確定不需要轉(zhuǎn)換,則返回null檬洞。否則狸膏,它應(yīng)該創(chuàng)建一個新的byte[]數(shù)組,將輸入classfileBuffer連同所有所需的轉(zhuǎn)換復(fù)制到其中添怔,并返回新數(shù)組湾戳。不能修改輸入classfileBuffer。
在重新轉(zhuǎn)換和重新定義的情況下澎灸,轉(zhuǎn)換器必須支持重新定義語義:如果轉(zhuǎn)換器在初始定義期間更改的類稍后被重新轉(zhuǎn)換或重新定義院塞,轉(zhuǎn)換器必須確保第二個類輸出類文件是第一個輸出類文件的合法重新定義。
如果transformer拋出異常(它沒有捕獲異常)性昭,后續(xù)的transformer仍然會被調(diào)用,并且負(fù)載县遣、重新定義或重新轉(zhuǎn)換仍然會被嘗試糜颠。因此,拋出異常的效果與返回null相同萧求。為了防止在transformer代碼中生成未檢查異常時出現(xiàn)意外行為其兴,transformer可以捕獲Throwable。如果轉(zhuǎn)換器認(rèn)為classFileBuffer不代表一個有效格式化的類文件夸政,它應(yīng)該拋出一個IllegalClassFormatException;而這與返回null具有相同的效果元旬。它有助于記錄或調(diào)試格式錯誤。
參數(shù)說明:
- loader 要轉(zhuǎn)換的類的定義類加載器守问,如果是bootstrap loader則為空
- className 類名的內(nèi)部形式為Java虛擬機(jī)規(guī)范中定義的完全限定類名和接口名匀归。例如:"java/util/List"。
- classBeingRedefined 如果這是由重新定義或重新轉(zhuǎn)換觸發(fā)的耗帕,則這個類存在重新定義或重新轉(zhuǎn)換穆端,否則為null。
- protectionDomain 正在定義或重新定義的類的保護(hù)域
- classfileBuffer 類文件格式的輸入字節(jié)緩沖區(qū)-不能修改
返回:
格式良好的類文件緩沖區(qū)(轉(zhuǎn)換的結(jié)果)仿便,如果沒有執(zhí)行轉(zhuǎn)換体啰,則為null攒巍。
Instrumentation
該類提供測試Java編程語言代碼所需的服務(wù)。插裝是將字節(jié)碼添加到方法中荒勇,以便收集工具使用的數(shù)據(jù)柒莉。由于這些更改純粹是附加的,所以這些工具不會修改應(yīng)用程序狀態(tài)或行為沽翔。此類良性工具的例子包括監(jiān)視代理常柄、分析器、覆蓋率分析器和事件日志記錄器搀擂。
獲取Instrumentation 接口實例有兩種方法:
- 當(dāng)JVM以指示代理類的方式啟動時西潘。在這種情況下,將一個插裝實例傳遞給代理類的premain方法哨颂。
- 當(dāng)JVM在啟動后的某個時候提供啟動代理的機(jī)制時喷市。在這種情況下,將一個插裝實例傳遞給代理代碼的agentmain方法威恼。
一旦代理獲得一個Instrumentation 實例品姓,代理可以在任何時候調(diào)用該實例上的方法。
Instrumentation.addTransformer(new Transformer(), true);
第二個參數(shù)表示是否可以重新轉(zhuǎn)換已經(jīng)定義好了的類
對于啟動后再附加agent的方式箫措,如果想要改變已經(jīng)加載了的類腹备,需要設(shè)置為true
并且注意修改manifest文件中的
Can-Retransform-Classes: true
否則會報錯:
adding retransformable transformers is not supported in this environment
示例
pom 文件示例:
<dependencies>
<dependency>
<groupId>jdk.tools</groupId>
<artifactId>jdk.tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!-- 參數(shù)方式啟動agent需要這個 -->
<Premain-Class>
com.wwh.agentmain.AgentMain
</Premain-Class>
<!-- 啟動后附加啟動agent需要這個 -->
<Agent-Class>
com.wwh.agentmain.AgentMain
</Agent-Class>
<!-- 是否可以重新轉(zhuǎn)換類 -->
<Can-Retransform-Classes>
true
</Can-Retransform-Classes>
<!-- 是否可以重新定義類 -->
<Can-Redefine-Classes>
true
</Can-Redefine-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
manifest文件示例:
META-INF/MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: com.wwh.agentmain.AgentMain
Archiver-Version: Plexus Archiver
Built-By: Administrator
Agent-Class: com.wwh.agentmain.AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: Apache Maven 3.5.3
Build-Jdk: 1.8.0_151