什么是Instrumentation?
查阅java api可知,
软件包 java.lang.instrument 的描述
提供允许 Java 编程语言代理监测运行在 JVM 上的程序的服务。监测的机制是对方法的字节码的修改。
包规范
在启动 JVM 时,通过指示代理类及其代理选项 启动一个代理程序。
该代理类必须实现公共的静态premain
方法,该方法原理上类似于 main
应用程序入口点:
public static void premain(String agentArgs, Instrumentation inst);
JVM 被初始化之后,每个premain
方法将按照指定代理的顺序被调用。然后,调用实际的应用程序 main
方法。每个 premain
方法必须按顺序返回,以便启动序列能够继续。代理类将由加载包含应用程序main
方法的类的同一类加载器加载。premain
方法将在与应用程序 main
方法相同的安全性和类加载器规则下运行。不存在代理premain
方法可以执行的建模限制。应用程序 main
可以执行的任何事情(包括生成线程)从 premain
来看都是合法的。
每个代理程序通过agentArgs
参数传递其代理选项。代理选项作为单个字符串传递,任何其他分析应由代理程序本身执行。
如果该代理程序不能被解析(例如,由于无法加载代理类,或由于代理类没有一致的premain
方法),则 JVM 将中止。如果 premain
方法抛出未捕获的异常,则 JVM 将中止。
启动命令行接口
在带有命令行接口的 JVM 上,通过向 JVM 命令行添加此开关来指定代理程序:
-javaagent:
jarpath[=
options]
jarpath 是指向代理程序 JAR 文件的路径。options 是代理选项。此开关可以在同一命令行上多次使用,从而创建多个代理程序。多个代理程序可以使用同一jarpath。代理 JAR 文件必须符合 JAR 文件规范。下面的清单属性是针对代理 JAR 文件定义的:
Premain-Class
代理类。即包含
premain
方法的类。此属性是必需的,如果它不存在,JVM 将中止。注:这是类名,而不是文件名或路径。
Boot-Class-Path
由引导类加载器搜索的路径列表。路径表示目录或库(在许多平台上通常作为 jar 或 zip 库被引用)。查找类的特定于平台的机制出现故障之后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件的语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。此属性是可选的。
Can-Redefine-Classes
布尔值(
true
或false
,与大小写无关)。能够重定义此代理所需的类。值如果不是true
,则被认为是false
。此属性是可选的,默认值为false
。
代理 JAR 文件附加到类路径之后。
Java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。 java SE5中使用JVM TI替代了JVM PI和JVM DI。提供一套代理机制,支持独立于JVM应用程序之外的程序以代理的方式连接和访问JVM。Java.lang.instrument是在JVM TI的基础上提供的Java版本的实现。 Instrumentation提供的主要功能是修改jvm中类的行为。 Java SE6中由两种应用Instrumentation的方式,premain(命令行)和agentmain(运行时)
premain
在Java SE5时代,Instrument只提供了premain一种方式,即在真正的应用程序(包含main方法的程序)main方法启动前启动一个代理程序。例如使用如下命令:
java -javaagent:agent_jar_path[=options] java_app_name
可以在启动名为java_app_name的应用之前启动一个agent_jar_path指定位置的agent jar。 实现这样一个agent jar包,必须满足两个条件:
- 在这个jar包的manifest文件中包含Premain-Class属性,并且改属性的值为代理类全路径名。
- 代理类必须提供一个public static void premain(String args, Instrumentation inst)或 public static void premain(String args) 方法。
当在命令行启动该代理jar时,VM会根据manifest中指定的代理类,使用于main类相同的系统类加载器(即ClassLoader.getSystemClassLoader()获得的加载器)加载代理类。在执行main方法前执行premain()方法。如果premain(String args, Instrumentation inst)和premain(String args)同时存在时,优先使用前者。其中方法参数args即命令中的options,类型为String(注意不是String[]),因此如果需要多个参数,需要在方法中自己处理(比如用";"分割多个参数之类);inst是运行时由VM自动传入的Instrumentation实例,可以用于获取VM信息。
premain实例-打印所有的方法调用
下面实现一个打印程序执行过程中所有方法调用的功能,这个功能可以通过AOP其他方式实现,这里只是尝试使用Instrumentation进行ClassFile的字节码转换实现:
构造agent类
premain方式的agent类必须提供premain方法,代码如下:
package test;
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String args, Instrumentation inst){
System.out.println("Hi, I'm agent!");
inst.addTransformer(new TestTransformer());
}
}
premain有两个参数,args为自定义传入的代理类参数,inst为VM自动传入的Instrumentation实例。 premain方法的内容很简单,除了标准输出外,只有
inst.addTransformer(new TestTransformer());
这行代码的意思是向inst中添加一个类的转换器。用于转换类的行为。
构造Transformer
下面来实现上述过程中的TestTransformer来完成打印调用方法的类定义转换。
package test;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
public class TestTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader arg0, String arg1, Class<?> arg2,
ProtectionDomain arg3, byte[] arg4)
throws IllegalClassFormatException {
ClassReader cr = new ClassReader(arg4);
ClassNode cn = new ClassNode();
cr.accept(cn, 0);
for (Object obj : cn.methods) {
MethodNode md = (MethodNode) obj;
if ("<init>".endsWith(md.name) || "<clinit>".equals(md.name)) {
continue;
}
InsnList insns = md.instructions;
InsnList il = new InsnList();
il.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System",
"out", "Ljava/io/PrintStream;"));
il.add(new LdcInsnNode("Enter method-> " + cn.name+"."+md.name));
il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream", "println", "(Ljava/lang/String;)V"));
insns.insert(il);
md.maxStack += 3;
}
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
return cw.toByteArray();
}
}
TestTransformer实现了ClassFileTransformer接口,该接口只有一个transform方法,参数传入包括该类的类加载器,类名,原字节码字节流等,返回被转换后的字节码字节流。 TestTransformer主要使用ASM实现在所有的类定义的方法中,在方法开始出添加了一段打印该类名和方法名的字节码。在转换完成后返回新的字节码字节流。详细的ASM使用请参考ASM手册。
设置MANIFEST.MF
设置MANIFEST.MF文件中的属性,文件内容如下:
Manifest-Version: 1.0
Premain-Class: test.Agent
Created-By: 1.6.0_29
测试
代码编写完成后将代码编译打成agent.jar。 编写测试代码:
public class TestAgent {
public static void main(String[] args) {
TestAgent ta = new TestAgent();
ta.test();
}
public void test() {
System.out.println("I'm TestAgent");
}
}
从命令行执行该类,并设置agent.jar
java -javaagent:agent.jar TestAgent
将打印出程序运行过程中实际执行过的所有方法名:
Hi, I'm agent!
Enter method-> test/TestAgent.main
Enter method-> test/TestAgent.test
I'm TestAgent
Enter method-> java/util/IdentityHashMap$KeySet.iterator
Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext
Enter method-> java/util/IdentityHashMap$KeyIterator.next
Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.nextIndex
Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext
Enter method-> java/util/IdentityHashMap$KeySet.iterator
Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext
Enter method-> java/util/IdentityHashMap$KeyIterator.next
Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.nextIndex
Enter method-> com/apple/java/Usage$3.run
。。。
从输出中可以看出,程序首先执行的是代理类中的premain方法(不过代理类自身不会被自己转换,所以不能打印出代理类的方法名),然后是应用程序中的main方法。