javaagent使用指南

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

JVM啓動前靜態Instrument

Javaagent 是什麼?spring

Javaagent是java命令的一個參數。參數 javaagent 能夠用於指定一個 jar 包,而且對該 java 包有2個要求:shell

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

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

在命令行輸入 java能夠看到相應的參數,其中有 和 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 中定義的一個包,該路徑下有兩個重要的類:json

該包提供了一些工具幫助開發人員在 Java 程序運行時,動態修改系統中的 Class 類型。其中,使用該軟件包的一個關鍵組件就是 Javaagent。從名字上看,彷佛是個 Java 代理之類的,而實際上,他的功能更像是一個Class 類型的轉換器,他能夠在運行時接受從新外部請求,對Class類型進行修改。windows

從本質上講,Java Agent 是一個遵循一組嚴格約定的常規 Java 類。 上面說到 javaagent命令要求指定的類中必需要有premain()方法,而且對premain方法的簽名也有要求,簽名必須知足如下兩種格式:api

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

JVM 會優先加載 帶 Instrumentation 簽名的方法,加載成功忽略第二種,若是第一種沒有,則加載第二種方法。這個邏輯在sun.instrument.InstrumentationImpl 類中:springboot

Instrumentation 類 定義以下:app

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

最爲重要的是上面註釋的幾個方法,下面咱們會用到。

如何使用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包路徑 啓動要代理的方法。

在執行以上步驟後,JVM 會先執行 premain 方法,大部分類加載都會經過該方法,注意:是大部分,不是全部。固然,遺漏的主要是系統類,由於不少系統類先於 agent 執行,而用戶類的加載確定是會被攔截的。也就是說,這個方法是在 main 方法啓動前攔截大部分類的加載活動,既然能夠攔截類的加載,那麼就能夠去作重寫類這樣的操做,結合第三方的字節碼編譯工具,好比ASM,javassist,cglib等等來改寫實現類。

經過上面的步驟咱們用代碼實現來實現。實現 javaagent 你須要搭建兩個工程,一個工程是用來承載 javaagent類,單獨的打成jar包;一個工程是javaagent須要去代理的類。即javaagent會在這個工程中的main方法啓動以前去作一些事情。

1.首先來實現javaagent工程。

工程目錄結構以下:

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

第一步是須要建立一個類,包含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;
        }
    }
}

上面就是我實現的一個類,實現了帶Instrumentation參數的premain()方法。調用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

這是默認的文件,包含當前的一些版本信息,當前工程的啓動類,它還有別的參數容許你作更多的事情,能夠用上的有:

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

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

Boot-Class-Path :設置引導類加載器搜索的路徑列表。查找類的特定於平臺的機制失敗後,引導類加載器會搜索這些路徑。按列出的順序搜索路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑組件語法。若是該路徑以斜槓字符(「/」)開頭,則爲絕對路徑,不然爲相對路徑。相對路徑根據代理 JAR 文件的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。若是代理是在 VM 啓動以後某一時刻啓動的,則忽略不表示 JAR 文件的路徑。(可選)

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

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

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

即在該文件中主要定義了程序運行相關的配置信息,程序運行前會先檢測該文件中的配置項。

一個java程序中-javaagent參數的個數是沒有限制的,因此能夠添加任意多個javaagent。全部的java agent會按照你定義的順序執行,例如:

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

程序執行的順序將會是:

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

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

另外的再說一種不去手動寫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");
    }
}

很簡單,而後須要作的就是將上面的 代理類 和 這個測試類關聯起來。有兩種方式:

若是你用的是idea,那麼你能夠點擊菜單: run-debug configuration,而後將你的代理類包 指定在 啓動參數中便可:

另外一種方式是不用 編譯器,採用命令行的方法。與上面大體相同,將 上面的測試類編譯成 class文件,而後 運行該類便可:

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

使用上面兩種方式均可以運行,輸出結果以下:

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

上面的輸出結果咱們可以發現:

  1. 執行main方法以前會加載全部的類,包括系統類和自定義類;
  2. 在ClassFileTransformer中會去攔截系統類和本身實現的類對象;
  3. 若是你有對某些類對象進行改寫,那麼在攔截的時候抓住該類使用字節碼編譯工具便可實現。

下面是使用javassist來動態將某個方法替換掉:

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

上面介紹的Instrumentation是在 JDK 1.5中提供的,開發者只能在main加載以前添加手腳,在 Java SE 6 的 Instrumentation 當中,提供了一個新的代理操做方法:agentmain,能夠在 main 函數開始運行以後再運行。

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 方法中帶Instrumentation參數的方法也比不帶優先級更高。開發者必須在 manifest 文件裏面設置「Agent-Class」來指定包含 agentmain 函數的類。

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

  1. VirtualMachine 字面意義表示一個Java 虛擬機,也就是程序須要監控的目標虛擬機,提供了獲取系統信息(好比獲取內存dump、線程dump,類信息統計(好比已加載的類以及實例個數等), loadAgent,Attach 和 Detach (Attach 動做的相反行爲,從 JVM 上面解除一個代理)等方法,能夠實現的功能能夠說很是之強大 。該類容許咱們經過給attach方法傳入一個jvm的pid(進程id),遠程鏈接到jvm上 。

    代理類注入操做只是它衆多功能中的一個,經過loadAgent方法向jvm註冊一個代理程序agent,在該agent的代理程序中會獲得一個Instrumentation實例,該實例能夠 在class加載前改變class的字節碼,也能夠在class加載後從新加載。在調用Instrumentation實例的方法時,這些方法會使用ClassFileTransformer接口中提供的方法進行處理。

  2. VirtualMachineDescriptor 則是一個描述虛擬機的容器類,配合 VirtualMachine 類完成各類功能。

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

經過VirtualMachine類的attach(pid)方法,即可以attach到一個運行中的java進程上,以後即可以經過loadAgent(agentJarPath)來將agent的jar包注入到對應的進程,而後對應的進程會調用agentmain方法。

既然是兩個進程之間通訊那確定的創建起鏈接,VirtualMachine.attach動做相似TCP建立鏈接的三次握手,目的就是搭建attach通訊的鏈接。然後面執行的操做,例如vm.loadAgent,其實就是向這個socket寫入數據流,接收方target VM會針對不一樣的傳入數據來作不一樣的處理。

咱們來測試一下agentmain的使用:

工程結構和 上面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存在則向它發送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 {
        //獲取當前系統中全部 運行中的 虛擬機
        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()看到當前系統都有哪些JVM進程在運行。由於main函數執行起來的時候進程名爲當前類名,因此經過這種方式能夠去找到當前的進程id。

注意:在mac上安裝了的jdk是能直接找到 VirtualMachine 類的,可是在windows中安裝的jdk沒法找到,若是你遇到這種狀況,請手動將你jdk安裝目錄下:lib目錄中的tools.jar添加進當前工程的Libraries中。

運行main方法的輸出爲:

能夠看到其實是啓動了一個socket進程去傳輸agent.jar。先打印了「running JVM start」表名main方法是先啓動了,而後才進入代理類的transform方法。

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 初始化完成以後作下面的事情:

    1. 建立 InstrumentationImpl 對象 ;

    2. 監聽 ClassFileLoadHook 事件 ;

    3. 調用 InstrumentationImpl 的loadClassAndCallPremain方法,在這個方法裏會去調用 javaagent 中 MANIFEST.MF 裏指定的Premain-Class 類的 premain 方法 ;
  3. 解析 javaagent 中 MANIFEST.MF 文件的參數,並根據這些參數來設置 JPLISAgent 裏的一些內容。

運行時加載instrument agent過程:

經過 JVM 的attach機制來請求目標 JVM 加載對應的agent,過程大體以下:

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

Instrumentation的侷限性

大多數狀況下,咱們使用Instrumentation都是使用其字節碼插樁的功能,或者籠統說就是類重定義(Class Redefine)的功能,可是有如下的侷限性:

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

除了上面的方式,若是想要從新定義一個類,能夠考慮基於類加載器隔離的方式:建立一個新的自定義類加載器去經過新的字節碼去定義一個全新的類,不過也存在只能經過反射調用該全新類的侷限性。

相關文章
相關標籤/搜索