Java-基於 Instrument 的 Agent


Agent 為 JVMTI 的客戶端。

這里記錄的是基於Java Instrument 的 Agent 實現,還有直接基於 JVMTI 的 Agent 實現

在 JDK1.5 以后,我們可以使用 Agent 技術構建一個獨立於應用程序的代理程序,用來協助監測、運行甚至替換其他 JVM 上的程序。使用它可以實現虛擬機級別的 AOP 功能。

Agent 分為兩種,一種是在主程序之前運行的 Agent,一種是在主程序之后運行的 Agent(JDK1.6 以后)。

 

一、在主程序運行之前的代理程序

1.編寫 agent 程序

package before;

import java.lang.instrument.Instrumentation;

public class AgentApplication {
    public static void premain(String arg, Instrumentation instrumentation) {
        System.err.println("agent startup , args is " + arg);
    }
}

2.添加 MANIFEST.MF 文件

路徑為 META-INF/MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: before.AgentApplication
Can-Redefine-Classes: true
Can-Retransform-Classes: true

若使用的是 Maven 編譯就不用手動添加,配置 pom.xml 即可

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <configuration>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                    </manifest>
                    <manifestEntries>
                        <Premain-Class>before.AgentApplication</Premain-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

3.運行

新建一個測試類並編譯為 class 文件

package com;

public class Main {
    public static void main(String[] args) {
        System.out.println("123");
    }
}

使用命令運行

java -javaagent:..\mahout-1.0-SNAPSHOT.jar=abc com.Main

可以看到 premain 方法在 main 之前運行了

 

二、在主程序運行之后的代理程序

關於動態 attach:https://openjdk.java.net/groups/hotspot/docs/Serviceability.html#battach

https://juejin.im/post/5b0d020d518825153f10403f

1.編寫 agent 程序

由於是在主程序運行后再執行,意味着我們可以獲取主程序運行時的信息,這里我們打印出來主程序中加載的類名。

package after;

import java.lang.instrument.Instrumentation;

public class AgentApplication {
    public static void agentmain(String arg, Instrumentation instrumentation) {
        System.err.println("agent startup , args is " + arg);

        Class<?>[] classes = instrumentation.getAllLoadedClasses();
        for (Class<?> cls : classes) {
            System.out.println(cls.getName());
        }
    }
}

2.添加 MANIFEST.MF 文件

路徑為 META-INF/MANIFEST.MF

Manifest-Version: 1.0
Agent-Class: after.AgentApplication
Can-Redefine-Classes: true
Can-Retransform-Classes: true

若使用的是 Maven 編譯就不用手動添加,配置 pom.xml 即可

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <configuration>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                    </manifest>
                    <manifestEntries>
                        <Agent-Class>after.AgentApplication</Agent-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

3.運行

由於是在程序運行之后運行,需要先有一個 Java 進程,直接用 IDE 運行即可,不用使用命令行。

package com;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        for (; ; ) {
            System.out.println("123");
            Thread.sleep(1000);
        }
    }
}

運行后查看改程序的進程號,這里為 9084

然后將 agent 程序附加到上面程序的進程中

com.sun.tools.attach.VirtualMachine 在 JAVA_HOME 路徑下 lib/tools.jar 中,如果 IDE 報找不到,可以手動將 tools.jar 添加進來,或配置 CLASSPATH 環境變量。

package com;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class Attach {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        VirtualMachine vm = VirtualMachine.attach("9084");
        vm.loadAgent("D:\\IDEA\\CodeLib\\jhxxb\\mahout\\target\\mahout-1.0-SNAPSHOT.jar");
    }
}

直接 IDE 運行即可,執行完畢后回到 Main 程序的打印控制台

 

三、使用相關

其中 -javaagent 可以有多個,但如果把 -javaagent 放在 -jar 后面,則不會生效。也就是放在主程序后面的 agent 是無效的。

如:java -javaagent:D:\myagent-1.jar=ABC -javaagent:D:\myagent-2.jar=DEF -jar myapp.jar -javaagent:D:\myagent-3.jar=GHI,其中 myagent-3.jar 是無效的。

 

其中 premain(agentmain) 方法有兩個:

  1. public static void premain(String agentArgs, Instrumentation inst)
  2. public static void premain(String agentArgs)

JVM 會優先加載 1,加載成功則忽略 2,如沒有 1,則加載 2。加載邏輯在 sun.instrument.InstrumentationImpl 類中:

private void loadClassAndStartAgent(String classname, String methodname, String optionsString) throws Throwable {
    ClassLoader mainAppLoader = ClassLoader.getSystemClassLoader();
    Class<?> javaAgentClass = mainAppLoader.loadClass(classname);

    Method m = null;
    NoSuchMethodException firstExc = null;
    boolean twoArgAgent = false;

    // The agent class must have a premain or agentmain method that
    // has 1 or 2 arguments. We check in the following order:
    //
    // 1) declared with a signature of (String, Instrumentation)
    // 2) declared with a signature of (String)
    // 3) inherited with a signature of (String, Instrumentation)
    // 4) inherited with a signature of (String)
    //
    // So the declared version of either 1-arg or 2-arg always takes
    // primary precedence over an inherited version. After that, the
    // 2-arg version takes precedence over the 1-arg version.
    //
    // If no method is found then we throw the NoSuchMethodException
    // from the first attempt so that the exception text indicates
    // the lookup failed for the 2-arg method (same as JDK5.0).

    try {
        m = javaAgentClass.getDeclaredMethod(methodname, new Class<?>[]{String.class, java.lang.instrument.Instrumentation.class});
        twoArgAgent = true;
    } catch (NoSuchMethodException x) {
        // remember the NoSuchMethodException
        firstExc = x;
    }

    if (m == null) {
        // now try the declared 1-arg method
        try {
            m = javaAgentClass.getDeclaredMethod(methodname, new Class<?>[]{String.class});
        } catch (NoSuchMethodException x) {
            // ignore this exception because we'll try
            // two arg inheritance next
        }
    }

    if (m == null) {
        // now try the inherited 2-arg method
        try {
            m = javaAgentClass.getMethod(methodname, new Class<?>[]{String.class, java.lang.instrument.Instrumentation.class});
            twoArgAgent = true;
        } catch (NoSuchMethodException x) {
            // ignore this exception because we'll try
            // one arg inheritance next
        }
    }

    if (m == null) {
        // finally try the inherited 1-arg method
        try {
            m = javaAgentClass.getMethod(methodname, new Class<?>[]{String.class});
        } catch (NoSuchMethodException x) {
            // none of the methods exists so we throw the
            // first NoSuchMethodException as per 5.0
            throw firstExc;
        }
    }

    // the premain method should not be required to be public,
    // make it accessible so we can call it
    // Note: The spec says the following:
    //     The agent class must implement a public static premain method...
    setAccessible(m, true);

    // invoke the 1 or 2-arg method
    if (twoArgAgent) {
        m.invoke(null, new Object[]{optionsString, this});
    } else {
        m.invoke(null, new Object[]{optionsString});
    }

    // don't let others access a non-public premain method
    setAccessible(m, false);
}
View Code

 

Instrument premain、agentmain 方法執行時機:

premain 執行時機:在 JVM 啟動時(所有的 Java 類都未被初始化,所有的對象實例都未被創建),初始化函數 eventHandlerVMinit 會調用 sun.instrument.instrumentationImpl 類的 loadClassAndCallPremain 方法去執行 Premain-Class 指定類的 premain 方法。

agentmain 執行時機:在 JVM 啟動后,通過 VirtualMachine 附着一個 Instrument,如:vm.loadAgent(jar),會調用 sun.instrument.instrumentationImpl 類的 loadClassAndCallAgentmain 方法去執行 Agentmain-Class 指定類的 agentmain 方法。

 

premain、agentmain 方法中兩個參數:

agentArgs:代理程序命令行中輸入參數,隨同 “-javaagent” 一起傳入,與 main 函數不同的是,這個參數是一個字符串而不是一個字符串數組。

inst:java.lang.instrument.Instrumentation 實例,由 JVM 自動傳入,集中了幾乎所有功能方法,如:類操作、classpath 操作等。

 

META-INF/MAINFEST.MF 參數:

  • Premain-Class:指定包含 premain 方法的類名。
  • Agent-Class:指定包含 agentmain 方法的類名。
  • Boot-Class-Path:指定引導類加載器搜索的路徑列表。查找類的特點於平台的機制失敗后,引導類加載器會搜索這些路徑。
  • Can-Redefine-Class:是否能重新定義此代理所需的類,默認為 false。
  • Can-Retransform-Class:是否能重新轉換此代理所需的類,默認為 false。
  • Can-Set-Native-Method-Prefix:是否能設置此代理所需的本機方法前綴,默認值為 false。

 


https://www.jianshu.com/p/63c328ca208d

https://yq.aliyun.com/articles/658806

https://www.jianshu.com/p/9f4e8dcb3e2f

https://juejin.im/post/5b0925ec51882538aa1ee248

https://www.ibm.com/developerworks/cn/java/j-lo-jpda2


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM