Javassist框架研究

上一篇我们简单介绍了ASM,这一篇我们介绍一下Javassist。javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的Shigeru Chiba (千叶滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类(这一点与ASM相比用起来爽很多,但是性能上还是ASM更快)。

Javassist中最为重要的是ClassPool,CtClass ,CtMethod 以及 CtField这几个类。

ClassPool: 一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。需要的注意的是当CtClass数量过多时,会占用大量的内存,API中给出的解方案是有意识的调用CtClass的detach方法释放内存。

  • getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
  • appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
  • toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
  • get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。

CtClass: 表示一个类,这些CtClass对象可以从ClassPool获得。

CtMethods: 表示类中的方法。

  • insertBefore : 在方法的起始位置插入代码;
  • insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
  • insertAt : 在指定的位置插入代码;
  • setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
  • make : 创建一个新的方法。

需要注意的是 传入insertBefore(),insertAfter(),addCatch()和insertAt()的字符串对象被Javassist包含的编译器编译。因此编译器支持语言扩展,专有的以$开头的标识符有特殊的意义:

$0, $1, $2,…参数数组,$args的类型为object[]
$argsthis和实际的参数
$$所有实际的参数,例如m($$)与m($1,$2,…)等同
$cflow(…)cflow变量
$r返回类型。它被用在转换表达式中。
$w包装类类型。它被用在转换表达式中。
$_返回值
$sig一个java.lang.Class对象数组,表示正式的参数类型。
$type一个java.lang.Class对象,表示了正式的返回类型。
$class一个java.lang.Class对象,表示当前编辑的类。

$0, $1, $2, …
传入目标方法的参数需要使用$1, $2, … 而不是原始的参数名。$1 表示第一个参数,$2 表示第二个参数等等。这些变量的类型和参数的类型一致。$0 等于this。如果方法是静态的,$0 无法获取。

另外需要注意的是:上面的insertBefore() 和 setBody()中的语句,如果你是单行语句可以直接用双引号,但是有多行语句的情况下,你需要将多行语句用{}括起来。javassist只接受单个语句或用大括号括起来的语句块。
更多详情参考javassist文档.

CtFields :表示类中的字段。

动态生成一个类:

import javassist.*;
import javassist.bytecode.AccessFlag;

import java.io.IOException;
import java.lang.reflect.Field;

public class Test {

    public static void genClassMethod() {
        ClassPool pool = ClassPool.getDefault();
        // 创建一个public类,假如类已经存在会覆盖原类
        CtClass ct = pool.makeClass("com.pch.javassist.GenClass");
        // 让类实现Cloneable接口
        ct.setInterfaces(new CtClass[]{pool.makeInterface("java.lang.Cloneable")});
        try {
            // 获得一个类型为int,名称为id的字段
            CtField f = new CtField(CtClass.intType, "id", ct);
            // 将字段设置为public
            f.setModifiers(AccessFlag.PUBLIC);
            // 将字段设置到类上
            ct.addField(f);
            // 添加构造函数
            CtConstructor constructor = CtNewConstructor.make("public GenClass(int id) {this.id = id;}", ct);
            ct.addConstructor(constructor);
            // 添加方法
            CtMethod testM = CtNewMethod.make("public void print(String des){ System.out.println(des);}", ct);
            ct.addMethod(testM);
            // 将生成的.class文件保存到磁盘
            ct.writeFile();

            // 下面的代码为验证代码
            Field[] fields = ct.toClass().getFields();
            System.out.println("属性名称:" + fields[0].getName() + "  属性类型:" + fields[0].getType());
        } catch (CannotCompileException | NotFoundException | IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        genClassMethod();
    }
}

这样我们就动态的生成了一个类。是不是比ASM使用起来简单很多。

注意:使用过程中如果出现以下报错

Exception in thread "main" java.lang.NullPointerException
	at javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:103)
	at javassist.util.proxy.DefineClassHelper.toClass3(DefineClassHelper.java:151)
	at javassist.util.proxy.DefineClassHelper.toClass2(DefineClassHelper.java:134)
	at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:95)
	at javassist.ClassPool.toClass(ClassPool.java:1143)
	at javassist.ClassPool.toClass(ClassPool.java:1106)
	at javassist.ClassPool.toClass(ClassPool.java:1064)
	at javassist.CtClass.toClass(CtClass.java:1275)
	at com.pch.javassist.Test.genClassMethod(Test.java:34)
	at com.pch.javassist.Test.main(Test.java:42)
  • 首先,检查是不是路径有问题,如果你使用的是idea,看一下Working directory的路径
    在这里插入图片描述
  • 如果路径没有问题,需要看一下JDK版本与javassist版本是否一致?
    例如:我们本地使用的是jdk11,而我们用的是javassist的3.22.0-GA版本就不行,查看ClassPool源码我们发现
public class ClassPool {
    private static java.lang.reflect.Method definePackage = null;

    static {
        // 原因原来在这
        if (ClassFile.MAJOR_VERSION < ClassFile.JAVA_9)
            try {
                AccessController.doPrivileged(new PrivilegedExceptionAction(){
                    public Object run() throws Exception{
                        Class cl = Class.forName("java.lang.ClassLoader");
                        definePackage = cl.getDeclaredMethod("definePackage",
                                new Class[] { String.class, String.class, String.class,
                                        String.class, String.class, String.class,
                                        String.class, java.net.URL.class });
                        return null;
                    }
                });
            }
            catch (PrivilegedActionException pae) {
                throw new RuntimeException("cannot initialize ClassPool",
                                           pae.getException());
            }
    }

所以,我们要保证javassist的版本与jdk版本匹配。

其实,javassist提供了手动设置路径的方式

 ClassPool pool = ClassPool.getDefault();
 // 设置类路径
 pool.appendClassPath("***/Fighting/src/main/java/");
 // 创建一个public类,假如类已经存在会覆盖原类
 CtClass ct = pool.makeClass("com.pch.javassist.GenClass");

同时,写入方法也支持路径指定

//这里会将这个创建的类对象编译为.class文件
cc.writeFile("**/Fighting/src/main/java/");

动态修改方法体

AOP就是使用这种技术,动态的在一个方法中插入代码,下面我们举一个例子
例如,我们有一个如下类

public class Point {
    private int x;
    private int y;

    public Point() {
    }

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void move(int dx, int dy) {
        this.x += dx;
        this.y += dy;
    }
}

动态的在move方法体前后插入一些代码

 private static void modifyMethod() {
        ClassPool pool = ClassPool.getDefault();
        try {
            CtClass ct = pool.getCtClass("com.pch.javassist.Point");
            CtMethod m = ct.getDeclaredMethod("move");
            m.insertBefore("{ System.out.print(\"dx:\"+$1); System.out.println(\"dy:\"+$2);}");
            m.insertAfter("{System.out.println(this.x); System.out.println(this.y);}");
            ct.writeFile();
            
            //通过反射调用方法,查看结果
            Class pc = ct.toClass();
            Method move = pc.getMethod("move", new Class[]{int.class, int.class});
            Constructor<?> con = pc.getConstructor(new Class[]{int.class, int.class});
            move.invoke(con.newInstance(1, 2), 1, 2);

        } catch (Exception e) {
            e.printStackTrace();
        }
   }

另外需要注意的是:上面的insertBefore() 和 setBody()中的语句,如果你是单行语句可以直接用双引号,但是有多行语句的情况下,你需要将多行语句用{}括起来。javassist只接受单个语句或用大括号括起来的语句块。
需要注意的是: 语句和代码块可以引用属性和方法。如果方法是用-g选项编译的(也可以在类文件中包含局部变量属性),它们也可以引用它们插入的方法的参数。否则,它们必须通过下文描述的特殊变量$0, $1, $2等访问方法的参数。访问方法声明的本地变量是不允许的,尽管允许代码块声明新的本地变量。然而,如果这些变量在指定的行号可以获得且目标方法使用-g选项编译,insertAt()运行语句和代码块访问本地变量。

Javassist不允许移除方法或属性,但是它允许修改其名称。Javassist也不允许添加一个额外的参数到现有的方法。反之,应该添加一个新的接受额外参数或者其它参数的方法。

javassist先介绍到这里,下一篇文章我们介绍一下APT框架

参考:

  • https://www.cnblogs.com/rickiyang/p/11336268.html
  • https://blog.csdn.net/ShuSheng0007/article/details/81269295
  • https://www.jianshu.com/p/b0986a86cd62
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值