lotus

贵有恒何必三更眠五更起 最无益只怕一日曝十日寒

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

叙述

关于java字节码的处理,目前有很多工具,如javassistbcelasm,cglib,以及jdk6引入的JavaCompiler等。

其中,CGLib的底层基于ASM实现,是一个高效高性能的生成库;

ASM是一个轻量级的类库,但需要涉及到JVM的操作和指令。这些实现都需要直接跟虚拟机指令打交道。如果你不想了解虚拟机指令。

javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。如果你不想了解虚拟机指令,可以采用javassist。

另外JDK6引入的JavaCompiler类,也可以帮助我们动态生成字节码对象。

很多框架都使用了动态字节码技术,例如mybatis,hibernate,spring,Struts2,dubbo等。

 

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

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

 通过javasssit,我们可以:

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

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

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

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

package com.tianshouzhi;

publicclassUser{

privateString name;

publicUser(String name){

this.name = name;

}

publicUser(){

}

publicString getName(){

return name;

}

publicvoid setName(String name){

this.name = name;

}

@Override

publicString 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方法的源码:

在最前加入: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();

    }

}

 

 
posted on 2021-04-26 19:51  白露~  阅读(908)  评论(0编辑  收藏  举报