http://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html
在 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-Class”类似,开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。
可是,跟 premain 不同的是,agentmain 需要在 main 函数开始运行后才启动,这样的时机应该如何确定呢,这样的功能又如何实现呢?
在 Java SE 6 文档当中,开发者也许无法在 java.lang.instrument 包相关的文档部分看到明确的介绍,更加无法看到具体的应用 agnetmain 的例子。不过,在 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 类完成各种功能。
业务类:
package agentmain;
public class TransClass {
public int getNumber() {
return 0;
}
}
使用业务类:
package agentmain;
public class TestMainInJar {
public static void main(String[] args) throws InterruptedException {
System.out.println(new TransClass().getNumber());
while (true) {
Thread.sleep(2000);
int number = new TransClass().getNumber();
System.out.println(number);
}
}
}
PS:如果业务类发生变化,使用者打印会感知到。
下面是 instrument 相关的类:
动态Agent:
package agentmain;
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 {
System.out.println("Agent Main Done");
inst.addTransformer(new Transformer(), true);
inst.retransformClasses(TransClass.class);
}
}
业务转换类:
package agentmain;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class Transformer implements ClassFileTransformer {
public static final String classNumberReturns2 = "/home/conquer/Desktop/aaaa/TransClass.class";
public static byte[] getBytesFromFile(String fileName) {
try {
System.out.println(System.getProperty("user.home"));
// precondition
File file = new File(fileName);
InputStream is = new FileInputStream(file);
long length = file.length();
byte[] bytes = new byte[(int) length];
// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset < bytes.length
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
if (offset < bytes.length) {
throw new IOException("Could not completely read file "
+ file.getName());
}
is.close();
return bytes;
} catch (Exception e) {
System.out.println("error occurs in _ClassTransformer!"
+ e.getClass().getName());
return null;
}
}
public byte[] transform(ClassLoader l, String className, Class> c,
ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
System.out.println(className);
if (!className.contains("TransClass")) {
System.out.println("no");
return null;
} else {
System.out.println("yes");
}
return getBytesFromFile(classNumberReturns2);
}
}
业务转换类会从 /home/conquer/Desktop/aaaa/TransClass.class 这个位置加载一个新的类以替换原有的业务类,注意这个类必需和原有业务类全名称相同,测试过程可以将返回值改成2或其它,以区分发生了变化,让业务使用类打印出一个新值。
将上面两个类达成jar包,如 agentmain.jar,并添加:META-INF/MANIFEST.MF,内容:
Manifest-Version: 1.0
Agent-Class: agentmain.AgentMain
好了,开始测试,先运行TestMainInJar循环打印当前的业务方法返回值,然后运行测试类:
package agentmain;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class AttachTest extends Thread {
private final List listBefore;
private final String jar;
AttachTest(String attachJar, List vms) {
listBefore = vms; // 记录程序启动时的 VM 集合
jar = attachJar;
}
public void run() {
VirtualMachine vm = null;
List 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) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
// new AttachThread("TestInstrument1.jar", VirtualMachine.list()).run();
// VirtualMachine attach = VirtualMachine.attach("5741");
// System.out.println(attach.id());
for (VirtualMachineDescriptor vmd : VirtualMachine.list()) {
System.out.println(vmd);
if (vmd.displayName().contains("TestMainInJar")) {
VirtualMachine vm = VirtualMachine.attach(vmd);
vm.loadAgent("/home/conquer/Desktop/aaaa/agentmain.jar");
System.out.println("loaded");
vm.detach();
System.out.println("detached");
break;
}
}
}
}
这样就能看到打印结果在运行agent之后由0变成2,类字节码被成功替换了。
测试使用用例的时候我们是通过每次new一个新的对象来观察修改结果,其实不是必需new的,因为方法的执行序列是存储在方法区(class文件的定义区),无论使用新的实例还是旧的(用旧的class创建的)实例都是可以感知到class的变化的,如,测试用例可以修改为:
package agentmain;
public class TestMainInJar {
static TransClass transClass = new TransClass();
public static void main(String[] args) throws InterruptedException {
while (true) {
Thread.sleep(2000);
System.out.println(transClass + " ==: " + transClass.getNumber());
}
}
}
以上,我们将TransClass定义为一个全局变量,循环调用打印一个旧实例的方法返回值,同样可以看到class替换后的结果。(PS:如果对jvm内部理解透彻的话是很好理解的,实例在内存中只是保存了区别于其它实例的属性或成员变量,其方法的执行序列依然走的是class的定义)
相对于jdk5只能通过启动脚本添加javaagent的方式植入代理,jdk6的动态attach也只是免去了修改启动脚本和不用重启的工作,并没有添加其它新的特性,即使不使用动态attach而是使用脚本添加javaagent的方式也可以达到随时修改class定义的目的,无论通过什么方式我们只要获取了 Instrumentation 实例,然后调用其addTransformer方法添加类转换器再调用retransformClasses就可以转换一个类的字节序列了,这里需要注意的是retransformClasses是jdk1.6定义的,jdk1.5只能使用redefineClasses,retransformClasses功能强大使用简单,但有不能修改方法签名,只能修改body等约束,redefineClasses则是一个可定细节制化的选择。
大名鼎鼎的Btrace就是基于jdk6实现的,它使用到了jdk6的动态attach(非脚本模式),同时使用到了jdk6提供的retransformClasses,其实要方便地操作字节码最好还是基于JDK6做吧。