Java在1.5引入了java.lang.instrument,它允许实现一个java agent,通过agent监测运行在JVM上的程序,监测的机制是对方法字节码的修改。有点类似AOP,但比AOP更加松耦合,因为AOP的代码还是在工程里面的,而agent却与被监控的程序完全隔离。
下面我们用instrument来实现一个简单的java agent。
在启动JVM时,通过指示代理类及其代理选项,会启动一个代理程序(agent),该代理类必须实现公共的静态premain方法,该方法原理上类似于main应用程序入口点:
public static void premain(String agentArgs, Instrumentation inst);
(1)Agent类
package org.kylin.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class Agent {
private static Instrumentation inst;
public static void premain(String agentArgs, Instrumentation _inst) {
System.out.println("Agent is called...");
inst = _inst;
ClassFileTransformer transformer = new AopClassTransformer();
inst.addTransformer(transformer);
}
}
Agent类必须实现premain方法,该方法有两个参数,第一个为自定义传入的代理类参数,第二个为VM自动传入的Instrumentation实例。我们需要给inst添加一个类的转换器,该转换器必须实现ClassFileTransformer接口。
(2)实现ClassFileTransformer类
package org.kylin.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
public class AopClassTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("Transforming Class: " + className);
if (!className.equals("org/kylin/agentTest")) {
return classfileBuffer;
}
byte[] data = null;
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassAdapter ca = new AddTimeClassAdapter(cw);
cr.accept(ca, ClassReader.SKIP_DEBUG);
data = cw.toByteArray();
return data;
}
}
这里我们实现一个AopClassTransformer类,实现了ClassFileTransformer接口,该类中必须实现transform方法。我们在该方法中打印输出被transform的类。为了方便测试,我们只对稍后做测试的类,即代码中的agentTest类做修改,其他类则直接返回原来的字节码。
在修改类的过程中,用了ASM框架,这里使用了一个ASM中经典案例,
public class C {
public void m() throws Exception{
Thread.sleep(100);
}
}
即将上述代码,修改如下。
public class C {
public static long timer;
public void m() throws Exception{
timer -= System.currentTimeMillis();
Thread.sleep(100);
timer += System.currentTimeMillis();
System.out.println(timer);
}
}
这样,我们就能得到一个方法执行的时间了。关于ASM这部分以及上述代码中用到的AddTimeClassAdapter类,在下一篇文章,
ASM学习中详细介绍。
(3)打包agent
在打包agent的时候,需在jar的META-INF/MANIFEST.MF中加入Premain-Class,即我们这里的agent类。
下面是maven的pom.xml。
<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>org.kylin</groupId>
<artifactId>agent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>agent</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
<version>3.3.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>org.kylin.agent.Agent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
build之后我们会得到一个包agent-0.0.1-SNAPSHOT.jar。
(4)测试agent
另写一个工程测试agent。
package org.kylin;
public class agentTest {
public static void main(String[] args) throws Exception {
System.out.println("Agent Test...");
Thread.sleep(100);
}
}
运行时,在Run Configurations -> Arguments -> VM arguments里面添加-javaagent:xx/xx/agent-0.0.1-SNAPSHOT.jar,即agent包的全路径。
运行结果如下:
Agent is called...
Transforming Class: java/lang/invoke/MethodHandleImpl
Transforming Class: java/lang/invoke/MethodHandleImpl$1
Transforming Class: java/lang/invoke/MethodHandleImpl$2
Transforming Class: java/util/function/Function
Transforming Class: java/lang/invoke/MethodHandleImpl$3
Transforming Class: java/lang/invoke/MethodHandleImpl$4
Transforming Class: java/lang/ClassValue
Transforming Class: java/lang/ClassValue$Entry
Transforming Class: java/lang/ClassValue$Identity
Transforming Class: java/lang/ClassValue$Version
Transforming Class: java/lang/invoke/MemberName$Factory
Transforming Class: java/lang/invoke/MethodHandleStatics
Transforming Class: java/lang/invoke/MethodHandleStatics$1
Transforming Class: sun/misc/PostVMInitHook
Transforming Class: sun/launcher/LauncherHelper
Transforming Class: sun/misc/URLClassPath$FileLoader$1
Transforming Class: org/kylin/agentTest
Transforming Class: sun/launcher/LauncherHelper$FXHelper
Transforming Class: java/lang/Class$MethodArray
Transforming Class: java/lang/Void
Agent Test...
100
Transforming Class: java/lang/Shutdown
Transforming Class: java/lang/Shutdown$Lock
由输出结果可以看到,agent确实被执行了,且先于测试类agentTest的main方法,ASM修改类的字节码计算方法执行时间也实现了。
由此也可以看出,java instrument实现的监控代码和应用代码完全隔离了。