什么是 Java Agent
Java Agent 是从 JDK1.5 开始引入的,算是一个比较老的技术了。作为 Java 的开发工程师,我们常用的命令之一就是 java 命令,而 Java Agent 本身就是 java 命令的一个参数(即 -javaagent)。
-javaagent
参数之后需要指定一个 jar 包,这个 jar 包需要同时满足下面两个条件:
- 在 META-INF 目录下的 MANIFEST.MF 文件中必须指定 premain-class 配置项。
- 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 这一个类,项目的整体结构如图所示。
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类似,如图所示:
在 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,如图所示:
启动agent-demo-test之后得到了如下输出:
修改类实现
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 文件:
之后将 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() 方法。