前言
结合上一篇文章[Java 灰盒测试实战经验分享二之 Java Instrumentation 新功能(JAVA SE 5)]主要讲解了如何使用Java的Instrumentation特性(Java SE 5提出)实现对Java程序中main函数启动前的修改。
这一功能(或特性)可以灵活运用于如灰盒测试在内的不修改主程序而达到故障注入的框架设计中。
不过,还是存在一定的局限性。即,需要在java程序(Application)启动之前预先定义好启动脚本(如Manifast中定义Premain-Class),在进行程序的运行,方能达到外界修改main函数的效果。
这个局限性导致不能在Java程序运行过程中实现动态代理。
所以,到了Java SE 6时代,提供了新的特性 - 虚拟机启动后的动态Instrument。
Java 虚拟机(JVM)启动后的动态Instrument
在Java SE 5当中,开发者只能在premain当中施展想象力,所做的Instrumentation也仅限于main函数执行之前,这样的方法存在一定的局限性。
在Java SE 5的基础上,Java SE 6针对这种状况做出了改进,开发者可以在main函数开始执行后,再启动自己的Instrumentation程序。
在Java SE 6的Instrumentation当中,有一个跟premain称得上“并驾齐驱”的“agentmain”方法,可以再main函数开始运行之后再运行。
跟premain函数一样,程序员们可以编写一个含有“agentmain”函数的Java类:
public static void agentmain (String agentArgs, Instrumentation inst); [1]
public static void agentmain (String agentArgs); [2]
同样,[1]的优先级高于[2],将会被优先执行。
跟premain函数一样,开发者可以再agentmain中进行对类的各种操作。其中的agentArgs和Inst的作用跟premain一模一样。
与“Premain-Class”类似,开发者必须在manifest文件里面设置“Agent-Class”来指定包含agentmain函数的类。
可是,跟premain不同的是,agentmain需要在main函数开始运行后才启动,这样的时机该如何确定是好呢,这样的功能又如何实现是好呢?
在Java SE 6文档当中,开发者也许无法在java.lang.instrument包相关的文档部分看到明确的介绍,更无法看到具体的应用agentmain的例子。不过,在Java SE 6的新特性里面,有一个不起眼的地方,揭示了agentmain的用法。
这就是Java SE 6当中的Attach API。
Attach API不是Java标准的API,而是Sun公司提供的一套扩展API。用来向目标JVM“附着(Attach)”代理工具程序的。有了它,开发者可以方便监控一个JVM,运行一个外加的代理程序。
Attach API很简单,只有2个主要的类,都在com.sun.tools.attach包里面:VirtualMachine代表一个Java虚拟机,也就是程序需要监控的目标虚拟机,提供了JVM枚举,Attach动作和Detach动作(Attach动作的相反 行为,从JVM上面解除一个代理)等等。
VirtualMachineDescriptor则是一个描述虚拟机的容器类,配合VirtualMachine类完成各种功能。
为了很简单起见,我们举例简化如下:依然用类文件替换的方法,将一个返回1的函数替换成返回2的函数。Attach API卸载一个线程里面,用睡眠等待的方式,每隔半秒时间检查一次所有的Java虚拟机,当发现有新的虚拟机出现的时候,就调用attach函数,随后再按照Attach API文档里面所描述的方式装在Jar文件。等到5秒的时候,attach程序自动结束。而在main函数里,程序每隔半秒就会输出一次返回值(显示出返回值从1变成2)。
TransClass类和T按时former类的代码不变,参照上一篇文章介绍。含有main函数的TestMainInJar代码为:
public class TestMainInJar {
public static void main(String[] args) throws InterruptedException {
System.out.println(new TransClass().getNumber());
int count = 0;
while (true) {
Thread.sleep(500);
count++;
int number = new TransClass().getNumber();
System.out.println(number);
if (3 == number || count >= 10) {
break;
}
}
}
}
含有agentmain的AgentMain类的代码为:
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst)
throws ClassNotFoundException, UnmodifiableClassException,
InterruptedException {
inst.addTransformer(new Transformer (), true);
inst.retransformClasses(TransClass.class);
System.out.println("Agent Main Done");
}
}
其中,retransformClasses是Java SE 6里面的新方法,它跟redefineClasses一样,可以批量转换类定义,多用于agentmain场合。
Jar文件跟Premain那个例子里面的Jar文件差不多,也是把main和agentmain的类,TransClass,Transformer等类放在一起,打包为“TestInstrument1.jar”,而Jar文件当中的Manifest文件为:
Manifest-Version: 1.0
Agent-Class: AgentMain
另外,为了运行Attach API,我们可以再洗诶个控制程序来模拟监控过程:
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
……
// 一个运行 Attach API 的线程子类
static class AttachThread extends Thread {
private final List<VirtualMachineDescriptor> listBefore;
private final String jar;
AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) {
listBefore = vms; // 记录程序启动时的 VM 集合
jar = attachJar;
}
public void run() {
VirtualMachine vm = null;
List<VirtualMachineDescriptor> listAfter = null;
try {
int count = 0;
while (true) {
listAfter = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : listAfter) {
if (!listBefore.contains(vmd)) {
// 如果 VM 有增加,我们就认为是被监控的 VM 启动了
// 这时,我们开始监控这个 VM
vm = VirtualMachine.attach(vmd);
break;
}
}
Thread.sleep(500);
count++;
if (null != vm || count >= 10) {
break;
}
}
vm.loadAgent(jar);
vm.detach();
} catch (Exception e) {
ignore
}
}
}
……
public static void main(String[] args) throws InterruptedException {
new AttachThread("TestInstrument1.jar", VirtualMachine.list()).start();
}
运行时,可以首先运行上面这个启动新线程的main函数,然后,在5秒钟内,运行如下命令,启动测试Jar文件:
java – javaagent:TestInstrument2.jar – cp TestInstrument2.jar TestMainInJar
如果时间掌握的不太差的话,程序首先会在控制台输出1,这是改动前的类的输出,然后会打印出一些2,这表示agentmain已经被Attach API成功附着到JVM上,代理程序生效。
当然,还可以看到“Agent Main Done”字样的输出。
以上例子,仅仅只是简单实示例,简单说明这个特性。真实的例子往往比较复杂,而且可能运行在分布式环境中的多个JVM之中。