參考資料
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虛擬機(JVM)之后,將按照指定代理的順序調(diào)用每個premain方法,然后調(diào)用真正的應(yīng)用程序main方法。每個premain方法必須返回,以便繼續(xù)啟動程序。
agent Jar包中的manifest文件必須包含Premain-Class, 指向代理的入口類,這個類中包含了一個公共靜態(tài)的premain方法
premain 方法有兩種簽名,虛擬機會嘗試先運行下面這個
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ī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啟動后啟動代理的機制,包含了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虛擬機
VirtualMachine表示已附加到的Java虛擬機。它所附加的Java虛擬機有時稱為目標(biāo)虛擬機或目標(biāo)VM。應(yīng)用程序(通常是managemet控制臺或分析器之類的工具)使用虛擬機將代理加載到目標(biāo)VM中。例如,用Java語言編寫的分析器工具可能附加到正在運行的應(yīng)用程序,并加載其分析器代理來分析正在運行的應(yīng)用程序。
通過調(diào)用帶有標(biāo)識目標(biāo)虛擬機的標(biāo)識符的attach方法來獲得虛擬機。標(biāo)識符依賴于實現(xiàn),但在每個Java虛擬機都在自己的操作系統(tǒng)進程中運行的環(huán)境中,標(biāo)識符通常是進程標(biāo)識符(或pid)。另外,通過使用從list方法返回的虛擬機描述符列表中獲得的VirtualMachineDescriptor調(diào)用attach方法來獲得虛擬機實例。一旦獲得對虛擬機的引用,就使用loadAgent、loadAgentLibrary和loadAgentPath方法將代理加載到目標(biāo)虛擬機中。loadAgent方法用于加載用Java語言編寫并部署在JAR文件中的代理。loadAgentLibrary和loadAgentPath方法用于加載部署在動態(tài)庫或靜態(tài)鏈接到VM并使用JVM工具接口的代理。
除了加載代理之外,虛擬機還提供對目標(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();
虛擬機對于多個并發(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虛擬機規(guī)范中定義的完全限定類名和接口名。例如:"java/util/List"。
- classBeingRedefined 如果這是由重新定義或重新轉(zhuǎn)換觸發(fā)的,則這個類存在重新定義或重新轉(zhuǎn)換,否則為null。
- protectionDomain 正在定義或重新定義的類的保護域
- 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在啟動后的某個時候提供啟動代理的機制時。在這種情況下,將一個插裝實例傳遞給代理代碼的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