java agent探究

目前我們的業(yè)務(wù)遇到線上問題時經(jīng)常需要加log調(diào)試。從加一行l(wèi)og到push代碼再到j(luò)ekens編譯、打包、最后再部署,這個過程時間消耗非常長,而且各個環(huán)節(jié)都可能出現(xiàn)其他因素干擾(如如多人同時提交導(dǎo)致代碼沖突編譯不過等),造成的時間消耗就更長了。甚至有時候需要逐步加日志排查問題,重復(fù)很多次改代碼再打包部署的操作,實在是費(fèi)心費(fèi)力。。。
可不可以在服務(wù)器上直接改代碼使之實時生效?結(jié)論是可以的。

一、java Instrumentation

從java5開始,jdk中新增了一個java.lang.instrument.Instrumentation 類,它提供在運(yùn)行時重新加載某個類的的class文件的api。下面是它的一些主要api

public interface Instrumentation {
/**
     * 加入一個轉(zhuǎn)換器Transformer,之后的所有的類加載都會被Transformer攔截。
     * ClassFileTransformer類是一個接口,使用時需要實現(xiàn)它,該類只有一個方法,該方法傳遞類的信息,返回值是轉(zhuǎn)換后的類的字節(jié)碼文件。
     */
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);    

 /**
     * 對JVM已經(jīng)加載的類重新觸發(fā)類加載。使用的就是上面注冊的Transformer。
     * 該方法可以修改方法體、常量池和屬性值,但不能新增、刪除、重命名屬性或方法,也不能修改方法的簽名
     */
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    
/**
   *此方法用于替換類的定義,而不引用現(xiàn)有的類文件字節(jié),就像從源代碼重新編譯以進(jìn)行修復(fù)和繼續(xù)調(diào)試時所做的那樣。
   *在要轉(zhuǎn)換現(xiàn)有類文件字節(jié)的地方(例如在字節(jié)碼插裝中),應(yīng)該使用retransformClasses。
   *該方法可以修改方法體、常量池和屬性值,但不能新增、刪除、重命名屬性或方法,也不能修改方法的簽名
   */
    void redefineClasses(ClassDefinition... definitions)throws  ClassNotFoundException, UnmodifiableClassException;

    /**
     * 獲取一個對象的大小
     */
    long getObjectSize(Object objectToSize);
    
    /**
     * 將一個jar加入到bootstrap classloader的 classpath里
     */
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);
    
    /**
     * 獲取當(dāng)前被JVM加載的所有類對象
     */
    Class[] getAllLoadedClasses();
}

通過addTransformer可以加入一個轉(zhuǎn)換器,轉(zhuǎn)換器可以實現(xiàn)對類加載的事件進(jìn)行攔截并返回轉(zhuǎn)換后新的字節(jié)碼,通過redefineClasses或retransformClasses都可以觸發(fā)類的重新加載事件。通過這幾個方法的組合,就可以實現(xiàn)文章開頭提到的不修改代碼使之實時生效的目的了。

二、JAVA Agent

通過操作Instrumentation的api就可以實現(xiàn)不重啟服務(wù)對單個類進(jìn)行簡單的修改。Instrumentation是一個interface,它的實現(xiàn)類InstrumentationImpl只有一個private的構(gòu)造方法。
怎么拿到這個對象呢?下面是Instrumentation類的一段注釋說明:



有兩種方式拿到Instrumentation對象:
在jvm啟動時指定agent,Instrumentation對象會通過agent的premain方法傳遞。
在jvm啟動后通過jvm提供的機(jī)制加載agent,Instrumentation對象會通過agent的agentmain方法傳遞。

三、實踐java啟動時加載agent 獲取Instrumentation對象

編寫agent類并編譯成.class文件,之后把它打成jar包,然后在jvm啟動參數(shù)中指定jar包位置,具體操作步驟:
1、創(chuàng)建一個agent類,并創(chuàng)建premain方法,premain方法的參數(shù)是固定的。

public class preMainAgentClz {
    private static Instrumentation instrumentation;
    public static void premain(String agentArgs, Instrumentation inst) {
        instrumentation = inst;
        System.err.println("com.hexuan.agent.demo1.preMainAgentClz 我在main啟動之前啟動");
    }
}

2、指定premain方法的位置(兩種指定方式,設(shè)置一種就行)
方式1)創(chuàng)建并編輯 resources/META-INF/MANIFEST.MF 文件,當(dāng)打jar包時將該文件一并打包

Premain-Class: com.hexuan.agent.demo1.preMainAgentClz #premain方法所在類的位置

方式2)如果是maven項目,在pom.xml加入

 <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>com.hexuan.agent.demo1.preMainAgentClz</Premain-Class>
                            <Agent-Class>com.hexuan.agent.demo1.agentMainAgentClz</Agent-Class>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>

3、如果是在pom中配置的,直接maven package就好了。如果是MANIFEST.MF文件指定的方式,將包含premain的類編譯成class文件,并和MANIFEST.MF一起文件打包jar。
4、啟動時指定agent位置,在jvm啟動參數(shù)中加入-javaagent參數(shù)并指定jar文件位置。

-javaagent:/Users/hexuan/IdeaProjects/acfun_WorkSpace/java-agent-demo/target/java-agent-demo-1.0-SNAPSHOT.jar

5、啟動java,agent的premain方法會在main方法之前執(zhí)行。

四、在java啟動后以attach的方式加載agent

上文介紹了java進(jìn)程啟動時加載agent的方式和步驟,通過它在啟動之前將指定的類進(jìn)行替換。但如果要實現(xiàn)文章開頭提到的調(diào)試線上代碼,我們需要在修改了class文件后重啟jvm并且設(shè)置-javaagent參數(shù),顯然這種方式不是我們最想要的。上文提到過我們可以在jvm啟動后通過jvm提供的機(jī)制加載agent,也就是說我們能夠在任何時候去加載agent,然后替換類文件。這個機(jī)制就是jdk的attach api。

Attach API是Sun公司提供的一套擴(kuò)展API,用來向目標(biāo)JVM"附著"(Attach)代理工具程序的。有了它,開發(fā)者可以方便的監(jiān)控一個JVM,運(yùn)行一個外加的代理程序,Sun JVM Attach API功能上非常簡單,僅提供了如下幾個功能:

  1. 列出當(dāng)前所有的JVM實例描述
  2. Attach到其中一個JVM上,建立通信管道
  3. 讓目標(biāo)JVM加載Agent

Attach Api 對應(yīng)的代碼位置在 com.sun.tools.attach 包,包里邊有一個類VirtualMachine,它有兩個比較重要方法:

/**
  *傳遞一個進(jìn)程號作為參數(shù),返回目標(biāo)jvm進(jìn)程的vm對象。
  *該方法其實是JVM進(jìn)程之間指令傳遞的橋梁,底層通過socket進(jìn)行通信。
  *JVM A可以發(fā)送一些指令給JVM B,B收到指令之后,可以執(zhí)行對應(yīng)的邏輯
  * 比如在命令行中經(jīng)常使用的jstack、jcmd、jps等,很多都是基于這種機(jī)制實現(xiàn)的
  **/
public static VirtualMachine attach(String var0) throws AttachNotSupportedException, IOException 

/**
  *該方法允許我們將agent對應(yīng)的jar文件地址作為參數(shù)傳遞目標(biāo)jvm
  *目標(biāo)jvm收到該命令后會加載這個agent
  **/
public void loadAgent(String var1) throws AgentLoadException, AgentInitializationException, IOException

顯然,我們可以創(chuàng)建一個java進(jìn)程,用它attach到對應(yīng)的jvm,并加載agent,agent加載后我們的類也就被成功替換了。

五、怎么得到新的類文件

Instrumentation操作的是.class文件,對于我們開發(fā)人員來講,我們看不懂.class文件,更無法直接修改它了。還是考慮文章一開始提到的線上改代碼調(diào)試的場景,我們知道了如何去替換類,但是如何得到新的.class類文件呢?
方式1:線下修改.java文件 -->編譯成.class文件 -->上傳到線上機(jī)器-->instrument
方式2:線上.class舊文件 -->反編譯成.java文件 -->修改java文件 -->編譯成.class文件 -->instrument
方式3:通過ASM或其他操作字節(jié)碼的組件直接修改.class文件-->instrument
...
無論哪種方式,流程太復(fù)雜容易出錯,有成熟的組件嗎?有,Arthas和Btrace

六、Arthas&Btrace

BTrace 是基于動態(tài)字節(jié)碼修改技術(shù)(Instrumentation)來實現(xiàn)運(yùn)行時 java 程序的跟蹤和替換。大體的原理可以用下面的公式描述:Client(Java compile api + attach api) + Agent(腳本解析引擎 + ASM + JDK6 Instumentation) + Socket其實 BTrace 就是使用了 java attach api 附加 agent.jar ,然后使用腳本解析引擎+asm來重寫指定類的字節(jié)碼,再使用 instrument 實現(xiàn)對原有類的替換。
但是BTrace腳本在使用上有一定的學(xué)習(xí)成本,如果能把一些常用的功能封裝起來,對外直接提供簡單的命令即可操作的話,那就再好不過了。2018年9月份阿里開源了自己的Java診斷工具Arthas。Arthas功能非常強(qiáng)大,通過簡單的命令行操作即可完成對應(yīng)功能。究其背后的技術(shù)原理,和本文中提到的大致無二。

Btrace開源地址:https://github.com/btraceio/btrace
Arthas開源地址:https://github.com/alibaba/arthas

七、總結(jié)

java instrument在很多應(yīng)用領(lǐng)域都發(fā)揮著重要的作用,比如:

  • apm:(Application Performance Management)應(yīng)用性能管理。pinpoint、cat、skywalking等都基于Instrumentation實現(xiàn)
  • idea的HotSwap、Jrebel等熱部署工具
  • 應(yīng)用級故障演練
  • Java診斷工具Arthas、Btrace等

java agent加載的時序圖:

image

附1:java Instrumentation的redefineClasses 和retransformClasses 的補(bǔ)充說明:

  • 二者的區(qū)別:都是替換已經(jīng)存在的class文件,redefineClasses是自己提供字節(jié)碼文件替換掉已存在的class文件,retransformClasses是在已存在的字節(jié)碼文件上修改后再替換之。
  • 相互依賴的類加載: 允許傳類集合,以滿足類之間相互依賴的情況,加載順序為集合順序
  • 替換后生效時機(jī):如果一個被修改的方法已經(jīng)在棧楨中存在,則棧楨中的會使用舊字節(jié)碼定義的方法繼續(xù)運(yùn)行,新字節(jié)碼會在新棧楨中執(zhí)行
  • 不修改變量值:該方法不會導(dǎo)致類的一些初始化方法執(zhí)行、不會修改靜態(tài)變量的值
  • 只改變方法體:該方法可以改變類的方法體、常量池和屬性值,但不能新增、刪除、重命名屬性或方法,也不能修改方法的簽名
  • 字節(jié)碼有問題時不加載:在類轉(zhuǎn)化前該方法不會check字節(jié)碼文件,如果結(jié)果字節(jié)碼出錯了,該方法將拋出異常。如果該方法拋出異常,則不會重新定義任何類

附:2、使用Arthas實現(xiàn)加log調(diào)試

#下載arthas agent
wget https://alibaba.github.io/arthas/arthas-boot.jar

#啟動agent
java -jar arthas-boot.jar --target-ip 0.0.0.0

#sc:search class 查找類文件
sc *SelectionController

#jad 反編譯class 并輸出到文件
jad --source-only com.acfun.controller.SelectionController > /tmp/SelectionController.java

#修改源代碼
vi /tmp/SelectionController.java

#sc查找加載UserController的ClassLoader  -d參數(shù)可以打印出類加載的具體信息
sc -d *SelectionController |grep classLoaderHash 

#編譯源代碼 使用mc(Memory Compiler)命令來編譯,并且通過-c參數(shù)指定ClassLoader
mc -c 3787f831 /tmp/SelectionController.java -d /tmp

#使用redefine命令重新加載新編譯好的class
redefine /tmp/com/acfun/controller/SelectionController.class

#redefine成功之后,訪問controller,觀察代碼是否生效
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容