使用javassist 无侵入实现方法时间统计

使用javassist 无侵入实现方法时间统计

在JAVA 语言中 我们知道最终JVM执行的是字节码文件,那么 改变字节码指令 其实就是修改了代码执行逻辑.
今天我们就来介绍下 java中操作字节码的工具 javasst


javassist

一种简单易用操作字节码的工具类 —— [ 官方网站 ]

修改类中的实例方法

public class Student {

    private int age;

    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String toString(){
        return "student";
    }

}

通过javap 查看下 生成的字节码

javap -v Student
 public java.lang.String toString();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #31                 // String student
         2: areturn
      LineNumberTable:
        line 26: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lsun/cn/demo/Student;

通过javassist api修改toString 方法

public class StudentByteCode {

    public void modifyByteCodeMethod() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        /** 获取类信息  */
        CtClass cls = pool.get(Student.class.getCanonicalName());
        /** 获取方法描述信息 */
        CtMethod toStringMethod = cls.getDeclaredMethod("toString");
        toStringMethod.insertBefore("System.out.println(\"toString before\");");
        toStringMethod.insertAfter("System.out.println(\"toString after\");");
        cls.writeFile(Student.class.getResource("../../../").getPath());
    }

    public static void main(String[] args) throws Exception {
        new StudentByteCode().modifyByteCodeMethod();
        Student s = new Student();
        s.toString();
    }
}

在来看下 生成的字节码

public java.lang.String toString();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=3, args_size=1
         0: getstatic     #40                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #42                 // String toString before
         5: invokevirtual #47                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: ldc           #31                 // String student
        10: goto          13
        13: astore_2
        14: getstatic     #40                 // Field java/lang/System.out:Ljava/io/PrintStream;
        17: ldc           #49                 // String toString after
        19: invokevirtual #47                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        22: aload_2
        23: areturn
      LineNumberTable:
        line 26: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Lsun/cn/demo/Student;
      StackMapTable: number_of_entries = 1
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/String ]

可以明显看到 字节码已经变更了我们在执行对象的toString方法

        Student s = (Student)Student.class.getClassLoader().loadClass(Student.class.getCanonicalName()).newInstance();
        System.out.println(s.toString());
toString before
toString after
student

但是 使用如下 方式运行 就得不到如上效果:

public class StudentByteCode {

    public void modifyByteCodeMethod() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        /** 获取类信息  */
        CtClass cls = pool.get(Student.class.getCanonicalName());
        /** 获取方法描述信息 */
        CtMethod toStringMethod = cls.getDeclaredMethod("toString");
        toStringMethod.insertBefore("System.out.println(\"toString before\");");
        toStringMethod.insertAfter("System.out.println(\"toString after\");");
        cls.writeFile(Student.class.getResource("../../../").getPath());
    }

    public static void main(String[] args) throws Exception {
        Student s1 = (Student)Student.class.getClassLoader().loadClass(Student.class.getCanonicalName()).newInstance();
        System.out.println(s1.toString());
        new StudentByteCode().modifyByteCodeMethod();
        Student s = (Student)Student.class.getClassLoader().loadClass(Student.class.getCanonicalName()).newInstance();
        System.out.println(s.toString());
    }
}
student
student

这是为什么呢?
因为 我们的Student 类描述信息是通过 classloader 加载的.而class改变了并不会使classloader 重新加载.
当然 JVM 有类卸载参数 比如 CMS收集器
-XX:+CMSClassUnloadingEnabled
卸载是JVM自身机制 不受外部因素影响(比如字节码文件的改动)
所以这里 通过classloader拿到的改动之前的字节码信息.

那如何实现字节码修改 在classloader里面变更呢?

jvm 提供了一套api 叫 JVMTI 是 jvm提供的一套native api 
开发的时候 我们不用关心JVMTI 做了什么 只需要在java 项目启动的时候 添加
-javaagent 即可.

我们利用 这个特性来实现无侵入的对方法添加耗时统计

public class TimeCount {
   /**
    * 实现javaagent 必须添加一个叫premain 的方法
    * (在1.6之后 也可以添加 一个叫 agentmain 具体参见官网)
    */
    public static void premain(String agentOps, Instrumentation inst) {  
        // 添加Transformer  
        inst.addTransformer(new TimeCountAgent());  
    }

}
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

public class TimeCountAgent implements ClassFileTransformer {

    /**
     * 方法返回一个字节数组,其实就是修改后的字节码.
     * 然后就会替换掉classloader中的类描述信息
     */
    @Override
    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        String newClassName = className.replace("/", ".");
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = null;
        try{
        /** 类信息是否处于冻结状态 */
        if(ctClass.isFrozen()){
            return null;/** 不需要转换 */
        }
        CtMethod[] methods = ctClass.getDeclaredMethods();
        byte[] result = null;
        if(methods != null && methods.length > 0){
            for(CtMethod method : methods){
                String name = method.getName();
                /** 2种实现 */
                //addTimeCountMethod2(method);
                addTimeCountMethod(ctClass,name,method);
            }
            return ctClass.toBytecode();
        }
        }catch(Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 修改方法体
     */
    private void addTimeCountMethod2(CtMethod method) throws Exception {
        method.addLocalVariable("start", CtClass.longType);
        method.insertBefore("start = System.currentTimeMillis();");
        method.insertAfter("System.out.println(\"exec time is :\" + (System.currentTimeMillis() - start) + \"ms\");");
    }

    /**
     * 新增方法 的实现
     */
    private byte[] addTimeCountMethod(CtClass cc,String oldName,CtMethod method) throws Exception{
        if(cc.isFrozen()){
            return null;/** 不需要转换 */
        }
        String newName = oldName + "tmp";
        /** 重命名老方法 */
        method.setName(newName);
        /** 新建一个 方法 名字 跟老方法一样 */
        CtMethod newMethod = CtNewMethod.copy(method, oldName, cc, null);
        String type = method.getReturnType().getName();
        StringBuilder body = new StringBuilder();
        /** 新方法增加计时器 */
        body.append("{\n long start = System.currentTimeMillis();\n");
        /** 判断老方法返回类型 */
        if(!"void".equals(type)){
            /** 类似 Object result = newName(args0,args1); */
            body.append(type + " result = ");
        }
        /** 执行老方法 */
        body.append( newName + "($$);\n");
        /** 统计执行完成时间 */
        body.append("System.out.println(\"Call to method " + oldName +
                " took \" +\n (System.currentTimeMillis()-start) + " +
                "\" ms.\");\n");
        if(!"void".equals(type)){
            /** 类似 Object result = newName(args0,args1); */
            body.append("return result;\n");
        }
        body.append("}");
        newMethod.setBody(body.toString());
        cc.addMethod(newMethod);
        return cc.toBytecode();
    }
}

pom.xml 添加

                            <plugin>  
                <groupId>org.apache.maven.pluginsgroupId>  
                <artifactId>maven-shade-pluginartifactId>  
                <version>3.0.0version>  
                <executions>  
                    <execution>  
                        <phase>packagephase>  
                        <goals>  
                            <goal>shadegoal>  
                        goals>  
                        <configuration>  
                            <transformers>  
                                <transformer  
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">  
                                    <manifestEntries>  
                                        <Premain-Class>sun.cn.agent.TimeCountPremain-Class>  
                                    manifestEntries>  
                                transformer>  
                            transformers>  
                        configuration>  
                    execution>  
                executions>  
            plugin>

最后生成的jar 里面有个
META-INF/MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: sun.cn.agent.TimeCount
Archiver-Version: Plexus Archiver

在java 项目启动的时候 添加 javaagent 参数
java -server -javaagent:timeCount.jar XXX
执行结果如下:

=====java.io.FileOutputStream$1=====
Call to method close took 5 ms.
Call to method run took 399 ms.
Call to method run took 466 ms.

你可能感兴趣的:(java)