動(dòng)手寫一個(gè)javaagent

子曰:小勝靠智,大勝靠德,常勝靠身體。

1 什么是javaagent

javaagent是一個(gè)JVM“插件”,一種專門精心制作的.jar文件,它能夠利用JVM提供的Instrumentation API。

1.1 概要

Java Agent由三部分組成:代理類、代理類元信息和JVM加載.jar和代理的機(jī)制,整體內(nèi)容如下圖所示:


Java Agent

1.2 javaagent的基石

java.lang.instrument為javaagent 通過(guò)修改方法字節(jié)碼的方式操作運(yùn)行在JVM上的程序提供服務(wù)。javaagent以JAR包的形式部署,JAR文件清單中的屬性指定要加載的代理類,以啟動(dòng)代理。javaagent的啟動(dòng)方式有以下幾種:

    1. 通過(guò)在命令行指定參數(shù)啟動(dòng)。
    1. JVM啟動(dòng)后啟動(dòng)。例如,提供一種工具,該工具可以依附到已運(yùn)行的應(yīng)用,并允許在已運(yùn)行的應(yīng)用內(nèi)加載代理。
    1. 與應(yīng)用一起打包為可執(zhí)行文件。

1.3 啟動(dòng) javaagent

1.3.1 命令行啟動(dòng)

命令行啟動(dòng)參數(shù)如下:

-javaagent:<jarpath>[=<options>]

<jarpath> :javaagent的路徑,比如/opt/var/Agent-1.0.0.jar。
<options> : javaagent參數(shù),參數(shù)的解析由javaagent負(fù)責(zé)。
javaagent JAR文件清單必須包含 Premain-Class屬性,屬性的值為agent class的全路徑名(包名+類名)。代理類必須實(shí)現(xiàn)premain 方法,premain方法和main方法一樣分別是代理和應(yīng)用的入口點(diǎn)。JVM初始化完成后首先調(diào)用代理的premain函數(shù),然后調(diào)用應(yīng)用的main函數(shù),premain方法必須返回后進(jìn)程才能啟動(dòng)。

premain方法簽名如下:

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

JVM首先嘗試在代理中調(diào)用簽名為1的方法,如果代理類沒有實(shí)現(xiàn)簽名為1的方法,JVM嘗試調(diào)用簽名為2的方法:

代理類可以有一個(gè)agentmain函數(shù),函數(shù)會(huì)在JVM啟動(dòng)完成之后調(diào)用。如果,使用命令行啟動(dòng)代理,agentmain方式不會(huì)被調(diào)用。

代理的所有參數(shù)被當(dāng)作一個(gè)字符串通過(guò)agentArgs變量傳遞,代理負(fù)責(zé)解析參數(shù)字符串。
如果代理因?yàn)榇眍悷o(wú)法被加載、代理類未實(shí)現(xiàn)premain方法或拋出了未被捕獲的異常,JVM將會(huì)退出。

javaagent的啟動(dòng)不要求實(shí)現(xiàn)一定提供命令行的方式,如果,實(shí)現(xiàn)支持通過(guò)命令行啟動(dòng),實(shí)現(xiàn)必須支持在命令行中通過(guò)指定-javaagent參數(shù)啟動(dòng)。-javaagent可以在命令行中使用多次,啟動(dòng)多個(gè)代理。premain函數(shù)的調(diào)用順序和命令行中指定的順序一致,多個(gè)代理可以使用相同<jarpath>.

沒有一個(gè)嚴(yán)格模型來(lái)定義premain函數(shù)的工作范圍,任何main函數(shù)可以做的工作,比如創(chuàng)建線程,在premain函數(shù)中都是合法的。

1.3.2 JVM啟動(dòng)后啟動(dòng)

實(shí)現(xiàn)可以提供在JVM啟動(dòng)之后再啟動(dòng)代理的機(jī)制。代理如何啟動(dòng)的細(xì)節(jié)特定于實(shí)現(xiàn),通常應(yīng)用程序已經(jīng)啟動(dòng),并且它的main方法已經(jīng)被調(diào)用。如果實(shí)現(xiàn)支持在JVM啟動(dòng)后啟動(dòng)代理,代理必須滿足以下條件:

  1. 清單文件包含Agent-Class屬性,屬性的值為代理類全名。

  2. 代理類必須實(shí)現(xiàn) public static agentmain 方法。

agentmain方法有以下兩個(gè)函數(shù)簽名:

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

JVM首先嘗試調(diào)用具有簽名1的方法,如果,代理類沒有實(shí)現(xiàn)該方法,JVM嘗試調(diào)用簽名為2的方法。

代理類可以同時(shí)實(shí)現(xiàn)premainagentmain兩個(gè)方法,當(dāng)代理以命令行方式啟動(dòng)時(shí),JVM調(diào)用premain函數(shù),當(dāng)代理在JVM啟動(dòng)之后啟動(dòng)時(shí),JVM調(diào)用agentmain函數(shù),而且JVM不會(huì)調(diào)用premain函數(shù)。

agentmain函數(shù)參數(shù)的傳遞也是通過(guò)agentArgs,所有參數(shù)組合為一個(gè)字符串,參數(shù)的解析由代理負(fù)責(zé)。

agentmain函數(shù)必須完成啟動(dòng)代理所有必須的初始化動(dòng)作,當(dāng)啟動(dòng)完成后,agentmain函數(shù)必須返回。如果,代理不能啟動(dòng)或拋出未捕獲的異常,JVM都會(huì)退出。

1.3.3 打包為可執(zhí)行文件

如果代理打包到可執(zhí)行JAR文件中,可執(zhí)行JAR文件的清單中必須包含Launcher-Agent-Class 屬性,指定一個(gè)在應(yīng)用main函數(shù)調(diào)用之前代理啟動(dòng)的類。JVM嘗試在代理上調(diào)用以下方法:

public static void agentmain(String agentArgs, Instrumentation inst)

如果,代理類沒有實(shí)現(xiàn)上述方法,JVM則調(diào)用下面的方法。

public static void agentmain(String agentArgs)

agentArgs 參數(shù)的值必須為空字符串。
agentmain函數(shù)必須完成代理啟動(dòng)必須的所有初始化動(dòng)作并在啟動(dòng)后返回。如果,代理無(wú)法啟動(dòng)或拋出未捕獲的異常,JVM會(huì)退出。

1.3.4 加載代理類以及代理類可用的模塊/類

系統(tǒng)類加載器負(fù)責(zé)加載代理JAR文件中的所有類,并且成為系統(tǒng)類加載器的未命名模塊的成員。 系統(tǒng)類加載器通常也定義包含應(yīng)用程序main方法的類。對(duì)代理類可見的所有類都對(duì)系統(tǒng)類加載器可見,必須滿足下面的最低要求:

  • 啟動(dòng)層中的模塊導(dǎo)出的包中的類。 啟動(dòng)層是否包含所有平臺(tái)模塊取決于初始模塊或應(yīng)用程序的啟動(dòng)方式。

  • 類可被系統(tǒng)類加載器定義。

  • 啟動(dòng)類加載器定義的所有代理的類為其未命名模塊的成員。

如果代理類需要鏈接到不在啟動(dòng)層中的平臺(tái)(或其他)模塊中的類,則需要以確保這些模塊位于啟動(dòng)層中的方式啟動(dòng)應(yīng)用程序。 例如,在JDK實(shí)現(xiàn)中,--add-modules命令行選項(xiàng)可用于將模塊添加到要在啟動(dòng)時(shí)解析的根模塊集中。

啟動(dòng)類加載器可以加載代理支持的類(通過(guò)appendToBootstrapClassLoaderSearch或指定Boot-Class-Path屬性)必須僅鏈接到定義啟動(dòng)類加載器的類。 無(wú)法保證啟動(dòng)類加載器可以在所有平臺(tái)工作。

如果配置了自定義系統(tǒng)類加載器(通過(guò)getSystemClassLoader方法中指定的系統(tǒng)屬性java.system.class.loader),則必須定義appendToSystemClassLoaderSearch中指定的appendToClassPathForInstrumentation方法。 換句話說(shuō),自定義系統(tǒng)類加載器必須支持將代理JAR文件添加到系統(tǒng)類加載器搜索范圍內(nèi)的機(jī)制。

1.4 javaagent清單屬性

屬性 說(shuō)明 是否必選 默認(rèn)值
Premain-Class 包含premain方法的類 依賴啟動(dòng)方式 無(wú)
Agent-Class 包含agentmain方法的類 依賴啟動(dòng)方式 無(wú)
Boot-Class-Path 啟動(dòng)類加載器搜索路徑 無(wú)
Can-Redefine-Classes 是否可以重定義代理所需的類 false
Can-Retransform-Classes 是否能夠重新轉(zhuǎn)換此代理所需的類 false
Can-Set-Native-Method-Prefix 是否能夠設(shè)置此代理所需的本機(jī)方法前綴 false

2 寫一個(gè)Java Agent

基于上面的介紹,我們實(shí)現(xiàn)一個(gè)下載JVM中所有非系統(tǒng)類的javaagent。整個(gè)開發(fā)過(guò)程包括以下三步:1)定義代理類,實(shí)現(xiàn)類下載功能;2)配置、打包;3)命令行啟動(dòng)測(cè)試。

2.1 代理類實(shí)現(xiàn)

實(shí)現(xiàn)premain函數(shù)

package io.ct.java.agent;

import java.lang.instrument.Instrumentation;

public class AgentApplication {
    public static void premain(String arg, Instrumentation instrumentation) {
        System.err.println("agent startup , args is " + arg);
        // 注冊(cè)我們的文件下載函數(shù)
        instrumentation.addTransformer(new DumpClassesService());
    }
}

文件下載類實(shí)現(xiàn)ClassFileTransformer接口,在類被加載時(shí)下載類的字節(jié)碼:

package io.ct.java.agent;

import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.List;

/**
 * Copyright (C), 2018-2018, open source
 * FileName: DumpClassesService
 *
 * @author : 大哥
 * Date:     2018/12/8 21:01
 */
public class DumpClassesService implements ClassFileTransformer {
    private static final List<String> SYSTEM_CLASS_PREFIX = Arrays.asList("java", "sum", "jdk");

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (!isSystemClass(className)) {
            System.out.println("load class " + className);
            FileOutputStream fos = null;
            try {
                // 將類名統(tǒng)一命名為classNamedump.class格式
                fos = new FileOutputStream(className + "dump.class");
                fos.write(classfileBuffer);
                fos.flush();
            } catch (IOException ioe) {
                ioe.printStackTrace();
            } finally {
                // 關(guān)閉文件輸出流
                if (null != fos) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return classfileBuffer;
    }

    /**
     * 判斷一個(gè)類是否為系統(tǒng)類
     *
     * @param className 類名
     * @return System Class then return true,else return false
     */
    private boolean isSystemClass(String className) {
        // 假設(shè)系統(tǒng)類的類名不為NULL而且不為空
        if (null == className || className.isEmpty()) {
            return false;
        }

        for (String prefix : SYSTEM_CLASS_PREFIX) {
            if (className.startsWith(prefix)) {
                return true;
            }
        }
        return false;
    }
}

2.2 配置MANIFEST.MF

MANIFEST.MF文件兩種方式生成:手動(dòng)配置和自動(dòng)生成,手動(dòng)配置只需要在resources文件下創(chuàng)建META-INF/MENIFEST.MF文件即可。除去手動(dòng)配置外,可以使用maven插件在打包階段自動(dòng)生成,maven的插件配置如下:

           <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>io.ct.java.agent.AgentApplication</Premain-Class>
                            <Agent-Class>io.ct.java.agent.AgentApplication</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>

生成的jar包格式如下:


java-agent.jpg

其中MANIFEST.MF的文件內(nèi)容如下(不同的配置生成的文件內(nèi)容不完全一致):

Manifest-Version: 1.0
Implementation-Title: agent
Premain-Class: io.ct.java.agent.AgentApplication
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: chentong
Agent-Class: io.ct.java.agent.AgentApplication
Can-Redefine-Classes: true
Implementation-Vendor-Id: io.ct.java
Can-Retransform-Classes: true
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_171
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
 ot-starter-parent/agent

2.3 命令行啟動(dòng)Java Agent

執(zhí)行下面的命令,運(yùn)行已經(jīng)編譯好的類Hello,可以在同級(jí)目錄下生成一個(gè)名為Hellodump.class的文件。

java -javaagent:agent-0.0.1-SNAPSHOT.jar Hello
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,711評(píng)論 19 139
  • 用兩張圖告訴你,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 14,137評(píng)論 2 59
  • 0 介紹 使用 Instrumentation,使得開發(fā)者可以構(gòu)建一個(gè)獨(dú)立于應(yīng)用程序的代理程序(Agent),用來(lái)...
    七寸知架構(gòu)閱讀 28,648評(píng)論 3 85
  • 倉(cāng)鼠在森林里找尋食物,她餓了,但是畢竟是成年了,所以想著不能再依賴鼠爸鼠媽了所以今天她決定自己在森林里面建造一個(gè)家...
    00_cca3閱讀 1,067評(píng)論 0 0

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