• Java 动态字节码生成技术 javassist


    叙述

    关于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();

        }

    }

     
  • 相关阅读:
    浏览器es6报错 babelplugintransformruntime 引入 代替babelpolyfill
    windows11下安装MySQL 8.0 步骤
    【其它】idea 2022.1 超详细破解教程,亲测有效!(Webstorm,Goland,Pycharm,DataGrip等全家桶)
    MyBatis的配置
    PVE配置证书
    shell脚本生成证书SAN证书,chrome自签名证书无效。
    virtiowin下载地址
    DOCKER 2375 2376 TLS
    H3C交换机同步网络时钟
    openwrt配置证书,ddnspod证书NGINX使用
  • 原文地址:https://www.cnblogs.com/shoshana-kong/p/14706214.html
Copyright © 2020-2023  润新知