Java Agent 的簡單使用

在上一篇文章《每天學習一點點之 Spring 計時器 StopWatch》中簡單提到了 Java Agent 可以做應用代碼的無侵入,也跟朋友進行了討論。在之前《ThreadLocal 系列之 TransmittableThreadLocal》中,有一個地方沒有討論到的就是 TransmittableThreadLocal 也提供了 Java Agent 的方式。這裏簡單介紹一下。

Java Agent 也是一個比較早的技術,屬於 JVMTI 的一種實現,它的流程跟 AOP 很像,不過它攔截的是 class,這也可以理解爲 JVM 事先已經埋了鉤子,然後我們根據相關的規範去實現就能觸發。這樣我們就可以使用 ASM 或者 Javassist 去操作 class,從而實現無侵入性。

Java Agent 有兩種啓動場景,JVM 啓動時和運行時候,接下來介紹 JVM 啓動時這種方式,可以配置啓動命令進行加載:

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

jarpath 就是 JAR 的路徑,options 是相關的參數。

The manifest of the agent JAR file must contain the attribute Premain-Class in its main manifest. The value of this attribute is the name of the agent class. The agent class must implement a public static premain method similar in principle to the main application entry point. After the Java Virtual Machine (JVM) has initialized, the premain method will be called, then the real application main method. The premain method must return in order for the startup to proceed.

JAR 的 MANIFEST.MF 文件必須指定 Premain-Class 屬性,即指定一個 Premain 類,然後這個類又必須要指定 premain 方法。

The premain method has one of two possible signatures. The JVM first attempts to invoke the following method on the agent class:

public static void premain(String agentArgs, Instrumentation inst)

If the agent class does not implement this method then the JVM will attempt to invoke:

public static void premain(String agentArgs)

premain 方法有兩種實現,JVM 會先嚐試調用第一個,如果第一個沒有實現那麼就會調用第二個。Instrumentation 接口是 JDK1.5 之後提供的,我們可以基於它來編寫 Agent。

在 main 方法之前執行

這裏基於 Maven 進行構建,先創建一個 Agent 類試一試:

public class AgentDemo {

    public static void premain(String arg, Instrumentation instrumentation){
        System.out.println("執行了 premain 方法,arg參數:"+arg);
    }
}

pom.xml 配置爲:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dongguabai</groupId>
    <artifactId>agent-dmo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <build>
        <finalName>agent-demo</finalName>
    </build>
</project>

打包後查看 MANIFEST.MF 文件:

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dongguabai
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_202

明顯與規定不符合,根據官方介紹 agent JAR 可以設置如下屬性:

The following manifest attributes are defined for an agent JAR file:

  • Premain-Class

    When an agent is specified at JVM launch time this attribute specifies the agent class. That is, the class containing the premain method. When an agent is specified at JVM launch time this attribute is required. If the attribute is not present the JVM will abort. Note: this is a class name, not a file name or path.

  • Agent-Class

    If an implementation supports a mechanism to start agents sometime after the VM has started then this attribute specifies the agent class. That is, the class containing the agentmain method. This attribute is required if it is not present the agent will not be started. Note: this is a class name, not a file name or path.

  • Launcher-Agent-Class

    If an implementation supports a mechanism to start an application as an executable JAR then the main manifest may include this attribute to specify the class name of an agent to start before the application main method is invoked.

  • Boot-Class-Path

    A list of paths to be searched by the bootstrap class loader. Paths represent directories or libraries (commonly referred to as JAR or zip libraries on many platforms). These paths are searched by the bootstrap class loader after the platform specific mechanisms of locating a class have failed. Paths are searched in the order listed. Paths in the list are separated by one or more spaces. A path takes the syntax of the path component of a hierarchical URI. The path is absolute if it begins with a slash character (’/’), otherwise it is relative. A relative path is resolved against the absolute path of the agent JAR file. Malformed and non-existent paths are ignored. When an agent is started sometime after the VM has started then paths that do not represent a JAR file are ignored. This attribute is optional.

  • Can-Redefine-Classes

    Boolean (true or false, case irrelevant). Is the ability to redefine classes needed by this agent. Values other than true are considered false. This attribute is optional, the default is false.

  • Can-Retransform-Classes

    Boolean (true or false, case irrelevant). Is the ability to retransform classes needed by this agent. Values other than true are considered false. This attribute is optional, the default is false.

  • Can-Set-Native-Method-Prefix

    Boolean (true or false, case irrelevant). Is the ability to set native method prefix needed by this agent. Values other than true are considered false. This attribute is optional, the default is false.

An agent JAR file may have both the Premain-Class and Agent-Class attributes present in the manifest. When the agent is started on the command-line using the -javaagent option then the Premain-Class attribute specifies the name of the agent class and the Agent-Class attribute is ignored. Similarly, if the agent is started sometime after the VM has started, then the Agent-Class attribute specifies the name of the agent class (the value of Premain-Class attribute is ignored).

這時候需要使用 maven-jar-plugin 插件,修改 pom.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dongguabai</groupId>
    <artifactId>agent-dmo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <build>
        <finalName>agent-demo</finalName>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>com.dongguabai.AgentDemo</Premain-Class>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

接下來我創建了一個最簡單的 Spring Boot 工程。然後配置啓動參數,這個可以在 IDEA 上直接進行配置:

在這裏插入圖片描述

或者使用命令行的方式(要注意 -javaagent 參數要在 -jar 之前):

java -javaagent:/Users/dongguabai/Desktop/temp/agent-demo/target/agent-demo.jar=hello  -jar /Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar

啓動後控制檯打印:

➜  demo java -javaagent:/Users/dongguabai/Desktop/temp/agent-demo/target/agent-demo.jar=hello  -jar /Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar 
執行了 premain 方法,arg參數:hello

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.6.RELEASE)

2020-04-29 16:31:31.267  INFO 17572 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication v0.0.1-SNAPSHOT on Dongguabai.local with PID 17572 (/Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar started by dongguabai in /Users/dongguabai/Desktop/temp/demo)

可以看到定義的 AgentDemo 類的 premain 方法已經執行。

計算函數執行時間

這裏基於 Java Agent 去實現上一篇文章《每天學習一點點之 Spring 計時器 StopWatch》中的計時功能。這裏統計 Spring Boot 項目中 main 方法的執行時長,首先將項目改爲非 Web 項目:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //SpringApplication.run(DemoApplication.class, args);
        long start = System.currentTimeMillis();
        new SpringApplicationBuilder(DemoApplication.class).web(WebApplicationType.NONE).run(args);
        long end = System.currentTimeMillis();
        System.out.println("---------耗時:" + (end - start));

    }

}

可以看到這裏的方式是顯示的,如果要做到無侵入,那我們需要去修改 DemoApplication 類,這時候就要用到 Instrumentation 類:

/**
* 添加 class 文件轉換器,類裝載的時候會調用,如果調用過程出現異常,JVM 會調用其他的轉換器,已註冊的ClassFileTransformer 將攔截所有應用程序類的加載,並能夠訪問他們的字節碼。ClassFileTransformer 也可以轉換應用程序類的字節碼,並使JVM的加載行爲與原來預期的字節完全不同。
* canRetransform 表示是否允許重新轉換,缺省 false;
*
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);

ClassFileTransformer 是個接口,需要重寫這個方法:

/**
* loader:當前的 ClassLoader;
*  className:當前類的名稱,注意這裏是以 / 分割
*  classBeingRedefined:已經被裝載的 Class;
*  protectionDomain:類的域
*   classfileBuffer:類文件原本的字節碼
*/
byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;

這個方法返回的就是要裝載的 class,如果返回 null 就相當於是沒有執行轉換,會返回之前的 class。修改 class 最方便的就是使用 Javaassist,在 Agent 項目中引入相關依賴:

<dependency>
			<groupId>org.javassist</groupId>
			<artifactId>javassist</artifactId>
			<version>3.20.0-GA</version>
		</dependency>

接下來目標就是將這麼幾行代碼動態的插入進去:

在這裏插入圖片描述

Javassist 就是增加代碼塊,這裏就相當於是代理增強了原來的 main 方法:

public class AgentDemo {


    private static final String METHOD_NAME = "main";

    public static void premain(String arg, Instrumentation instrumentation) {
        System.out.println("執行了 premain 方法,arg參數:" + arg);
        instrumentation.addTransformer(new ClassFileTransformer() {
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain, byte[] classfileBuffer) {

                if ("com/example/demo/DemoApplication".equals(className)) {
                    byte[] newClassFile;
                    ClassPool pool = ClassPool.getDefault();
                    CtClass ct = null;
                    try {
                        pool.insertClassPath(new LoaderClassPath(loader));
                        ct = pool.get(className.replace("/","."));
                        //獲取方法
                        CtMethod method = ct.getDeclaredMethod(METHOD_NAME);

                        String oldMethodName = METHOD_NAME + "$old";
                        //舊方法重命名
                        method.setName(oldMethodName);

                        // 複製原來的方法,名字爲原來的名字,用來增強之前的方法
                        CtMethod newMethod = CtNewMethod.copy(method, "main", ct, null);

                        // 構建代碼塊
                        StringBuilder bodyStr = new StringBuilder();
                        bodyStr.append("{");

                        //添加局部變量
                       // newMethod.addLocalVariable("startTime", CtClass.longType);
                        bodyStr.append("long startTime = System.currentTimeMillis();");
                        bodyStr.append("System.out.println(startTime);");

                        // 調用原方法,類似於method();($$)表示所有的參數
                        bodyStr.append(oldMethodName+"($$);\n");

                       // newMethod.addLocalVariable("endTime", CtClass.longType);
                        bodyStr.append("long endTime = System.currentTimeMillis();");
                        bodyStr.append("System.out.println(endTime-startTime);");
                        bodyStr.append("}");

                        //設置新方法方法體
                        newMethod.setBody(bodyStr.toString());

                        // 替換舊方法,增加新方法
                        ct.addMethod(newMethod);

                        newClassFile = ct.toBytecode();
                        System.out.println("22222");
                    } catch (Exception ignored) {
                        ignored.printStackTrace();
                        newClassFile = classfileBuffer;
                    } finally {
                        if (ct != null) {
                            ct.detach();
                        }
                    }
                    return newClassFile;
                }
                return null;
            }
        });
    }
}

出現異常:

Caused by: java.lang.ClassNotFoundException: javassist.ClassPath
        at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 7 more

在網上找了一下,需要修改一下 Agent 的 pom.xml 文件,配置 Boot-Class-Path

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dongguabai</groupId>
    <artifactId>agent-dmo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>


    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.20.0-GA</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>agent-demo</finalName>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>com.dongguabai.AgentDemo</Premain-Class>
                            <Boot-Class-Path>
                                /Users/dongguabai/develope/maven/repository/org/javassist/javassist/3.20.0-GA/javassist-3.20.0-GA.jar
                            </Boot-Class-Path>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Spring Boot 項目的啓動類:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //SpringApplication.run(DemoApplication.class, args);
        new SpringApplicationBuilder(DemoApplication.class).web(WebApplicationType.NONE).run(args);
    }

}

將兩個項目重新打包:

mvn clean package

運行:

➜  demo java -javaagent:/Users/dongguabai/Desktop/temp/agent-demo/target/agent-demo.jar=hello  -jar /Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar 

控制檯打印:

➜  demo java -javaagent:/Users/dongguabai/Desktop/temp/agent-demo/target/agent-demo.jar=hello  -jar /Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar 
執行了 premain 方法,arg參數:hello
22222
1588162691595

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.6.RELEASE)

2020-04-29 20:18:12.261  INFO 26265 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication v0.0.1-SNAPSHOT on Dongguabai.local with PID 26265 (/Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar started by dongguabai in /Users/dongguabai/Desktop/temp/demo)
2020-04-29 20:18:12.264  INFO 26265 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-04-29 20:18:12.829  INFO 26265 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'genericWebMvcConfig' of type [com.example.demo.GenericWebMvcConfig$$EnhancerBySpringCGLIB$$4e144799] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-04-29 20:18:12.860  INFO 26265 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'validator' of type [org.hibernate.validator.internal.engine.ValidatorImpl] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-04-29 20:18:12.998  INFO 26265 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.133 seconds (JVM running for 1.641)
1404

可以看到,的確是無侵入的增加了時間計算。

References

  • https://docs.oracle.com/javase/10/docs/api/java/lang/instrument/package-summary.html#package.description
  • https://blog.csdn.net/a158123/article/details/76957903
  • https://blogs.oracle.com/ouchina/javaagent
  • https://blog.csdn.net/chao_1990/article/details/70256503

歡迎關注公衆號
​​​​​​在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章