在前面的一些例子中,已经用到了jdk5中premain和instrumentation的一些基本用法,给了我们很多惊喜,相当的给力。不过也有一些不方便的地方,由于其必须在命令行指定代理jar,并且代理类必须在main方法前启动。因此,要求开发者在应用前就必须确认代理的处理逻辑和参数内容等等,在有些场合下,这是比较困难的。比如正常的生产环境下,一般不会开启代理功能,但是在发生问题时,我们不希望停止应用就能够动态的去修改一些类的行为,以帮助排查问题,这在应用启动前是无法确定的。 为解决运行时启动代理类的问题,Java SE6开始,提供了在应用程序的VM启动后在动态添加代理的方式,即agentmain方式。 与Permain类似,agent方式同样需要提供一个agent jar,并且这个jar需要满足:
在manifest中指定Agent-Class属性,值为代理类全路径
代理类需要提供public static void agentmain(String args, Instrumentation inst)或public static void agentmain(String args)方法。并且再二者同时存在时以前者优先。args和inst和premain中的一致。
假如上面的agent的jar包打好了,那面临另外一个问题——如何在应用程序启动之后再开启代理程序呢? JDK6中提供了Java Tools API,其中Attach API可以满足这个需求。
在Attach API中的VirtualMachine代表一个运行中的VM。其提供了loadAgent()方法,可以在运行时动态加载一个代理jar。
Attach API在%java_home%\lib这个目录下的tool.jar包中。我是将它打在工程的lib包中的。
Attact API主要在这个目录下,也就这些东西,如图:
最主要的类就是VirtualMachine,下面写个例子熟悉下它。
这个例子做的事情很简单:
1、一个VMTest类没1秒钟输出一行:System.out.println("wait.....");
2、写一个MyAgentMain类,这个类实现了
public static void agentmain(Stringargs, Instrumentation inst) 方法,在这个方法中打印出所有已经装在的class,其他什么也不做(也可以像先前讲解premin方法中的加入方法耗时统计)。
下面是具体的实现:
首先是具体的应用类:
/** * TODO Comment of VMTest * @author yongkang.qiyk */ public class VMTest { public static void main(String[] args) throwsInterruptedException { while(true){ Thread.sleep(10000); new Thread(new WaitThread()).start(); } } static class WaitThread implements Runnable { @Override public void run() { try { WaitTest w = new WaitTest(); w.waiting(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
这个是首先运行的类,运行后,每10秒打印一次 System.out.println("wait.....");
然后编写我们的运行时要加载的agent类:
public class MyAgentMain { public static void agentmain(String args, Instrumentation inst)throws UnmodifiableClassException{ System.out.println("MyAgentMain agentmain attach..."); System.getProperties().setProperty("monitor.conf", args); for (Class clazz :inst.getAllLoadedClasses()){ System.out.println(clazz.getName()); } // inst.addTransformer(newMonitorAgentTransformer(),true); // inst.retransformClasses(WaitTest.class); System.out.println("MyAgentMain agentmain end..."); } }
可以看到,这个agent类只是打印了加载的类,以及运行时设置了一个系统属性,这个系统属性是监控配置的文件路径,在这个例子中我没有用到它。
上面的写好之后,记得打成jar包,。注意:METAINF.MF的中记得加入Agent-Class:这个属性:
Manifest-Version: 1.0
Premain-Class: monitor.agent.MyAgent
Agent-Class: monitor.agentmain.MyAgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes:true
Boot-Class-Path: javassist.jar
打包好monitor.jar的包之后,写一个在运行时加载代理的实现:
public class VirtualMachineTest { public static void main(String[] args) throwsAttachNotSupportedException, IOException, AgentLoadException,AgentInitializationException, InterruptedException { // attach to target VM VirtualMachine vm = VirtualMachine.attach("13712"); vm.loadAgent("D:/tools/java/monitor.jar", "D:/tools/java/profile.txt"); Thread.sleep(1000); vm.detach(); } }
可以这个类做的事情也很简单,该程序接受一个参数为目标应用程序的进程id,通过Attach Tools API的VirtualMachine.attach方法绑定到目标VM,并向其中加载代理jar。13712就是刚才我运行VMTest的进程pid。
运行VirtualMachineTest 可以看到运行结果:
同样的,你也可以通过instrumentation的 inst.addTransformer()方法加入ClassFileTransformer的实现类,然后对指定的class进行修改。
但是在运行时对class修改和premain的方式对class的修改是有点差别的,至少在jdk6以及以前的版本是这样的。
通过premain的方式在应用启动前对class可以做任何修改,可以添加class,删除class,添加删除修改方法体,添加成员变量等等没有任何限制,
然而通过agentmain的方式在运行时修改class是有限制的,,比如在class已经被加载过的情况下,是不能对class添加,删除方法的,只能重定义方法体,这对很多功能其实做了限制。相信jdk6以后的版本应该会放开这个限制吧。
但是我个人感觉,通过premain这种方式在vm启动前修改好class应该基本满足我们的需要了,如果一定要在运行时添加方法,删除方法等,也一定有方法可以绕开jdk6 instrumentation的这个限制。会找到办法的。。。。
参考文档:http://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html
http://www.jar114.com/jdk6/zh_CN/api/java/lang/instrument/package-summary.html