Java Agent

280 阅读5分钟

什么是 Java Agent

Java Agent 是从 JDK1.5 开始引入的,算是一个比较老的技术了。作为 Java 的开发工程师,我们常用的命令之一就是 java 命令,而 Java Agent 本身就是 java 命令的一个参数(即 -javaagent)。

-javaagent参数之后需要指定一个 jar 包,这个 jar 包需要同时满足下面两个条件:

  1. 在 META-INF 目录下的 MANIFEST.MF 文件中必须指定 premain-class 配置项。
  2. premain-class 配置项指定的类必须提供了premain()方法。

premain() 方法有两个重载,如下所示,如果两个重载同时存在,【1】将会被忽略,只执行【2】:

public static void premain(String agentArgs) [1]
public static void premain(String agentArgs, Instrumentation inst); [2]
  • agentArgs 参数:-javaagent 命令携带的参数。
  • inst 参数:java.lang.instrumen.Instrumentation 是 Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法。

使用 Java Agent

使用 Java Agent 的步骤大致如下:

1.  定义一个 MANIFEST.MF 文件,在其中添加 premain-class 配置项。

2.  创建  premain-class 配置项指定的类,并在其中实现 premain() 方法,方法签名如下:

public static void premain(String agentArgs, Instrumentation inst){
   ... 
}

3.  将 MANIFEST.MF 文件和  premain-class 指定的类一起打包成一个 jar 包。

4.  使用 -javaagent 指定该 jar 包的路径即可执行其中的 premain() 方法。

Java Agent示例

首先,我们创建一个最基本的 Maven 项目,然后创建AgentDemo.java 这一个类,项目的整体结构如图所示。

image.png

AgentDemo代码如下所示:

public class AgentDemo {
    public static void premain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("this is a java agent with two args");
        System.out.println("参数:" + agentArgs + "\n");
    }
    public static void premain(String agentArgs) {
        System.out.println("this is a java agent only one args");
        System.out.println("参数:" + agentArgs + "\n");
    }
}

接下来还需要创建 MANIFEST.MF 文件并打包,直接使用 maven-assembly-plugin 打包插件来完成这两项功能。在 pom.xml 中引入maven-assembly-plugin 插件并添加相应的配置,如下所示:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>2.4</version>
    <configuration>
        <appendAssemblyId>false</appendAssemblyId>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifest>
                <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
            </manifest>
            <manifestEntries>
                <Premain-Class>com.codersm.agent.demo.AgentDemo</Premain-Class>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

最后执行 maven 命令进行打包,如下:

mvn package -Dcheckstyle.skip -DskipTests

测试 Agent

再创建一个普通的 Maven 项目:agent-demo-test,项目结构与 agent-demo类似,如图所示:

image.png

在 Main 这个类中定义了该项目的入口 main() 方法,如下所示:

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

在启动agent-demo-test项目之前,需要在 VM options 中使用 -javaagent 命令指定前面创建的agent-demo.jar,如图所示:

image.png

启动agent-demo-test之后得到了如下输出:

image.png

修改类实现

Java Agent 可以实现的功能远不止添加一行日志这么简单,这里需要关注一下 premain() 方法中的第二个参数:Instrumentation。

Instrumentation 位于 java.lang.instrument 包中,通过这个工具包,我们可以编写一个强大的 Java Agent 程序,用来动态替换或是修改某些类的定义。

 Instrumentation类提供了如下API方法:

  • addTransformer()/removeTransformer() 方法:注册/注销一个 ClassFileTransformer 类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义。
  • redefineClasses() 方法:该方法针对的是已经加载的类,它会对传入的类进行重新定义。
  • getAllLoadedClasses()方法:返回当前 JVM 已加载的所有类。
  • getInitiatedClasses() 方法:返回当前 JVM 已经初始化的类。
  • getObjectSize()方法:获取参数指定的对象的大小。

下面我们通过一个示例演示 Instrumentation 如何与 Java Agent 配合修改类定义。首先我们提供一个普通的 Java 类:TestClass,其中提供一个 getNumber() 方法:

public class TestClass {
    public int getNumber() { return 1;  }
}

编译生成 TestClass.class 文件之后,我们将 getNumber() 方法返回值修改为 2,然后再次编译,并将此次得到的 class 文件重命名为 TestClass.class.2 文件,如图所示,我们得到两个 TestClass.class 文件:

image.png

之后将 TestClass.getNumber() 方法返回值改回 1 ,重新编译。

然后编写一个 main() 方法,新建一个 TestClass 对象并输出其 getNumber() 方法的返回值:

public class Main {
    public static void main(String[] args) {
        System.out.println(new TestClass().getNumber());
    }
}

接下来编写 premain() 方法,并注册一个 Transformer 对象:

public class TestAgent {
    public static void premain(String agentArgs, Instrumentation inst) 
              throws Exception {
        // 注册一个 Transformer,该 Transformer在类加载时被调用
        inst.addTransformer(new Transformer(), true);
        // 让类重新加载,从而使得注册的类修改器能够重新修改类的字节码。
        inst.retransformClasses(TestClass.class);
        System.out.println("premain done");
    }
}

Transformer  实现了 ClassFileTransformer,其中的 transform() 方法实现可以修改加载到的类的定义,具体实现如下:

class Transformer implements ClassFileTransformer {

    public byte[] transform(ClassLoader l, String className, 
       Class<?> c, ProtectionDomain pd, byte[] b)  {
        if (!c.getSimpleName().equals("TestClass")) {
            return null; // 只修改TestClass的定义
        }
        // 读取 TestClass.class.2这个 class文件,作为 TestClass类的新定义
        return getBytesFromFile("TestClass.class.2");
    }
}

最后,打包启动应用,得到的输出如下:

premain done
2

Attach API 基础

在 Java 5 中,Java 开发者只能通过 Java Agent 中的 premain() 方法在 main() 方法执行之前进行一些操作,这种方式在一定程度上限制了灵活性。Java 6 针对这种状况做出了改进,提供了一个 agentmain() 方法,Java 开发者可以在 main() 方法执行以后执行 agentmain() 方法实现一些特殊功能。

agentmain()方法同样有两个重载,它们的参数与premain() 方法相同,而且前者优先级也是高于后者的:

public static void agentmain (String agentArgs, Instrumentation inst);[1]

public static void agentmain (String agentArgs); [2]

agentmain() 方法主要用在 JVM Attach 工具中,Attach API 是 Java 的扩展 API,可以向目标 JVM “附着”(Attach)一个代理工具程序,而这个代理工具程序的入口就是 agentmain() 方法。