您好,登錄后才能下訂單哦!
這篇文章主要介紹關(guān)于java agent的使用方法,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
JVM啟動前靜態(tài)Instrument
Java agent 是什么?
Java agent是java命令的一個參數(shù)。參數(shù) javaagent 可以用于指定一個 jar 包,并且對該 java 包有2個要求:
premain 方法,從字面上理解,就是運行在 main 函數(shù)之前的的類。當Java 虛擬機啟動時,在執(zhí)行 main 函數(shù)之前,JVM 會先運行-javaagent
所指定 jar 包內(nèi) Premain-Class 這個類的 premain 方法 。
在命令行輸入 java
可以看到相應(yīng)的參數(shù),其中有 和 java agent相關(guān)的:
-agentlib:<libname>[=<選項>] 加載本機代理庫 <libname>, 例如 -agentlib:hprof
另請參閱 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<選項>]
按完整路徑名加載本機代理庫
-javaagent:<jarpath>[=<選項>]
加載 Java 編程語言代理, 請參閱 java.lang.instrument
在上面-javaagent
參數(shù)中提到了參閱java.lang.instrument
,這是在rt.jar 中定義的一個包,該路徑下有兩個重要的類:
該包提供了一些工具幫助開發(fā)人員在 Java 程序運行時,動態(tài)修改系統(tǒng)中的 Class 類型。其中,使用該軟件包的一個關(guān)鍵組件就是 Javaagent。從名字上看,似乎是個 Java 代理之類的,而實際上,他的功能更像是一個Class 類型的轉(zhuǎn)換器,他可以在運行時接受重新外部請求,對Class類型進行修改。
從本質(zhì)上講,Java Agent 是一個遵循一組嚴格約定的常規(guī) Java 類。 上面說到 javaagent命令要求指定的類中必須要有premain()方法,并且對premain方法的簽名也有要求,簽名必須滿足以下兩種格式:
public static void premain(String agentArgs, Instrumentation inst) public static void premain(String agentArgs)
JVM 會優(yōu)先加載 帶 Instrumentation
簽名的方法,加載成功忽略第二種,如果第一種沒有,則加載第二種方法。這個邏輯在sun.instrument.InstrumentationImpl 類中:
Instrumentation 類 定義如下:
public interface Instrumentation { //增加一個Class 文件的轉(zhuǎn)換器,轉(zhuǎn)換器用于改變 Class 二進制流的數(shù)據(jù),參數(shù) canRetransform 設(shè)置是否允許重新轉(zhuǎn)換。 void addTransformer(ClassFileTransformer transformer, boolean canRetransform); //在類加載之前,重新定義 Class 文件,ClassDefinition 表示對一個類新的定義,如果在類加載之后,需要使用 retransformClasses 方法重新定義。addTransformer方法配置之后,后續(xù)的類加載都會被Transformer攔截。對于已經(jīng)加載過的類,可以執(zhí)行retransformClasses來重新觸發(fā)這個Transformer的攔截。類加載的字節(jié)碼被修改后,除非再次被retransform,否則不會恢復(fù)。 void addTransformer(ClassFileTransformer transformer); //刪除一個類轉(zhuǎn)換器 boolean removeTransformer(ClassFileTransformer transformer); boolean isRetransformClassesSupported(); //在類加載之后,重新定義 Class。這個很重要,該方法是1.6 之后加入的,事實上,該方法是 update 了一個類。 void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; boolean isRedefineClassesSupported(); void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException; boolean isModifiableClass(Class<?> theClass); @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); @SuppressWarnings("rawtypes") Class[] getInitiatedClasses(ClassLoader loader); //獲取一個對象的大小 long getObjectSize(Object objectToSize); void appendToBootstrapClassLoaderSearch(JarFile jarfile); void appendToSystemClassLoaderSearch(JarFile jarfile); boolean isNativeMethodPrefixSupported(); void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix); }
最為重要的是上面注釋的幾個方法,下面我們會用到。
如何使用javaagent?
使用 javaagent 需要幾個步驟:
在執(zhí)行以上步驟后,JVM 會先執(zhí)行 premain 方法,大部分類加載都會通過該方法,注意:是大部分,不是所有。當然,遺漏的主要是系統(tǒng)類,因為很多系統(tǒng)類先于 agent 執(zhí)行,而用戶類的加載肯定是會被攔截的。也就是說,這個方法是在 main 方法啟動前攔截大部分類的加載活動,既然可以攔截類的加載,那么就可以去做重寫類這樣的操作,結(jié)合第三方的字節(jié)碼編譯工具,比如ASM,javassist,cglib等等來改寫實現(xiàn)類。
通過上面的步驟我們用代碼實現(xiàn)來實現(xiàn)。實現(xiàn) javaagent 你需要搭建兩個工程,一個工程是用來承載 javaagent類,單獨的打成jar包;一個工程是javaagent需要去代理的類。即javaagent會在這個工程中的main方法啟動之前去做一些事情。
1.首先來實現(xiàn)javaagent工程。
工程目錄結(jié)構(gòu)如下:
-java-agent
----src
--------main
--------|------java
--------|----------com.rickiyang.learn
--------|------------PreMainTraceAgent
--------|resources
-----------META-INF
--------------MANIFEST.MF
第一步是需要創(chuàng)建一個類,包含premain 方法:
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; /** * @author: rickiyang * @date: 2019/8/12 * @description: */ public class PreMainTraceAgent { public static void premain(String agentArgs, Instrumentation inst) { System.out.println("agentArgs : " + agentArgs); 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 { System.out.println("premain load Class:" + className); return classfileBuffer; } } }
上面就是我實現(xiàn)的一個類,實現(xiàn)了帶Instrumentation參數(shù)的premain()方法。調(diào)用addTransformer()方法對啟動時所有的類進行攔截。
然后在 resources 目錄下新建目錄:META-INF,在該目錄下新建文件:MANIFREST.MF:
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: PreMainTraceAgent
注意到第5行有空行。
說一下MANIFREST.MF文件的作用,這里如果你不去手動指定的話,直接 打包,默認會在打包的文件中生成一個MANIFREST.MF文件:
Manifest-Version: 1.0
Implementation-Title: test-agent
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: yangyue
Implementation-Vendor-Id: com.rickiyang.learn
Spring-Boot-Version: 2.0.9.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.rickiyang.learn.LearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.2
Build-Jdk: 1.8.0_151
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/test-agent
這是默認的文件,包含當前的一些版本信息,當前工程的啟動類,它還有別的參數(shù)允許你做更多的事情,可以用上的有:
即在該文件中主要定義了程序運行相關(guān)的配置信息,程序運行前會先檢測該文件中的配置項。
一個java程序中-javaagent參數(shù)的個數(shù)是沒有限制的,所以可以添加任意多個javaagent。所有的java agent會按照你定義的順序執(zhí)行,例如:
java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar
程序執(zhí)行的順序?qū)牵?/p>
MyAgent1.premain -> MyAgent2.premain -> MyProgram.main
說回上面的 javaagent工程,接下來將該工程打成jar包,我在打包的時候發(fā)現(xiàn)打完包之后的 MANIFREST.MF文件被默認配置替換掉了。所以我是手動將上面我的配置文件替換到j(luò)ar包中的文件,這里你需要注意。
另外的再說一種不去手動寫MANIFREST.MF文件的方式,使用maven插件:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.0</version> <configuration> <archive> <!--自動添加META-INF/MANIFEST.MF --> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class>com.rickiyang.learn.PreMainTraceAgent</Premain-Class> <Agent-Class>com.rickiyang.learn.PreMainTraceAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin>
用這種插件的方式也可以自動生成該文件。
agent代碼就寫完了,下面再重新開一個工程,你只需要寫一個帶 main 方法的類即可:
public class TestMain { public static void main(String[] args) { System.out.println("main start"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main end"); } }
很簡單,然后需要做的就是將上面的 代理類 和 這個測試類關(guān)聯(lián)起來。有兩種方式:
如果你用的是idea,那么你可以點擊菜單: run-debug configuration,然后將你的代理類包 指定在 啟動參數(shù)中即可:
另一種方式是不用 編譯器,采用命令行的方法。與上面大致相同,將 上面的測試類編譯成 class文件,然后 運行該類即可:
#將該類編譯成class文件 > javac TestMain.java #指定agent程序并運行該類 > java -javaagent:c:/alg.jar TestMain
使用上面兩種方式都可以運行,輸出結(jié)果如下:
D:\soft\jdk1.8\bin\java.exe -javaagent:c:/alg.jar "-javaagent:D:\soft\IntelliJ IDEA 2019.1.1\lib\idea_rt.jar=54274:D:\soft\IntelliJ IDEA 2019.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\soft\jdk1.8\jre\lib\charsets.jar;D:\soft\jdk1.8\jre\lib\deploy.jar;D:\soft\jdk1.8\jre\lib\ext\access-bridge-64.jar;D:\soft\jdk1.8\jre\lib\ext\cldrdata.jar;D:\soft\jdk1.8\jre\lib\ext\dnsns.jar;D:\soft\jdk1.8\jre\lib\ext\jaccess.jar;D:\soft\jdk1.8\jre\lib\ext\jfxrt.jar;D:\soft\jdk1.8\jre\lib\ext\localedata.jar;D:\soft\jdk1.8\jre\lib\ext\nashorn.jar;D:\soft\jdk1.8\jre\lib\ext\sunec.jar;D:\soft\jdk1.8\jre\lib\ext\sunjce_provider.jar;D:\soft\jdk1.8\jre\lib\ext\sunmscapi.jar;D:\soft\jdk1.8\jre\lib\ext\sunpkcs11.jar;D:\soft\jdk1.8\jre\lib\ext\zipfs.jar;D:\soft\jdk1.8\jre\lib\javaws.jar;D:\soft\jdk1.8\jre\lib\jce.jar;D:\soft\jdk1.8\jre\lib\jfr.jar;D:\soft\jdk1.8\jre\lib\jfxswt.jar;D:\soft\jdk1.8\jre\lib\jsse.jar;D:\soft\jdk1.8\jre\lib\management-agent.jar;D:\soft\jdk1.8\jre\lib\plugin.jar;D:\soft\jdk1.8\jre\lib\resources.jar;D:\soft\jdk1.8\jre\lib\rt.jar;D:\workspace\demo1\target\classes;E:\.m2\repository\org\springframework\boot\spring-boot-starter-aop\2.1.1.RELEASE\spring-
...
...
...
1.8.11.jar;E:\.m2\repository\com\google\guava\guava\20.0\guava-20.0.jar;E:\.m2\repository\org\apache\commons\commons-lang3\3.7\commons-lang3-3.7.jar;E:\.m2\repository\com\alibaba\fastjson\1.2.54\fastjson-1.2.54.jar;E:\.m2\repository\org\springframework\boot\spring-boot\2.1.0.RELEASE\spring-boot-2.1.0.RELEASE.jar;E:\.m2\repository\org\springframework\spring-context\5.1.3.RELEASE\spring-context-5.1.3.RELEASE.jar com.springboot.example.demo.service.TestMain
agentArgs : null
premain load Class :java/util/concurrent/ConcurrentHashMap$ForwardingNode
premain load Class :sun/nio/cs/ThreadLocalCoders
premain load Class :sun/nio/cs/ThreadLocalCoders$1
premain load Class :sun/nio/cs/ThreadLocalCoders$Cache
premain load Class :sun/nio/cs/ThreadLocalCoders$2
premain load Class :java/util/jar/Attributes
premain load Class :java/util/jar/Manifest$FastInputStream
...
...
...
premain load Class :java/lang/Class$MethodArray
premain load Class :java/lang/Void
main start
premain load Class :sun/misc/VMSupport
premain load Class :java/util/Hashtable$KeySet
premain load Class :sun/nio/cs/ISO_8859_1$Encoder
premain load Class :sun/nio/cs/Surrogate$Parser
premain load Class :sun/nio/cs/Surrogate
...
...
...
premain load Class :sun/util/locale/provider/LocaleResources$ResourceReference
main end
premain load Class :java/lang/Shutdown
premain load Class :java/lang/Shutdown$LockProcess finished with exit code 0
上面的輸出結(jié)果我們能夠發(fā)現(xiàn):
下面是使用javassist來動態(tài)將某個方法替換掉:
package com.rickiyang.learn; import javassist.*; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; /** * @author rickiyang * @date 2019-08-06 * @Desc */ public class MyClassTransformer implements ClassFileTransformer { @Override public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined,final ProtectionDomain protectionDomain, final byte[] classfileBuffer) { // 操作Date類 if ("java/util/Date".equals(className)) { try { // 從ClassPool獲得CtClass對象 final ClassPool classPool = ClassPool.getDefault(); final CtClass clazz = classPool.get("java.util.Date"); CtMethod convertToAbbr = clazz.getDeclaredMethod("convertToAbbr"); //這里對 java.util.Date.convertToAbbr() 方法進行了改寫,在 return之前增加了一個 打印操作 String methodBody = "{sb.append(Character.toUpperCase(name.charAt(0)));" + "sb.append(name.charAt(1)).append(name.charAt(2));" + "System.out.println(\"sb.toString()\");" + "return sb;}"; convertToAbbr.setBody(methodBody); // 返回字節(jié)碼,并且detachCtClass對象 byte[] byteCode = clazz.toBytecode(); //detach的意思是將內(nèi)存中曾經(jīng)被javassist加載過的Date對象移除,如果下次有需要在內(nèi)存中找不到會重新走javassist加載 clazz.detach(); return byteCode; } catch (Exception ex) { ex.printStackTrace(); } } // 如果返回null則字節(jié)碼不會被修改 return null; } }
JVM啟動后動態(tài)Instrument
上面介紹的Instrumentation是在 JDK 1.5中提供的,開發(fā)者只能在main加載之前添加手腳,在 Java SE 6 的 Instrumentation 當中,提供了一個新的代理操作方法:agentmain,可以在 main 函數(shù)開始運行之后再運行。
跟premain函數(shù)一樣, 開發(fā)者可以編寫一個含有agentmain函數(shù)的 Java 類:
//采用attach機制,被代理的目標程序VM有可能很早之前已經(jīng)啟動,當然其所有類已經(jīng)被加載完成,這個時候需要借助Instrumentation#retransformClasses(Class<?>... classes)讓對應(yīng)的類可以重新轉(zhuǎn)換,從而激活重新轉(zhuǎn)換的類執(zhí)行ClassFileTransformer列表中的回調(diào) public static void agentmain (String agentArgs, Instrumentation inst) public static void agentmain (String agentArgs)
同樣,agentmain 方法中帶Instrumentation參數(shù)的方法也比不帶優(yōu)先級更高。開發(fā)者必須在 manifest 文件里面設(shè)置“Agent-Class”來指定包含 agentmain 函數(shù)的類。
在Java6 以后實現(xiàn)啟動后加載的新實現(xiàn)是Attach api。Attach API 很簡單,只有 2 個主要的類,都在 com.sun.tools.attach 包里面:
attach實現(xiàn)動態(tài)注入的原理如下:
通過VirtualMachine類的attach(pid)方法,便可以attach到一個運行中的java進程上,之后便可以通過loadAgent(agentJarPath)來將agent的jar包注入到對應(yīng)的進程,然后對應(yīng)的進程會調(diào)用agentmain方法。
既然是兩個進程之間通信那肯定的建立起連接,VirtualMachine.attach動作類似TCP創(chuàng)建連接的三次握手,目的就是搭建attach通信的連接。而后面執(zhí)行的操作,例如vm.loadAgent,其實就是向這個socket寫入數(shù)據(jù)流,接收方target VM會針對不同的傳入數(shù)據(jù)來做不同的處理。
我們來測試一下agentmain的使用:
工程結(jié)構(gòu)和 上面premain的測試一樣,編寫AgentMainTest,然后使用maven插件打包 生成MANIFEST.MF。
package com.rickiyang.learn; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; /** * @author rickiyang * @date 2019-08-16 * @Desc */ public class AgentMainTest { public static void agentmain(String agentArgs, Instrumentation instrumentation) { instrumentation.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 { System.out.println("premain load Class:" + className); return classfileBuffer; } } }
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.0</version> <configuration> <archive> <!--自動添加META-INF/MANIFEST.MF --> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Agent-Class>com.rickiyang.learn.AgentMainTest</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin>
將agent打包之后,就是編寫測試main方法。上面我們畫的圖中的步驟是:從一個attach JVM去探測目標JVM,如果目標JVM存在則向它發(fā)送agent.jar。我測試寫的簡單了些,找到當前JVM并加載agent.jar。
package com.rickiyang.learn.job; import com.sun.tools.attach.*; import java.io.IOException; import java.util.List; /** * @author rickiyang * @date 2019-08-16 * @Desc */ public class TestAgentMain { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { //獲取當前系統(tǒng)中所有 運行中的 虛擬機 System.out.println("running JVM start "); List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { //如果虛擬機的名稱為 xxx 則 該虛擬機為目標虛擬機,獲取該虛擬機的 pid //然后加載 agent.jar 發(fā)送給該虛擬機 System.out.println(vmd.displayName()); if (vmd.displayName().endsWith("com.rickiyang.learn.job.TestAgentMain")) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("/Users/yangyue/Documents/java-agent.jar"); virtualMachine.detach(); } } } }
list()方法會去尋找當前系統(tǒng)中所有運行著的JVM進程,你可以打印vmd.displayName()看到當前系統(tǒng)都有哪些JVM進程在運行。因為main函數(shù)執(zhí)行起來的時候進程名為當前類名,所以通過這種方式可以去找到當前的進程id。
注意:在mac上安裝了的jdk是能直接找到 VirtualMachine 類的,但是在windows中安裝的jdk無法找到,如果你遇到這種情況,請手動將你jdk安裝目錄下:lib目錄中的tools.jar添加進當前工程的Libraries中。
運行main方法的輸出為:
可以看到實際上是啟動了一個socket進程去傳輸agent.jar。先打印了“running JVM start”表名main方法是先啟動了,然后才進入代理類的transform方法。
instrument原理
instrument的底層實現(xiàn)依賴于JVMTI(JVM Tool Interface),它是JVM暴露出來的一些供用戶擴展的接口集合,JVMTI是基于事件驅(qū)動的,JVM每執(zhí)行到一定的邏輯就會調(diào)用一些事件的回調(diào)接口(如果有的話),這些接口可以供開發(fā)者去擴展自己的邏輯。JVMTIAgent是一個利用JVMTI暴露出來的接口提供了代理啟動時加載(agent on load)、代理通過attach形式加載(agent on attach)和代理卸載(agent on unload)功能的動態(tài)庫。而instrument agent可以理解為一類JVMTIAgent動態(tài)庫,別名是JPLISAgent(Java Programming Language Instrumentation Services Agent),也就是專門為java語言編寫的插樁服務(wù)提供支持的代理。
啟動時加載instrument agent過程:
1.創(chuàng)建并初始化 JPLISAgent;
2.監(jiān)聽 VMInit 事件,在 JVM 初始化完成之后做下面的事情:
3.解析 javaagent 中 MANIFEST.MF 文件的參數(shù),并根據(jù)這些參數(shù)來設(shè)置 JPLISAgent 里的一些內(nèi)容。
運行時加載instrument agent過程:
通過 JVM 的attach機制來請求目標 JVM 加載對應(yīng)的agent,過程大致如下:
1.創(chuàng)建并初始化JPLISAgent;
2.解析 javaagent 里 MANIFEST.MF 里的參數(shù);
3.創(chuàng)建 InstrumentationImpl 對象;
4.監(jiān)聽 ClassFileLoadHook 事件;
5.調(diào)用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在這個方法里會去調(diào)用javaagent里 MANIFEST.MF 里指定的Agent-Class類的agentmain方法。
Instrumentation的局限性
大多數(shù)情況下,我們使用Instrumentation都是使用其字節(jié)碼插樁的功能,或者籠統(tǒng)說就是類重定義(Class Redefine)的功能,但是有以下的局限性:
1.premain和agentmain兩種方式修改字節(jié)碼的時機都是類文件加載之后,也就是說必須要帶有Class類型的參數(shù),不能通過字節(jié)碼文件和自定義的類名重新定義一個本來不存在的類。
2.類的字節(jié)碼修改稱為類轉(zhuǎn)換(Class Transform),類轉(zhuǎn)換其實最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有以下限制:
除了上面的方式,如果想要重新定義一個類,可以考慮基于類加載器隔離的方式:創(chuàng)建一個新的自定義類加載器去通過新的字節(jié)碼去定義一個全新的類,不過也存在只能通過反射調(diào)用該全新類的局限性。
以上是關(guān)于java agent的使用方法的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對大家有幫助,更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。