• Java并发编程
  • JVM
  • JMX
  • Java数据结构与算法
  • 动态字节码生成技术
  • 常用工具
  • 2.0 javassist

    2017-04-29 18:01:14 14,050 1

    Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。

    Javassist的官方网站:http://jboss-javassist.github.io/javassist/

     通过javasssit,我们可以:

  • 动态创建新类或新接口的二进制字节码

  • 动态扩展现有类或接口的二进制字节码(AOP)

  • 1、动态创建新类或新接口的二进制字节码

    假设我们需要生成一个User类:

    package com.tianshouzhi;
    
    public class User {
        private String name;
    
        public User(String name) {
            this.name = name;
        }
    
        public User() {
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "name="+name;
        }
    }

    javassist创建代码如下:

    package com.tianshouzhi;
    
    import javassist.*;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Method;
    
    public class UserGenerator {
        public static void main(String[] args) throws Exception {
            ClassPool classPool = ClassPool.getDefault();
            //定义User类
            CtClass ctClassUser = classPool.makeClass("com.tianshouzhi.User");
    
            //定义name字段
            CtClass fieldType = classPool.get("java.lang.String");//字段类型
            String name = "name";//字段名称
            CtField ctFieldName=new CtField(fieldType, name,ctClassUser);
            ctFieldName.setModifiers(Modifier.PRIVATE);//设置访问修饰符
            ctClassUser.addField(ctFieldName, CtField.Initializer.constant("javasssit"));//添加name字段,赋值为javassist
    
            //定义构造方法
            CtClass[] parameters = new CtClass[]{classPool.get("java.lang.String")};//构造方法参数
            CtConstructor constructor=new CtConstructor(parameters,ctClassUser);
            String body = "{this.name=$1;}";//方法体 $1表示的第一个参数
            constructor.setBody(body);
            ctClassUser.addConstructor(constructor);
    
            //setName getName方法
            ctClassUser.addMethod(CtNewMethod.setter("setName",ctFieldName));
            ctClassUser.addMethod(CtNewMethod.getter("getName",ctFieldName));
    
            //toString方法
            CtClass returnType = classPool.get("java.lang.String");
            String methodName = "toString";
            CtMethod toStringMethod=new CtMethod(returnType, methodName, null,ctClassUser);
            toStringMethod.setModifiers(Modifier.PUBLIC);
            String methodBody = "{return \"name=\"+$0.name;}";//$0表示的是this
            toStringMethod.setBody(methodBody);
            ctClassUser.addMethod(toStringMethod);
    
            //代表class文件的CtClass创建完成,现在将其转换成class对象
            Class clazz = ctClassUser.toClass();
            Constructor cons = clazz.getConstructor(String.class);
            Object user = cons.newInstance("wangxiaoxiao");
            Method toString = clazz.getMethod("toString");
            System.out.println(toString.invoke(user));
    
            ctClassUser.writeFile(".");//在当前目录下,生成com/tianshouzhi/User.class文件
        }
    }

    运行程序后输出:

    name=wangxiaoxiao

    通过反编译工具(例如:JD-gui )打开User.class 可以看到类似以下代码:

    Image.png

    2.  动态扩展现有类或接口的二进制字节码(AOP)

    假设我们现在有如下一个类

    package com.tianshouzhi;
    
    public class Looper {
        public void loop(){
            try {
                System.out.println("Looper.loop() invoked");
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    现在我们想统计Looper的loop方法的耗时时间。最简单的思路是使用javassist修改loop方法的ob欧宝娱乐app下载:

    在最前加入:long start=System.currentTimeMillis();

    在最后插入:System.out.println("耗时:"+(System.currentTimeMillis()-start)+"ms");

    如下:

    public class Looper {
    
        public void loop(){
    
            //记录方法调用的开始时间
    
            long start=System.currentTimeMillis();
    
            try {
    
                System.out.println("Looper.loop() invoked");
    
                Thread.sleep(1000L);
    
            } catch (InterruptedException e) {
    
                e.printStackTrace();
    
            }
    
            //方法结束时打印耗时
    
            System.out.println("耗时:"+(System.currentTimeMillis()-start)+"ms");
    
        }
    
    }

    javassist的CtClass方法提供的insertBefore和insertAfter方法,允许我们在一个方法开始和结束添加自己的代码。

    public class JavassisTimingWrong {
    
        public static void main(String[] args) throws Exception {
    
            //需要修改的已有的类名和方法名
    
            String className="com.tianshouzhi.javassist.Looper";
    
            String methodName="loop";
    
            ClassPool classPool = ClassPool.getDefault();
    
            CtClass clazz = classPool.get(className);
    
            CtMethod method = clazz.getDeclaredMethod(methodName);
    
            method.insertBefore("long start=System.currentTimeMillis();");
    
            method.insertAfter("System.out.println(\"耗时:\"+(System.currentTimeMillis()-start)+\"ms\");");
    
            //调用修改的Looper类的loop方法
    
            Looper looper = (Looper) clazz.toClass().newInstance();
    
            looper.loop();
    
        }
    
    }

    此时:

    因此运行时,会爆出类似以下的错

    Image.png

    这是因为,javassist插入的代码片段中,每次插入操作的代码,称之为一个插入代码块,后面的插入块不能使用前面的插入块定义的局部变量,而言且也不能使用方法中的原有局部变量。而上述代码中,我们分表调用了insertBefore和insertAfter插入了两个代码块,而后面的插入块不能使用前面的插入块定义的局部变量start,因此爆出了上面的错。

    而如果代码片段都位于一个插入块中,则局部变量是可以引用的。因此考虑使用如下的方法实现:

    具体的思路是:将原有的loop方法名改为loop$impl,然后再定义一个loop方法,新的loop方法内部会调用loop$impl,在调用之前和调用之后分别加入上述的代码片段。

    实现如下:

    package com.tianshouzhi;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    import javassist.CtNewMethod;
    
    public class JavassisTiming {
        public static void main(String[] args) throws Exception{
            //需要修改的已有的类名和方法名
            String className="com.tianshouzhi.Looper";
            String methodName="loop";
    
            //修改为原有类的方法名为loop$impl
            CtClass clazz = ClassPool.getDefault().get(className);
            CtMethod method = clazz.getDeclaredMethod(methodName);
            String newname = methodName + "$impl";
            method.setName(newname);
    
            //使用原始方法名loop,定义一个新方法,在这个方法内部调用loop$impl
            CtMethod newMethod = CtNewMethod.make("public void "+methodName+"(){" +
                            "long start=System.currentTimeMillis();" +
                            ""+newname+"();" +//调用loop$impl
                            "System.out.println(\"耗时:\"+(System.currentTimeMillis()-start)+\"ms\");" +
                            "}"
                    , clazz);
            clazz.addMethod(newMethod);
    
            //调用修改的Looper类的loop方法
            Looper looper = (Looper) clazz.toClass().newInstance();
            looper.loop();
        }
    }

    输出:

    Looper.loop() invoked耗时:1000ms

    此外还有一种更加简单的方式

    package com.tianshouzhi;
    
    import javassist.util.proxy.MethodFilter;
    import javassist.util.proxy.MethodHandler;
    import javassist.util.proxy.ProxyFactory;
    
    import java.lang.reflect.Method;
    
    public class JavassistAop {
        public static void main(String[] args) throws IllegalAccessException, InstantiationException {
            ProxyFactory factory=new ProxyFactory();
            //设置父类,ProxyFactory将会动态生成一个类,继承该父类
            factory.setSuperclass(Looper.class);
            factory.setFilter(new MethodFilter() {
                @Override
                public boolean isHandled(Method m) {
                    if(m.getName().equals("loop")){
                        return true;
                    }
                    return false;
                }
            });
            //设置拦截处理
            factory.setHandler(new MethodHandler() {
                @Override
                public Object invoke(Object self, Method thisMethod, Method proceed,
                                     Object[] args) throws Throwable {
                    long start=System.currentTimeMillis();
                    Object result = proceed.invoke(self, args);
                    System.out.println("耗时:"+(System.currentTimeMillis()-start)+"ms");
                    return result;
                }
            });
            Class<?> c=factory.createClass();
            Looper object=(Looper) c.newInstance();
            object.loop();
        }
    }