1 instrument
instrument是jdk 1.5之后提供的一个功能,它通过代理的方式运行在 JVM 上的程序的服务。作为代理的类必须首先打成jar包。在jdk1.6中支持两种方式来启动代理:
(1) 在程序启动的时候添加-javaagent:jarpath=options参数指定代理的jar来启动代理,这种情况下
代理入口类通过在META-INF/MENIFEST.MF清单文件中的Premain-Class属性指定,代理入口类必须实现以下两个函数之一(两个方法同时存在时优先调用第一个):
public static void premain(String agentArgs, Instrumentation inst);
或
public static void premain(String agentArgs);
(2)当目标程序已经在运行了,这种情况就只能采用第二种方式了。
首先必须在META-INF/MENIFEST.MF清单文件中通过Agent-Class属性来指定代理入口类。同样,代理入口类必须是实现以下两个函数之一(两个方法同时存在时优先调用第一个):
public static void agentmain(String agentArgs, Instrumentation inst);
或
public static void agentmain(String agentArgs);
具体内容可以参考http://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html中的描述
这里需要介绍一下Instrumentation类,此类提供检测 Java 编程语言代码所需的服务。检测是向方法中添加字节码,以搜集各种工具所使用的数据。通过它,我们才能操作JVM,并修改Class中的一些内容。
这个类中有两个addTransformer方法,
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)
该方法向Instrumentation注册一个转换类文件转换器。
ClassFileTransformer是一个接口,提供了一个
byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException
该方法实现可以转换提供的类文件,并返回一个新的替换类文件。若不想替换类文件,可以返回null。
第二种方式显然已经不能再通过java命令的启动参数来指定了,这里就需要用到Attach API
2 Attach API
The Attach API 这篇文章中介绍得很详细了。
3.javassist
javassist是一个修改字节码创建Java字节码的类库。比asm工具容易上手。
4.动态改变类的行为
在运行的程序中出现问题时,有时候我们不希望停止程序来排查问题,这个时候可以通过动态的改变运行中的某些类的行为,或者打印某个值来排查。这个时候就需要我们能在程序运行过程中动态的改变类的行为。通过上面三种工具的结合,可以解决这个问题。
首先我们给出运行的目标程序:
public class HelloServiceImpl{
@Override
public void sayHello() {
System.out.println("hello");
}
}
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
HelloServiceImpl service = new HelloServiceImpl();
while (true) {
service.sayHello();
try {
synchronized (service) {
service.wait(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里Client每隔3秒钟调用一次sayHello方法。输出结果为hello.
现在想在hello输出前再输出方法的名字。
agent:
public class MyAgent {
public static String className="com.alibaba.study.thread.HelloServiceImpl";
public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException {
Class[] allClass = inst.getAllLoadedClasses();
for (Class c : allClass) {
if(c.getName().equals(className)){
System.out.println("agent loaded");
inst.addTransformer(new DynamicClassTransformer(), true);
inst.retransformClasses(c);
}
}
}
}
public class DynamicClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
try {
CtClass ctClass = ClassPool.getDefault().get("com.alibaba.study.thread.HelloServiceImpl");
String methodName = "sayHello";
CtMethod ctMethod=ctClass.getDeclaredMethod(methodName);
System.out.println(ctMethod.getName());
ctMethod.insertBefore("System.out.println(\" sayHello\");");
ctClass.writeFile();
return ctClass.toBytecode();
} catch (Exception e) {
System.out.println(e.getMessage());
}
return null;
}
}
然后在配置清单中指定Agent-Class属性为MyAgent,并打成jar包。
将agent attach到指定的java虚拟机进程上。
public class AttachMain {
public static void main(String args[]) throws AttachNotSupportedException{
VirtualMachine vm;
List<VirtualMachineDescriptor> vmList= VirtualMachine.list();
if(vmList!=null){
for(int i=0;i<vmList.size();i++){
System.out.println("["+i+"] "+vmList.get(i).displayName()+" ,id:"+vmList.get(i).id()+" ,provider:"+vmList.get(i).provider());
}
try{
int num=System.in.read()-48;
if(num!=-1&&num<vmList.size()){
vm= VirtualMachine.attach(vmList.get(num));
vm.loadAgent("/home/tanfeng/myagent.jar");
System.in.read();
}
}catch(Exception e){
e.printStackTrace();
}
}
}
}
这样,当agent被加载之后,就可以看到在输出hello之前,会输出sayHello这个字符串。
这种功能相当强大,我们可以用它来做些其它用途。
当然我们没有必要自己去写这些程序了,因为已经有人帮我们做了,Btrace就是这样一款工具。