• javassist介绍


    (一)Javassist是什么
    Javassist是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。Javassist使用户不必关心字节码相关的规范也是可以编辑类文件的。
    使用流程:
    0
     
    (二)Javassist核心API
    在Javassist中每个需要编辑的class都对应一个CtCLass实例,CtClass的含义是编译时的类(compile time class),这些类会存储在Class Pool中(Class pool是一个存储CtClass对象的容器)。 CtClass中的CtField和CtMethod分别对应Java中的字段和方法。通过CtClass对象即可对类新增字段和修改方法等操作了。
    0
    1. ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似,常见方法列表:
    1. getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
    2. appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
    3. toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。
    4. 需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
    5. get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。
    2. CtClass: CtClass提供了类的操作,如在类中动态添加新字段、方法和构造函数、以及改变类、父类和接口的方法。,常见方法列表:
    1. freeze : 冻结一个类,使其不可修改;
    2. isFrozen : 判断一个类是否已被冻结;
    3. prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
    4. defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
    5. detach : 将该class从ClassPool中删除;
    6. writeFile : 根据CtClass生成 .class 文件;
    7. toClass : 通过类加载器加载该CtClass。
    上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。
    3. CtField:类的属性,通过它可以给类创建新的属性,还可以修改已有的属性的类型,访问修饰符等
    4. CtMethod:类中的方法,通过它可以给类创建新的方法,还可以修改返回类型,访问修饰符等, 甚至还可以修改方法体内容代码。一些重要的方法:
    1. insertBefore : 在方法的起始位置插入代码;
    2. insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
    3. insertAt : 在指定的位置插入代码;
    4. setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
    5. make : 创建一个新的方法。
    注意到在上面代码中的:setBody()的时候我们使用了一些符号:
    Copy// $0=this / $1,$2,$3... 代表方法参数 cons.setBody("{$0.name = $1;}");
    具体还有很多的符号可以使用,但是不同符号在不同的场景下会有不同的含义,所以在这里就不在赘述,可以看javassist 的说明文档。http://www.javassist.org/tutorial/tutorial2.html
    5. CtConstructor:与CtMethod类似
    API运用
    ClassPool
    //ClassPool
    // 类库, jvm中所加载的class
    ClassPool pool = ClassPool.getDefault();
    // 加载一个已知的类, 注:参数必须为全量类名
    CtClass ctClass = pool.get("com.dxz.dto.Person");
    // 创建一个新的类, 类名必须为全量类名
    CtClass tClass = pool.makeClass("com.dxz.Hello");
    CtField
    //CtField
    // 获取已知类的属性
    CtField ctField = ctClass.getDeclaredField("name");
    // 构建新的类的成员变量
    CtField ctFieldNew = new CtField(CtClass.charType, "address", ctClass);
    // 构建新的类的成员变量
    CtField ctFieldNew2 = new CtField(CtClass.intType, "phone", ctClass);
    // 设置类的访问修饰符为public
    ctFieldNew.setModifiers(Modifier.PUBLIC);
    // 设置类的访问修饰符为public
    ctFieldNew2.setModifiers(Modifier.PUBLIC);
    // 将属性添加到类中
    ctClass.addField(ctFieldNew);
    // 将属性添加到类中
    ctClass.addField(ctFieldNew2);
    CtMethod
    //CtMethod
    // 获取已有方法
    //CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");
    
    //创建新的方法, 参数1:方法的返回类型,参数2:名称,参数3:方法的参数,参数4:方法所属的类
    CtMethod ctMethod = new CtMethod(CtClass.intType, "calc", new CtClass[]
            {CtClass.intType, CtClass.intType}, tClass);
    // 设置方法的访问修饰
    ctMethod.setModifiers(Modifier.PUBLIC);
    // 将新建的方法添加到类中
    ctClass.addMethod(ctMethod);
    // 方法体内容代码 $1代表第一个参数,$2代表第二个参数
    ctMethod.setBody("return $1 + $2;");
    
    //ctClass.addMethod(ctMethod);
    CtConstructor
    // 获取已有的构造方法, 参数为构建方法的参数类型数组
    CtConstructor ctConstructor = ctClass.getDeclaredConstructor(new CtClass[]{});
    // 创建新的构造方法
    CtConstructor ctConstructor = new CtConstructor(new CtClass[]{CtClass.intType},ctClass); ctConstructor.setModifiers(Modifier.PUBLIC);
    ctConstructor.setBody("this.age = $1;");
    ctClass.addConstructor(ctConstructor);
    // 也可直接创建
    ctConstructor = CtNewConstructor.make("public Student(int age){this.age=age;}", ctClass);
    (三)javassist.bytecode.ClassFileWriter核心API
    Javassist提供了低级API来直接编辑类文件。为了使用这些API,你需要详细了解Java字节码和类文件的格式,这样你就可以通过这些API对类文件进行各种修改。
    如果你想要产生一个简单的类文件,javassist.bytecode.ClassFileWriter可能提供了最好的API。它提供了比javassist.bytecode.ClassFile更快的速度,尽管这个API更小一些。
    1. 获取ClassFile对象
    javassist.bytecode.ClassFile对象表示一个类文件。为了获取这个对象,应该调用CtClass的getClassFile()。 除此之外,你可以直接从类文件构造一个javassit.bytecode.ClassFile。例如,
    BufferedInputStream fin
            = new BufferedInputStream(new FileInputStream("Point.class"));
    ClassFile cf = new ClassFile(new DataInputStream(fin));
    这些代码片段从Point.class创建了一个ClassFile对象。
    一个ClassFile对象可以被写回类文件。ClassFile的write()将类文件的内容写入DataOutputStream。
    2. 添加和移除成员
    ClassFile提供了addField()和addMethod()来添加字段或方法(注意构造函数在字节码级别被当作普通的方法)。它提供addAttribute()来添加一个属性到类文件。
    注意FieldInfo,MethodInfo,AttributeInfo对象包括一个到ConstPool(常量池表)引用。ConstPool对象必须是ClassFile对象和FieldInfo(或MethodInfo等)的公用对象,该对象被添加到这些ClassFile对象中。换句话说,FieldInfo(或MethodInfo等)对象禁止在不同的ClassFile对象中共享。
    为了从ClassFile对象中移除字段或方法,你必须先获取一个包含了类所有字段的java.util.List对象。getFields()和getMethods()返回一个列表。字段或方法可以通过调用List对象的remove()方法移除。属性也可以用相似的方式移除。调用FieldInfo或MethodInfo的getAttributes()获取一个属性列表,然后从列表中移除。
    3. 遍历方法体
    为了检查方法体中所有的字节码指令,CodeIterator非常有效。为了获取这个对象,按下面的方式来做:
    MethodInfo minfo = cf.getMethod("move");    // we assume move is not overloaded.
    CodeAttribute ca = minfo.getCodeAttribute();
    CodeIterator ci = ca.iterator();
    CodeIterator对象允许你从头到尾逐条访问字节码指令。下面的方法是CodeIterator声明的部分方法:
    • void begin():移动到第一条指令。
    • void move(int index):移动到指定索引的指令。
    • boolean hasNext():如果还有指令,返回true。
    • int next():返回下一条指令的索引。注意,它不返回下一条操作码的索引。
    • int byteAt(int index):返回指定索引的正8位值。
    • int u16bitAt(int index):返回指定索引的正16位值。
    • int write(byte[] code, int index):将byte数组写入到指定索引。
    • void insert(int index, byte[] code):插入byte数组到索引。分支偏移量等被自动调整。
    下面的代码片段展示了方法体中包含的所有指令:
    while (ci.hasNext()) {
        int index = ci.next();
        int op = ci.byteAt(index);
        System.out.println(Mnemonic.OPCODE[op]);
    }
    完整示例:
    package com.dxz;
    
    import javassist.bytecode.*;
    
    import java.io.*;
    
    public class Demo2 {
        public static void main(String[] args) throws IOException, BadBytecode {
            BufferedInputStream fin
                    = new BufferedInputStream(new FileInputStream("D:\\study\\javaagent\\javassist-demo\\build\\classes\\java\\main\\com\\dxz\\dto\\Person.class"));
            ClassFile cf = new ClassFile(new DataInputStream(fin));
    
            MethodInfo minfo = cf.getMethod("sayHello");    // we assume move is not overloaded.
            CodeAttribute ca = minfo.getCodeAttribute();
            CodeIterator ci = ca.iterator();
    
            while (ci.hasNext()) {
                int index = ci.next();
                int op = ci.byteAt(index);
                System.out.println(Mnemonic.OPCODE[op]);
            }
        }
    }
    结果:
    > Task :Demo2.main()
    getstatic
    new
    dup
    invokespecial
    ldc
    invokevirtual
    aload_0
    getfield
    invokevirtual
    ldc
    invokevirtual
    aload_0
    getfield
    invokevirtual
    invokevirtual
    invokevirtual
    return
    4. 产生字节码序列
    Bytecode对象表示字节码指令的序列。它是字节码的可变数组。示例代码如下:
    ConstPool cp = ...;    // constant pool table
    Bytecode b = new Bytecode(cp, 1, 0);
    b.addIconst(3);
    b.addReturn(CtClass.intType);
    CodeAttribute ca = b.toCodeAttribute();
    这产生了如下序列:
    iconst_3
    ireturn
    你也可以通过调用Bytecode的get()方法获取包含这个序列的字节数组。获取的数组可以被插入到另一个代码属性中。
    Bytecode提供了大量方法来添加特殊的指令到序列中,它提供addOpcode()来添加8位操作码和addIndex()来添加一个索引。每一个操作码的8位值都在Opcode接口中定义。
    用来添加特殊指令的addOpcode()和其它方法是自动维护最大栈深的,除非控制流不包含分支。这个值可以通过调用Bytecode对象的getMaxStack()获取。它也反应在由ByteCode对象创建的CodeAttribute对象中。为了重新计算方法体的最大栈深,调用CodeAttribute的computeMaxStack()方法。
    5. 注解(元标记)
    注解存储在类文件中,作为运行时不可见(或可见)的注解属性。这些属性可以从ClassFile,MethodInfo,FieldInfo对象中获取。调用这些对象的getAttribute(AnnotationsAttribute.invisibleTag)。更详细的说明,可以查看javassist.bytecode.AnnotationsAttribute类和javassist.bytecode.annotation包的javadoc手册。
    Javassist也允许你通过高级的API访问注解。如果你想要通过CtClass访问注解,调用CtClass或者CtBehavior的getAnnotations()方法。
    (四)、调用生产的类对象
    1. 通过反射的方式调用
    通过javassist创建一个类对象然后输出该对象编译完之后的 .class 文件。那如果我们想调用生成的类对象中的属性或者方法应该怎么去做呢?javassist也提供了相应的api,生成类对象的代码还是和第一段一样,将最后写入文件的代码替换为如下:
    package com.dxz;
    
    import javassist.*;
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    public class Call1 {
        public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NotFoundException, CannotCompileException, InstantiationException {
            ClassPool pool = ClassPool.getDefault();
    
            // 1. 创建一个空类
            CtClass cc = pool.makeClass("com.dxz.dto.Compony");
    
            // 2. 新增一个字段 private String name;
            // 字段名为name
            CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
            // 访问级别是 public (setName方法的访问级别)
            param.setModifiers(Modifier.PUBLIC);
            // 初始值是 "xiaoming"
            cc.addField(param, CtField.Initializer.constant("xiaoming"));
    
            // 3. 生成 getter、setter 方法
            cc.addMethod(CtNewMethod.setter("setName", param));
            cc.addMethod(CtNewMethod.getter("getName", param));
    
            // 4. 添加无参的构造函数
            CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
            cons.setBody("{name = \"xiaohong\";}");
            cc.addConstructor(cons);
    
            // 5. 添加有参的构造函数
            cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
            // $0=this / $1,$2,$3... 代表方法参数
            cons.setBody("{$0.name = $1;}");
            cc.addConstructor(cons);
    
            // 6. 创建一个名为printName方法,无参数,无返回值,输出name值
            CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
            ctMethod.setModifiers(Modifier.PUBLIC);
            ctMethod.setBody("{System.out.println(name);}");
            cc.addMethod(ctMethod);
            // 这里不写入文件,直接实例化
            Object person = cc.toClass().newInstance();
    
            //下面通过反射的方式调用
            // 设置值
            Method setName = person.getClass().getMethod("setName", String.class);
            setName.invoke(person, "spring");
            // 输出值
            Method execute = person.getClass().getMethod("printName");
            execute.invoke(person);
        }
    }
    然后执行main方法就可以看到调用了 printName方法。结果:
    0
    2. 通过读取 .class 文件的方式调用
    package com.dxz;
    
    import javassist.*;
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    public class Call2 {
        public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NotFoundException, CannotCompileException, InstantiationException {
            ClassPool pool = ClassPool.getDefault();
            // 设置类路径
            pool.appendClassPath("D:\\study\\javaagent\\javassist-demo\\src\\main\\java\\");
            CtClass ctClass = pool.get("com.dxz.dto.Person");
            // 6. 创建一个名为printName方法,无参数,无返回值,输出name值
            CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, ctClass);
            ctMethod.setModifiers(Modifier.PUBLIC);
            ctMethod.setBody("{System.out.println(name);}");
            ctClass.addMethod(ctMethod);
    
            // 这里不写入文件,直接实例化
            Object person = ctClass.toClass().newInstance();
            //下面通过反射的方式调用
            // 设置值
            Method setName = person.getClass().getMethod("setName", String.class);
            setName.invoke(person, "spring call2");
            // 输出值
            Method execute = person.getClass().getMethod("printName");
            execute.invoke(person);
        }
    }
    结果:
    0
    3. 通过接口的方式
    上面两种其实都是通过反射的方式去调用,问题在于我们的工程中其实并没有这个类对象,所以反射的方式比较麻烦,并且开销也很大。那么如果你的类对象可以抽象为一些方法得合集,就可以考虑为该类生成一个接口类。这样在newInstance()的时候我们就可以强转为接口,可以将反射的那一套省略掉了。
    示例:
    package com.dxz.dto;
    
    public interface IStudent {
        void setName(String name);
        String getName();
        void printName();
    }
    package com.dxz.dto;
    
    public class GoodStudent {
        private String name;
        private int grade;
        public GoodStudent() {
            this.name = "good";
        }
        //get set省
    }
    package com.dxz;
    
    import com.dxz.dto.IStudent;
    import javassist.CannotCompileException;
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.NotFoundException;
    
    public class call3 {
        public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
            ClassPool pool = ClassPool.getDefault();
            pool.appendClassPath("D:\\study\\javaagent\\javassist-demo\\src\\main\\java");
    
            // 获取接口
            CtClass codeClassI = pool.get("com.dxz.dto.IStudent");
            // 获取上面生成的类
            CtClass ctClass = pool.get("com.dxz.dto.GoodStudent");
            // 使代码生成的类,实现 IPerson 接口
            ctClass.setInterfaces(new CtClass[]{codeClassI});
    
            // 以下通过接口直接调用 强转
            IStudent person = (IStudent) ctClass.toClass().newInstance();
            System.out.println(person.getName());
            person.setName("xiaohuihui");
            person.printName();
        }
    }
    结果:
    0
    (五)、总结
    javassist被用于struts2和hibernate中,都用来做动态字节码修改使用。一般开发中不会用到,但在封装框架时比 较有用。虽然javassist提供了一套简单易用的API,但如果用于平常的开发,会有如下几点不好的地方:
    • 1. 所引用的类型,必须通过ClassPool获取后才可以使用
    • 2. 代码块中所用到的引用类型,使用时必须写
    • 全量类名
    • 3. 即使代码块内容写错了,它也不会像eclipse等开发工具一样有提示,它只有在运行时才报错
    • 4. 动态修改的类,必须在修改之前,jvm中不存在这个类的实例对象。修改方法的实现必须在修改的类加载之前进行。
    • 参考:https://juejin.cn/post/6844903633100750862
    参考:https://juejin.cn/post/6844903633100750862
  • 相关阅读:
    找不到"javax.servlet.annotation.WebServlet"解决方法
    Nginx SSL+tomcat集群,request.getScheme() 取到https正确的协议
    Fiddler抓包工具使用
    利用window.open如何绕过浏览器拦截机制
    暂时性死区TDZ理解与总结
    利用vue-meta管理头部标签
    async、await总结
    正则中1、2的理解,利用正则找出重复最多的字符
    Vue优化:常见会导致内存泄漏问题及优化
    vue自定义指令导致的内存泄漏问题解决
  • 原文地址:https://www.cnblogs.com/duanxz/p/15664140.html
Copyright © 2020-2023  润新知