Java Agent 简介


一、写在前面

Java Agent 这个技术出现在 JDK1.5 之后,对于大多数人来说都比较陌生,但是多多少少又接触过,实际上,我们平时用的很多工具,都是基于 Java Agent 实现的,例如常见的热部署 JRebel,各种线上诊断工具(Btrace, Greys),还有阿里开源的 Arthas。

其实 Java Agent 一点都不神秘,也是一个 Jar 包,只是启动方式和普通 Jar 包有所不同,对于普通的Jar包,通过指定类的 main 函数进行启动,但是 Java Agent 并不能单独启动,必须依附在一个 Java 应用程序运行。

我们可以使用 Agent 技术构建一个独立于应用程序的代理程序,用来协助监测、运行甚至替换其他 JVM 上的程序,使用它可以实现虚拟机级别的 AOP 功能。

二、动手写一个 Java Agent

首先,我们先来写一段简单的 Agent 程序:

public class AgentTest {
    /**
     * 以 vm 参数的方式载入,在 java 程序的 main 方法执行之前执行
     *
     * @param agentArgs
     * @param inst      Agent技术主要使用的 api,我们可以使用它来改变和重新定义类的行为
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain start");

        System.out.println(agentArgs);
    }

    /**
     * 以 Attach 的方式载入,在 Java 程序启动后执行
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain start");

        System.out.println(agentArgs);
    }
}

因为 Java Agent 的特殊性,需要一些特殊的配置,例如指定 Agent 的启动类等。这样才能在加载 Java Agent 之后,找到并运行对应的 agentmain 或者 premain 方法。配置方式主要有两种,一种是利用 maven-assembly-plugin 插件(推荐),一种是 MANIFEST.MF 文件。

2.1 maven-assembly-plugin 插件

          <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>org.agent.AgentTest</Premain-Class>
                            <Agent-Class>org.agent.AgentTest</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>

2.2 MANIFEST.MF 文件

在 META-INF 目录下创建 MANIFEST.MF 文件:

Manifest-Version: 1.0
Agent-Class: org.agent.AgentTest
Premain-Class: org.agent.AgentTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true

值得一提的是,即使新建了 MANIFEST.MF 文件,仍然需要配置 maven-assembly-plugin 信息,否则 MANIFEST.MF 信息会被 Maven 生成的信息覆盖掉。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestFile>
                            src/main/resources/META-INF/MANIFEST.MF
                        </manifestFile>
                    </archive>
                </configuration>
            </plugin>

配置完上面的内容,运行 mvn assembly:single 打包属于 Java Agent 的 jar 包。

三、运行你的 Agent 程序

Java Agent 程序写好了,怎么运行它呢?上面看到 Agent 程序分为两种,一种是 premain 函数,在主程序运行之前执行;一种是 agentmain 函数,在主程序运行之后执行。Java 加载这两种 Agent 程序也有区别:

3.1 主程序运行前加载

通过 JVM 参数 -javaagent:**.jar[=test] 启动,其中 test 为传入 premain 的 agentArgs 的参数,程序启动的时候,会优先加载 Java Agent,并执行其 premain 方法,这个时候,其实大部分的类都还没有被加载,这个时候可以实现对新加载的类进行字节码修改,但是如果 premain 方法执行失败或抛出异常,那么 JVM 会被中止,这是很致命的问题。

3.2 主程序运行后加载

程序启动之后,通过某种特定的手段加载 Java Agent,这个特定的手段就是 VirtualMachine 的 attach api,这个 api 其实是 JVM 进程之间的的沟通桥梁,底层通过socket 进行通信,JVM A 可以发送一些指令给JVM B,B 收到指令之后,可以执行对应的逻辑,比如在命令行中经常使用的 jstack、jps 等,很多都是基于这种机制实现的。

VirtualMachine 的实现位于 tools.jar 中。

        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>

因为是进程间通信,所以使用 attach api 的也是一个独立的Java进程,下面是一个简单的实现:

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        VirtualMachine virtualMachine = null;
        try {
            // 1100 是进程号
            virtualMachine = VirtualMachine.attach("1100");
            // 第一个参数是 agent jar包路径,第二个参数为传入 agentmain 的 args 参数
            virtualMachine.loadAgent("D:\\concurrency-0.0.1-SNAPSHOT-jar-with-dependencies.jar", "test");
        } finally {
            if (virtualMachine != null) {
                virtualMachine.detach();
            }
        }

    }


推荐阅读:

基于Java Instrument的Agent实现


作者:JMCui,发布于:2020/03/26
原文:https://www.cnblogs.com/jmcui/p/12561004.html