在前面的一些例子中,已经用到了jdk5中premain和instrumentation的一些基本用法,给了我们很多惊喜,相当的给力。不过也有一些不方便的地方,由于其必须在命令行指定代理jar,并且代理类必须在main方法前启动。因此,要求开发者在应用前就必须确认代理的处理逻辑和参数内容等等,在有些场合下,这是比较困难的。比如正常的生产环境下,一般不会开启代理功能,但是在发生问题时,我们不希望停止应用就能够动态的去修改一些类的行为,以帮助排查问题,这在应用启动前是无法确定的。为解决运行时启动代理类的问题,JavaSE6开始,提供了在应用程序的VM启动后在动态添加代理的方式,即agentmain方式。与Permain类似,agent方式同样需要提供一个agentjar,并且这个jar需要满足:
在manifest中指定Agent-Class属性,值为代理类全路径
代理类需要提供publicstaticvoidagentmain(Stringargs,Instrumentationinst)或publicstaticvoidagentmain(Stringargs)方法。并且再二者同时存在时以前者优先。args和inst和premain中的一致。
假如上面的agent的jar包打好了,那面临另外一个问题——如何在应用程序启动之后再开启代理程序呢?JDK6中提供了JavaToolsAPI,其中AttachAPI可以满足这个需求。
在AttachAPI中的VirtualMachine代表一个运行中的VM。其提供了loadAgent()方法,可以在运行时动态加载一个代理jar。
AttachAPI在%java_home%\lib这个目录下的tool.jar包中。我是将它打在工程的lib包中的。
AttactAPI主要在这个目录下,也就这些东西,如图:
最主要的类就是VirtualMachine,下面写个例子熟悉下它。
这个例子做的事情很简单:
1、一个VMTest类没1秒钟输出一行:System.out.println("wait.....");
2、写一个MyAgentMain类,这个类实现了
publicstaticvoidagentmain(Stringargs,Instrumentationinst)方法,在这个方法中打印出所有已经装在的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,通过AttachToolsAPI的VirtualMachine.attach方法绑定到目标VM,并向其中加载代理jar。13712就是刚才我运行VMTest的进程pid。
运行VirtualMachineTest可以看到运行结果:
打印出了所有已经加载的class。
同样的,你也可以通过instrumentation的inst.addTransformer()方法加入ClassFileTransformer的实现类,然后对指定的class进行修改。
但是在运行时对class修改和premain的方式对class的修改是有点差别的,至少在jdk6以及以前的版本是这样的。
通过premain的方式在应用启动前对class可以做任何修改,可以添加class,删除class,添加删除修改方法体,添加成员变量等等没有任何限制,
然而通过agentmain的方式在运行时修改class是有限制的,,比如在class已经被加载过的情况下,是不能对class添加,删除方法的,只能重定义方法体,这对很多功能其实做了限制。相信jdk6以后的版本应该会放开这个限制吧。
但是我个人感觉,通过premain这种方式在vm启动前修改好class应该基本满足我们的需要了,如果一定要在运行时添加方法,删除方法等,也一定有方法可以绕开jdk6instrumentation的这个限制。会找到办法的。。。。