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

来源:互联网 发布:旋风十一人 知乎 编辑:程序博客网 时间:2024/06/13 22:04
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就是这样一款工具。

原创粉丝点击