转自:http://jiangbo.me/blog/2012/02/21/java-lang-instrument/ html
Instrumentation介绍: java
java Instrumentation指的是能够用独立于应用程序以外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。 Java SE5中使用JVM TI替代了JVM PI和JVM DI。提供一套代理机制,支持独立于JVM应用程序以外的程序以代理的方式链接和访问JVM。Instrumentation 的最大做用就是类定义的动态改变和操做。在 Java SE 5 及其后续版本当中,开发者能够在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,经过 – javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。 web
premain方式
在Java SE5时代,Instrument只提供了premain一种方式,即在真正的应用程序(包含main方法的程序)main方法启动前启动一个代理程序。例如使用以下命令:
api
java -javaagent:agent_jar_path[=options] java_app_name
premain实例-打印全部的方法调用
下面实现一个打印程序执行过程当中全部方法调用的功能,这个功能能够经过AOP其余方式实现,这里只是尝试使用Instrumentation进行ClassFile的字节码转换实现:
构造agent类
premain方式的agent类必须提供premain方法,代码以下:
oracle
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中添加一个类的转换器。用于转换类的行为。
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(); } }
设置MANIFEST.MF
设置MANIFEST.MF文件中的属性,文件内容以下:
app
Manifest-Version: 1.0 Premain-Class: test.Agent Created-By: 1.6.0_29测试
public class TestAgent { public static void main(String[] args) { TestAgent ta = new TestAgent(); ta.test(); } public void test() { System.out.println("I'm TestAgent"); } }
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方法。 ide
premain时Java SE5开始就提供的代理方式,给了开发者诸多惊喜,不过也有些须不变,因为其必须在命令行指定代理jar,而且代理类必须在main方法前启动。所以,要求开发者在应用前就必须确认代理的处理逻辑和参数内容等等,在有些场合下,这是比较苦难的。好比正常的生产环境下,通常不会开启代理功能,可是在发生问题时,咱们不但愿中止应用就可以动态的去修改一些类的行为,以帮助排查问题,这在应用启动前是没法肯定的。 为解决运行时启动代理类的问题,Java SE6开始,提供了在应用程序的VM启动后在动态添加代理的方式,即agentmain方式。 与Permain相似,agent方式一样须要提供一个agent jar,而且这个jar须要知足: 函数
不过如此设计的再运行时进行代理有个问题——如何在应用程序启动以后再开启代理程序呢? JDK6中提供了Java Tools API,其中Attach API能够知足这个需求。 测试
Attach API中的VirtualMachine表明一个运行中的VM。其提供了loadAgent()方法,能够在运行时动态加载一个代理jar。具体须要参考《Attach API》 spa
agentmain方式的代理类必须提供agentmain方法:
package loaded; import java.lang.instrument.Instrumentation; public class LoadedAgent { @SuppressWarnings("rawtypes") public static void agentmain(String args, Instrumentation inst){ Class[] classes = inst.getAllLoadedClasses(); for(Class cls :classes){ System.out.println(cls.getName()); } } }
agentmain方法经过传入的Instrumentation实例获取当前系统中已加载的类。
设置MANIFEST.MF文件,指定Agent-Class:
Manifest-Version: 1.0 Agent-Class: loaded.LoadedAgent Created-By: 1.6.0_29
将agent类和MANIFEST.MF文件编译打成loadagent.jar后,因为agent main方式没法向pre main方式那样在命令行指定代理jar,所以须要借助Attach Tools API。
package attach; import java.io.IOException; import com.sun.tools.attach.AgentInitializationException; import com.sun.tools.attach.AgentLoadException; import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine; public class Test { public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException { VirtualMachine vm = VirtualMachine.attach(args[0]); vm.loadAgent("/Users/jiangbo/Workspace/code/java/javaagent/loadagent.jar"); } }
该程序接受一个参数为目标应用程序的进程id,经过Attach Tools API的VirtualMachine.attach方法绑定到目标VM,并向其中加载代理jar。
构造一个测试用的目标应用程序:
package attach; public class TargetVM { public static void main(String[] args) throws InterruptedException{ while(true){ Thread.sleep(1000); } } }
这个测试程序什么都不作,只是不停的sleep。:) 运行该程序,得到进程ID=33902。 运行上面绑定到VM的Test程序,将进程id做为参数传入:
java attach.Test 33902
观察输出,会打印出系统当前全部已经加载类名
java.lang.NoClassDefFoundError java.lang.StrictMath java.security.SignatureSpi java.lang.Runtime java.util.Hashtable$EmptyEnumerator sun.security.pkcs.PKCS7 java.lang.InterruptedException java.io.FileDescriptor$1 java.nio.HeapByteBuffer java.lang.ThreadGroup [Ljava.lang.ThreadGroup; java.io.FileSystem 。。。
当一个代理jar包中的manifest文件中既有Premain-Class又有Agent-Class时,若是以命令行方式在VM启动前指定代理jar,则使用Premain-Class;反之若是在VM启动后,动态添加代理jar,则使用Agent-Class