Java Agent简介
Java Agent 是从 JDK1.5 开始引入的, 它本身就是 java 命令的一个参数(即 -javaagent)。-javaagent 参数之后需要指定一个 jar 包,这个 jar 包需要同时满足下面两个条件:
- 在 META-INF 目录下的 MANIFEST.MF 文件中必须指定 premain-class 配置项。
- premain-class 配置项指定的类必须提供了 premain() 方法。
在 Java 虚拟机启动时,执行 main() 函数之前,虚拟机会先找到 -javaagent 命令指定 jar 包,然后执行 premain-class 中的 premain() 方法。
使用 Java Agent 的步骤大致如下:
-
定义一个 MANIFEST.MF 文件,在其中添加 premain-class 配置项。
-
创建 premain-class 配置项指定的类,并在其中实现 premain() 方法,方法签名如下:
public static void premain(String agentArgs, Instrumentation inst){
}
3. 将 MANIFEST.MF 文件和 premain-class 指定的类一起打包成一个 jar 包。 4. 使用 -javaagent 指定该 jar 包的路径即可执行其中的 premain() 方法。
创建Java Agent
创建一个Maven项目,用于创建java agent
public class JavaAgentTest {
public static void premain(String agentArgs,
Instrumentation inst)throws Exception {
System.out.println("this is a java agent only 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 文件并打包,代码如下:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<!-- 将所有依赖包都打到jar包中-->
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest><!-- 添加MANIFEST.MF中的各项配置-->
<!-- 添加 mplementation-*和Specification-*配置项-->
<addDefaultImplementationEntries>true
</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true
</addDefaultSpecificationEntries>
</manifest>
<!-- 将 premain-class 配置项设置为com.dragon.TestAgent-->
<manifestEntries>
<Premain-Class>com.dragon.TestAgent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<!-- 绑定到package生命周期阶段上 -->
<phase>package</phase>
<goals>
<!-- 绑定到package生命周期阶段上 -->
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
打包
mvn package -Dcheckstyle.skip -DskipTests
完成打包之后,我们可以解压 target 目录下的 test-agent.jar,在其 META-INF 目录下可以找到 MANIFEST.MF 文件,其内容如下:
Manifest-Version: 1.0
Implementation-Title: JavaAgentTest
Implementation-Version: 1.0-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: wangjinlong
Premain-Class: com.dragon.JavaAgentTest
Specification-Title: JavaAgentTest
Implementation-Vendor-Id: com.dragon
Can-Retransform-Classes: true
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_265
Specification-Version: 1.0-SNAPSHOT
使用Java Agent
再创建一个普通的 Maven 项目,用于使用Java Agent Main方法如下
public class Main {
public static void main(String[] args) throws Exception {
Thread.sleep(1000);
System.out.println("Hello World Main!");
}
}
在程序运行时指定javaagent参数
-javaagent:/Users/wangjinlong/IdeaProjects-Communicity/TestAgent/target/test-agent.jar
原理分析
premain() 方法有两个重载,如下所示,如果两个重载同时存在,【1】将会被忽略,只执行【2】:
public static void premain(String agentArgs) [1]
public static void premain(String agentArgs,
Instrumentation inst); [2]
确定 premain() 方法的两个重载优先级的逻辑在 sun.instrument.InstrumentationImpl.java 中实现。 代理类必须有一个premain或者 agentamin方法,这个方法可以有一个或2个参数。这个是按以下顺序来判断。
- declared with a signature of (String, Instrumentation)
- declared with a signature of (String)
- inherited with a signature of (String, Instrumentation)
- inherited with a signature of (String) 所以,当前类中声明的方法会比继承的方法的优先级高,两个参数的方法的优先级比一个参数的方法的优先级高。 相关代码如下:
// WARNING: the native code knows the name & signature of this method
private void
loadClassAndCallPremain( String classname,
String optionsString)
throws Throwable {
loadClassAndStartAgent( classname, "premain", optionsString );
}
// Attempt to load and start an agent
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);
}
Instrumentation
Instrumentation 位于 java.lang.instrument 包中,通过这个工具包,我们可以编写一个强大的 Java Agent 程序,用来动态替换或是修改某些类的定义。
- addTransformer()/removeTransformer() 方法:注册/注销一个 ClassFileTransformer 类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义。
- redefineClasses() 方法:该方法针对的是已经加载的类,它会对传入的类进行重新定义。
- **getAllLoadedClasses()方法:**返回当前 JVM 已加载的所有类。
- getInitiatedClasses() 方法:返回当前 JVM 已经初始化的类。
- getObjectSize()方法:获取参数指定的对象的大小。
实现类的转换
首先定义要转换的类和转换后的类
转换的类
public class TestClass {
public String hello() {
return "I am TestClass";
}
}
转换后的类(类的包名和类名必须一样)
public class TestClass{
public String hello() {
return "I am TestClass from agent";
}
}
类转换器
class Transformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader l, String className,
Class<?> c, ProtectionDomain pd, byte[] b) {
if (!c.getSimpleName().equals("TestClass")) {
return null; // 只修改TestClass的定义
}
// 读取 TestClass.class这个 class文件,作为 TestClass类的新定义
return getBytesFromFile("Class文件所在绝对路径");
}
public static byte[] getBytesFromFile(String fileName) {
try {
// precondition
File file = new File(fileName);
InputStream is = new FileInputStream(file);
long length = file.length();
byte[] bytes = new byte[(int) length];
// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset < bytes.length
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
if (offset < bytes.length) {
throw new IOException("Could not completely read file "
+ file.getName());
}
is.close();
return bytes;
} catch (Exception e) {
System.out.println("error occurs in _ClassTransformer!"
+ e.getClass().getName());
return null;
}
}
}
编写 premain() 方法,并注册一个 Transformer 对象:、
public class TestAgent {
public static void agentmain(String agentArgs,
Instrumentation inst)throws Exception {
// 注册一个 Transformer,该 Transformer在类加载时被调用
inst.addTransformer(new com.dragon.Transformer(), true);
inst.retransformClasses(TestClass.class);
System.out.println("premain done");
}
public static void premain(String agentArgs) {
System.out.println("this is a java agent only one args");
System.out.println("参数:" + agentArgs + "\n");
}
}
在 maven-assembly-plugin 插件中添加 Can-Retransform-Classes 参数:
<manifestEntries>
<Premain-Class>com.dragon.TestAgent</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
使用类
-javaagent:/路径/test-agent.jar=option1=value2,option2=value2
结果
premain done
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
agentmain
在 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() 方法。
Attach API 中有 2 个核心类:
- VirtualMachine 是对一个 Java 虚拟机的抽象,在 Attach 工具程序监控目标虚拟机的时候会用到该类。VirtualMachine 提供了 JVM 枚举、Attach、Detach 等基本操作。
- VirtualMachineDescriptor 是一个描述虚拟机的容器类,后面示例中会介绍它如何与 VirtualMachine 配合使用。