利用instrument+Attach API+javassist动态改变方法逻辑

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 vmList= VirtualMachine.list();
        if(vmList!=null){
        for(int i=0;i

这样,当agent被加载之后,就可以看到在输出hello之前,会输出sayHello这个字符串。
这种功能相当强大,我们可以用它来做些其它用途。
当然我们没有必要自己去写这些程序了,因为已经有人帮我们做了,Btrace就是这样一款工具。

你可能感兴趣的:(java)