中文字幕精品亚洲无线码二区,国产黄a三级三级三级看三级,亚洲七七久久桃花影院,丰满少妇被猛烈进入,国产小视频在线观看网站

javaagent使用指南

今天打算寫一下 Javaagent,一開始我對它的概念也比較陌生,后來在別人口中聽到 字節碼插樁bTraceArthas后(hou)面才逐漸了(le)解到Java還提供(gong)了(le)這么個(ge)工具。

JVM啟動前靜態Instrument

Javaagent 是什(shen)么(me)?

Javaagent是java命令的一(yi)個參數。參數 javaagent 可(ke)以用于指定一(yi)個 jar 包,并且對該 java 包有2個要求:

  1. 這個 jar 包的 MANIFEST.MF 文件必須指定 Premain-Class 項。
  2. Premain-Class 指定的那個類必須實現 premain() 方法。

premain 方法,從字面上理解,就是運行在 main 函數之前的的類。當Java 虛擬機啟動時,在執行 main 函數之前,JVM 會先運行-javaagent所(suo)指定 jar 包內 Premain-Class 這個類(lei)的 premain 方法 。

在命令行輸入 java可以看到相應的參數,其(qi)中有 和 java agent相關的:

-agentlib:<libname>[=<選項>] 加載本機代理庫 <libname>, 例如 -agentlib:hprof
	另請參閱 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<選項>]
	按完整路徑名加載本機代理庫
-javaagent:<jarpath>[=<選項>]
	加載 Java 編程語言代理, 請參閱 java.lang.instrument

在上面-javaagent參數中提到了參閱 java.lang.instrument,這是在rt.jar 中定(ding)義的一個包,該路徑下有兩個重(zhong)要的類:

該包提(ti)供了一些工具幫助開(kai)發人(ren)員在 Java 程序運(yun)行(xing)(xing)時(shi),動態修改系統中的(de) Class 類型。其中,使(shi)用該軟件包的(de)一個(ge)關(guan)鍵組(zu)件就(jiu)是(shi)(shi) Javaagent。從名字上看,似(si)乎(hu)是(shi)(shi)個(ge) Java 代理之類的(de),而實際上,他的(de)功能更(geng)像(xiang)是(shi)(shi)一個(ge)Class 類型的(de)轉換器(qi),他可以在運(yun)行(xing)(xing)時(shi)接受(shou)重新外部請求,對Class類型進行(xing)(xing)修改。

從(cong)本質上講,Java Agent 是一(yi)個遵循一(yi)組嚴格(ge)(ge)約定(ding)的常規 Java 類。 上面(mian)說到 javaagent命令(ling)要求指(zhi)定(ding)的類中必須(xu)要有premain()方(fang)法,并且(qie)對premain方(fang)法的簽名也有要求,簽名必須(xu)滿足以下兩種格(ge)(ge)式:

public static void premain(String agentArgs, Instrumentation inst)
    
public static void premain(String agentArgs)

JVM 會優先加載 帶 Instrumentation 簽名的(de)方法(fa),加(jia)載(zai)成功忽略第二種(zhong)(zhong)(zhong),如果第一種(zhong)(zhong)(zhong)沒有,則加(jia)載(zai)第二種(zhong)(zhong)(zhong)方法(fa)。這(zhe)個邏輯(ji)在(zai)sun.instrument.InstrumentationImpl 類(lei)中(zhong):

Instrumentation 類 定義(yi)如下:

public interface Instrumentation {
    
    //增加一個Class 文件的轉換器,轉換器用于改變 Class 二進制流的數據,參數 canRetransform 設置是否允許重新轉換。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    //在類加載之前,重新定義 Class 文件,ClassDefinition 表示對一個類新的定義,如果在類加載之后,需要使用 retransformClasses 方法重新定義。addTransformer方法配置之后,后續的類加載都會被Transformer攔截。對于已經加載過的類,可以執行retransformClasses來重新觸發這個Transformer的攔截。類加載的字節碼被修改后,除非再次被retransform,否則不會恢復。
    void addTransformer(ClassFileTransformer transformer);

    //刪除一個類轉換器
    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);
}

最(zui)為重要的是上面(mian)注(zhu)釋的幾個(ge)方法,下面(mian)我們會(hui)用(yong)到。

如何(he)使(shi)用javaagent?

使用 javaagent 需要幾個步驟:

  1. 定義一個 MANIFEST.MF 文件,必須包含 Premain-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
  2. 創建一個Premain-Class 指定的類,類中包含 premain 方法,方法邏輯由用戶自己確定。
  3. 將 premain 的類和 MANIFEST.MF 文件打成 jar 包。
  4. 使用參數 -javaagent: jar包路徑 啟動要代理的方法。

在(zai)執行以(yi)(yi)(yi)上(shang)步驟后,JVM 會先(xian)執行 premain 方法,大(da)部(bu)分類(lei)加(jia)載(zai)都會通(tong)過該方法,注意(yi):是(shi)(shi)大(da)部(bu)分,不是(shi)(shi)所有。當(dang)然,遺(yi)漏的(de)主(zhu)要是(shi)(shi)系(xi)統類(lei),因為很多(duo)系(xi)統類(lei)先(xian)于 agent 執行,而(er)用(yong)戶(hu)類(lei)的(de)加(jia)載(zai)肯定是(shi)(shi)會被(bei)攔(lan)截(jie)的(de)。也就(jiu)是(shi)(shi)說,這(zhe)個方法是(shi)(shi)在(zai) main 方法啟動前攔(lan)截(jie)大(da)部(bu)分類(lei)的(de)加(jia)載(zai)活動,既然可以(yi)(yi)(yi)攔(lan)截(jie)類(lei)的(de)加(jia)載(zai),那么就(jiu)可以(yi)(yi)(yi)去(qu)做重寫(xie)類(lei)這(zhe)樣的(de)操作,結合(he)第三方的(de)字(zi)節(jie)碼編譯工具,比如ASM,javassist,cglib等等來改(gai)寫(xie)實現類(lei)。

通過上面的(de)步驟我們用(yong)代碼實現(xian)來(lai)(lai)實現(xian)。實現(xian) javaagent 你需要搭建兩(liang)個(ge)(ge)工程(cheng),一(yi)個(ge)(ge)工程(cheng)是用(yong)來(lai)(lai)承(cheng)載 javaagent類(lei),單獨(du)的(de)打成(cheng)jar包;一(yi)個(ge)(ge)工程(cheng)是javaagent需要去(qu)代理的(de)類(lei)。即javaagent會在這(zhe)個(ge)(ge)工程(cheng)中(zhong)的(de)main方法啟動之(zhi)前去(qu)做一(yi)些事情。

1.首先來實現javaagent工程。

工程目錄結構如下:

-java-agent
----src
--------main
--------|------java
--------|----------com.rickiyang.learn
--------|------------PreMainTraceAgent
--------|resources
-----------META-INF
--------------MANIFEST.MF

第一步(bu)是需要創建(jian)一個類(lei),包含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;
        }
    }
}

上面就(jiu)是我(wo)實現的一個類,實現了(le)帶Instrumentation參數的premain()方法(fa)(fa)。調(diao)用addTransformer()方法(fa)(fa)對(dui)啟動(dong)時所(suo)有的類進行(xing)攔(lan)截(jie)。

然后在 resources 目錄下新(xin)建(jian)目錄:META-INF,在該目錄下新(xin)建(jian)文件:MANIFREST.MF:

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: PreMainTraceAgent

注意到第5行有空行。

說一下(xia)MANIFREST.MF文(wen)件的作用,這里如果(guo)你不去手動(dong)指(zhi)定的話,直接 打包,默認會在(zai)打包的文(wen)件中生成一個(ge)MANIFREST.MF文(wen)件:

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: //projects.spring.io/spring-boot/#/spring-bo
 ot-starter-parent/test-agent

這是(shi)默認的文件,包含當前的一些版本信息,當前工程的啟動類,它還有(you)別的參數允許你做(zuo)更多的事情(qing),可以用上的有(you):

Premain-Class :包含 premain 方法的類(lei)(類(lei)的全路徑名)

Agent-Class :包含 agentmain 方(fang)法的(de)類(類的(de)全路(lu)徑名)

Boot-Class-Path :設置引(yin)(yin)導類(lei)(lei)加載(zai)(zai)器搜索(suo)的(de)(de)路(lu)徑(jing)(jing)列(lie)表(biao)。查找類(lei)(lei)的(de)(de)特(te)定于(yu)平臺(tai)的(de)(de)機(ji)制失敗后(hou),引(yin)(yin)導類(lei)(lei)加載(zai)(zai)器會搜索(suo)這些路(lu)徑(jing)(jing)。按列(lie)出(chu)的(de)(de)順序搜索(suo)路(lu)徑(jing)(jing)。列(lie)表(biao)中的(de)(de)路(lu)徑(jing)(jing)由一(yi)個(ge)或多個(ge)空格分開。路(lu)徑(jing)(jing)使用分層 URI 的(de)(de)路(lu)徑(jing)(jing)組(zu)件語法。如果該路(lu)徑(jing)(jing)以斜(xie)杠字(zi)符(fu)(“/”)開頭,則為絕對(dui)(dui)路(lu)徑(jing)(jing),否則為相對(dui)(dui)路(lu)徑(jing)(jing)。相對(dui)(dui)路(lu)徑(jing)(jing)根據代理 JAR 文(wen)件的(de)(de)絕對(dui)(dui)路(lu)徑(jing)(jing)解(jie)析。忽(hu)略格式不(bu)正確的(de)(de)路(lu)徑(jing)(jing)和不(bu)存在的(de)(de)路(lu)徑(jing)(jing)。如果代理是在 VM 啟(qi)動之后(hou)某(mou)一(yi)時刻啟(qi)動的(de)(de),則忽(hu)略不(bu)表(biao)示 JAR 文(wen)件的(de)(de)路(lu)徑(jing)(jing)。(可選)

Can-Redefine-Classes :true表示能重定義(yi)此(ci)代理所需的類,默認值(zhi)為 false(可(ke)選)

Can-Retransform-Classes :true 表(biao)示能(neng)重轉換此代理所(suo)需(xu)的類,默認值為(wei) false (可選)

Can-Set-Native-Method-Prefix: true表示能設(she)置此(ci)代理所需(xu)的本機方法前綴,默(mo)認(ren)值為 false(可選(xuan))

即(ji)在(zai)該文(wen)(wen)件中主要定(ding)義了(le)程序運行(xing)相關(guan)的(de)配(pei)置信(xin)息,程序運行(xing)前會先檢測(ce)該文(wen)(wen)件中的(de)配(pei)置項(xiang)。

一個java程序中-javaagent參數的(de)(de)個數是沒有限(xian)制的(de)(de),所以可以添(tian)加(jia)任意多個javaagent。所有的(de)(de)java agent會(hui)按照(zhao)你定(ding)義的(de)(de)順序執行,例(li)如(ru):

java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar

程序執(zhi)行的順序將會是(shi):

MyAgent1.premain -> MyAgent2.premain -> MyProgram.main

說回上面的 javaagent工程,接下來將該工程打成jar包,我在打包的時候發現打完包之后的 MANIFREST.MF文件被默認配置替換掉了。所以我是手動將上面我的配置文件替換到jar包中的文件,這里你需要注意。

另外的(de)(de)再說一種不(bu)去手動寫MANIFREST.MF文件(jian)的(de)(de)方式,使用maven插件(jian):

<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>

用(yong)這種插件的方式也可(ke)以自動生成(cheng)該文(wen)件。

agent代碼就(jiu)寫(xie)完了,下(xia)面再重新(xin)開一個工(gong)程,你只(zhi)需要寫(xie)一個帶(dai) 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");
    }
}

很簡單,然后需要做的就是將上面的 代理(li)類 和 這個測試類關聯起來。有兩(liang)種方式:

如果你用的(de)是(shi)idea,那么你可以點(dian)擊菜單(dan): run-debug configuration,然后(hou)將你的(de)代理類包 指(zhi)定在 啟動參數中即可:

另一種方(fang)式是不用 編譯器,采用命令行的(de)(de)方(fang)法。與(yu)上面大致(zhi)相同,將 上面的(de)(de)測試(shi)類(lei)編譯成(cheng) class文件,然后 運行該類(lei)即可:

 #將該類編譯成class文件
 > javac TestMain.java
 
 #指定agent程序并運行該類
 > java -javaagent:c:/alg.jar TestMain

使用上面(mian)兩種方(fang)式都可以運行,輸出(chu)結果如(ru)下:

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$Lock

Process finished with exit code 0

上面(mian)的輸出結(jie)果我們能夠發(fa)現:

  1. 執行main方法之前會加載所有的類,包括系統類和自定義類;
  2. 在ClassFileTransformer中會去攔截系統類和自己實現的類對象;
  3. 如果你有對某些類對象進行改寫,那么在攔截的時候抓住該類使用字節碼編譯工具即可實現。

下面(mian)是(shi)使用javassist來動態將某(mou)個(ge)方(fang)法替換掉:

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);

                // 返回字節碼,并且detachCtClass對象
                byte[] byteCode = clazz.toBytecode();
                //detach的意思是將內存中曾經被javassist加載過的Date對象移除,如果下次有需要在內存中找不到會重新走javassist加載
                clazz.detach();
                return byteCode;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        // 如果返回null則字節碼不會被修改
        return null;
    }
}

JVM啟動后動態Instrument

上面介紹的(de)(de)Instrumentation是在(zai) JDK 1.5中提供(gong)的(de)(de),開(kai)發(fa)者(zhe)只(zhi)能(neng)在(zai)main加(jia)載之(zhi)前添加(jia)手(shou)腳,在(zai) Java SE 6 的(de)(de) Instrumentation 當中,提供(gong)了一個新的(de)(de)代理操作(zuo)方法(fa):agentmain,可(ke)以在(zai) main 函數開(kai)始(shi)運行之(zhi)后再運行。

premain函數一樣, 開發者可以編寫一個含有agentmain函數的 Java 類:

//采用attach機制,被代理的目標程序VM有可能很早之前已經啟動,當然其所有類已經被加載完成,這個時候需要借助Instrumentation#retransformClasses(Class<?>... classes)讓對應的類可以重新轉換,從而激活重新轉換的類執行ClassFileTransformer列表中的回調
public static void agentmain (String agentArgs, Instrumentation inst)

public static void agentmain (String agentArgs)

同樣,agentmain 方法中帶(dai)Instrumentation參(can)數(shu)的方法也比(bi)不帶(dai)優先(xian)級更(geng)高。開(kai)發者必須在 manifest 文件(jian)里面設置“Agent-Class”來指定(ding)包含 agentmain 函數(shu)的類(lei)。

在Java6 以后實現啟動后加載的新實現是Attach api。Attach API 很簡單,只有 2 個主要的類,都在 com.sun.tools.attach 包里面:

  1. VirtualMachine 字面意義(yi)表(biao)示一個Java 虛擬機,也就(jiu)是(shi)程序需要(yao)監(jian)控的目(mu)標虛擬機,提供了獲取系統信(xin)息(比(bi)如(ru)獲取內存dump、線程dump,類信(xin)息統計(比(bi)如(ru)已加載的類以及實例(li)個數等), loadAgent,Attach 和 Detach (Attach 動(dong)作的相反行為,從 JVM 上面解除一個代理(li))等方法,可(ke)以實現的功(gong)能(neng)可(ke)以說非常之強(qiang)大 。該類允許我們通過給attach方法傳入一個jvm的pid(進程id),遠程連接到jvm上 。

    代理類注入操作只是它眾多功能中的一個,通過loadAgent方(fang)法向(xiang)jvm注冊一個代(dai)理程序agent,在(zai)該(gai)(gai)agent的(de)代(dai)理程序中(zhong)會得到一個Instrumentation實例,該(gai)(gai)實例可以(yi)(yi) 在(zai)class加(jia)載前改變class的(de)字節碼,也(ye)可以(yi)(yi)在(zai)class加(jia)載后重(zhong)新加(jia)載。在(zai)調用Instrumentation實例的(de)方(fang)法時,這些方(fang)法會使用ClassFileTransformer接(jie)口中(zhong)提(ti)供的(de)方(fang)法進行(xing)處理。

  2. VirtualMachineDescriptor 則是一(yi)個描述(shu)虛擬(ni)機的容(rong)器類(lei),配合(he) VirtualMachine 類(lei)完(wan)成各種功能。

attach實現動態注入的原理如下:

通過VirtualMachine類的attach(pid)方法,便可以attach到一個運行中的java進程上,之后便可以通過loadAgent(agentJarPath)來將agent的(de)jar包(bao)注入到(dao)對(dui)應的(de)進程(cheng),然后對(dui)應的(de)進程(cheng)會調用(yong)agentmain方法(fa)。

既然是兩(liang)個進程之間通信(xin)那肯定的(de)(de)(de)(de)建(jian)立起連(lian)接(jie),VirtualMachine.attach動作類似(si)TCP創建(jian)連(lian)接(jie)的(de)(de)(de)(de)三次握手,目(mu)的(de)(de)(de)(de)就是搭(da)建(jian)attach通信(xin)的(de)(de)(de)(de)連(lian)接(jie)。而后面執行的(de)(de)(de)(de)操作,例(li)如vm.loadAgent,其(qi)實就是向這個socket寫入數(shu)據流(liu),接(jie)收方target VM會(hui)針對(dui)不同的(de)(de)(de)(de)傳入數(shu)據來做不同的(de)(de)(de)(de)處理。

我們(men)來(lai)測試一下agentmain的(de)使用:

工程結構和 上(shang)面premain的測試一樣,編(bian)寫AgentMainTest,然后(hou)使用maven插(cha)件打(da)包(bao) 生(sheng)成(cheng)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打包之(zhi)后,就是編寫(xie)測試main方法。上面我(wo)們畫的(de)圖中(zhong)的(de)步驟是:從一個attach JVM去探測目標JVM,如果目標JVM存在(zai)則向它發送agent.jar。我(wo)測試寫(xie)的(de)簡單了些,找(zhao)到當前JVM并加載(zai)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 {
        //獲取當前系統中所有 運行中的 虛擬機
        System.out.println("running JVM start ");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //如果虛擬機的名稱為 xxx 則 該虛擬機為目標虛擬機,獲取該虛擬機的 pid
            //然后加載 agent.jar 發送給該虛擬機
            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()方法會去尋找當前系統中所有運行著的JVM進程,你可以打印vmd.displayName()看(kan)到當前系統(tong)都有哪些JVM進(jin)程在運行。因為(wei)(wei)main函數執行起來的(de)時候進(jin)程名為(wei)(wei)當前類名,所以(yi)通過這種方式可以(yi)去找到當前的(de)進(jin)程id。

注意:在(zai)(zai)mac上安裝了的jdk是(shi)能直接找到(dao) VirtualMachine 類的,但是(shi)在(zai)(zai)windows中(zhong)安裝的jdk無法找到(dao),如果你(ni)遇到(dao)這種情況,請手(shou)動將你(ni)jdk安裝目錄下:lib目錄中(zhong)的tools.jar添加進當前(qian)工(gong)程的Libraries中(zhong)。

運行(xing)main方法的輸出(chu)為:

可(ke)以看到(dao)實際上(shang)是(shi)啟(qi)(qi)動了一個(ge)socket進程去傳(chuan)輸agent.jar。先打印了“running JVM start”表名main方法(fa)是(shi)先啟(qi)(qi)動了,然后才(cai)進入代(dai)理(li)類的(de)transform方法(fa)。

instrument原理

instrument的底層實現依賴于JVMTI(JVM Tool Interface),它是JVM暴露出來的一些供用戶擴展的接口集合,JVMTI是基于事件驅動的,JVM每執行到一定的邏輯就會調用一些事件的回調接口(如果有的話),這些接口可以供開發者去擴展自己的邏輯。JVMTIAgent是一個利用JVMTI暴露出來的接口提供了代理啟動時加載(agent on load)、代理通過attach形式加載(agent on attach)和代理卸載(agent on unload)功能的動態庫。而instrument agent可以理解為一類JVMTIAgent動態庫,別名是JPLISAgent(Java Programming Language Instrumentation Services Agent),也就是專門為java語言編寫的插樁服務提供支持的代理

啟動時加載instrument agent過程:
  1. 創建并初始化 JPLISAgent;

  2. 監聽 VMInit 事件,在 JVM 初始化完成之(zhi)后做下面的事情:

    1. 創建 InstrumentationImpl 對象 ;

    2. 監聽 ClassFileLoadHook 事件 ;

    3. 調用 InstrumentationImpl 的loadClassAndCallPremain方(fang)法,在(zai)這個方(fang)法里(li)會去調用 javaagent 中 MANIFEST.MF 里(li)指定的Premain-Class 類的 premain 方(fang)法 ;

  3. 解析 javaagent 中 MANIFEST.MF 文件的參(can)數,并根據(ju)這些參(can)數來設置 JPLISAgent 里(li)的一些內容。

運行時加載instrument agent過程:

通過(guo) JVM 的attach機制(zhi)來請(qing)求目標(biao) JVM 加載對應(ying)的agent,過(guo)程大致如下(xia):

  1. 創建并初始化JPLISAgent;
  2. 解析 javaagent 里 MANIFEST.MF 里的參數;
  3. 創建 InstrumentationImpl 對象;
  4. 監聽 ClassFileLoadHook 事件;
  5. 調用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在這個方法里會去調用javaagent里 MANIFEST.MF 里指定的Agent-Class類的agentmain方法。

Instrumentation的局限性

大多(duo)數情況(kuang)下,我們使用(yong)Instrumentation都是使用(yong)其字節碼插樁的(de)功能(neng),或者籠統(tong)說就是類(lei)重(zhong)定義(Class Redefine)的(de)功能(neng),但是有(you)以下的(de)局限性:

  1. premain和agentmain兩種方式修改字節碼的時機都是類文件加載之后,也就是說必須要帶有Class類型的參數,不能通過字節碼文件和自定義的類名重新定義一個本來不存在的類。
  2. 類的字節碼修改稱為類轉換(Class Transform),類轉換其實最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有以下限制:
    1. 新類和老類的父類必須相同;
    2. 新類和老類實現的接口數也要相同,并且是相同的接口;
    3. 新類和老類訪問符必須一致。 新類和老類字段數和字段名要一致;
    4. 新類和老類新增或刪除的方法必須是private static/final修飾的;
    5. 可以修改方法體。

除了上面的方式(shi)(shi),如果想要(yao)重(zhong)新(xin)(xin)定義(yi)(yi)一(yi)個類(lei)(lei),可(ke)以考(kao)慮基于(yu)類(lei)(lei)加(jia)載(zai)器(qi)隔離的方式(shi)(shi):創(chuang)建一(yi)個新(xin)(xin)的自(zi)定義(yi)(yi)類(lei)(lei)加(jia)載(zai)器(qi)去通過新(xin)(xin)的字(zi)節(jie)碼去定義(yi)(yi)一(yi)個全新(xin)(xin)的類(lei)(lei),不過也存在只能(neng)通過反射(she)調用該全新(xin)(xin)類(lei)(lei)的局限性(xing)。

posted @ 2019-08-17 15:51  rickiyang  閱讀(126581)  評論(18)    收藏  舉報