Javassist基本用法

本文介绍了Javassist,一个Java字节码处理库,展示了如何动态创建类、添加字段、方法、构造函数,以及使用CtNewMethod生成getter/setter和自定义方法。通过实例演示了如何在运行时修改类文件,包括添加注解和控制代码执行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Javassist概述

Javassist是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件,添加新的方法,或者是修改已有的方法。Javassist使用户不必关心字节码相关的规范也是可以编辑类文件的。

Java常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassit。

核心API

Javassist中每个需要编辑的class都对应一个CtCLass实例,CtClass的含义是编译时的类(compile time class),这些类会存储在ClassPool中(ClassPool是一个存储CtClass对象的容器)。

CtClass中的CtFieldCtMethod分别对应Java中的字段和方法。通过CtClass对象即可对类新增字段和修改方法等操作了。

ClassPool

ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。

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

需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API中给出的解决方案是 有意识的调用 CtClass 的 detach() 方法以释放内存。

public class TestBean2 {
    public static void main(String[] args) {
        //获取默认ClassPath 下的 ClassPool
        ClassPool pool = ClassPool.getDefault();

        //插入ClassPath加到类搜索路径
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        pool.insertClassPath(new LoaderClassPath(cl));

        try {
            //获取指定类CtClass
            CtClass ctClass = pool.get("com.ymqx.动态增加属性和注解.TargetBean");
        } catch (NotFoundException e) {
            e.printStackTrace();
        }

        //创建新类
        CtClass newCtClass = pool.makeClass("com.ymqx.动态增加属性和注解.DestBean");

        //将修改后的CtClass加载至当前线程的上下文类加载器中
        try {
            pool.toClass(newCtClass);
        } catch (CannotCompileException e) {
            e.printStackTrace();
        }
    }
}

静态方法 ClassPool.getDefault () 返回的默认 ClassPool 将搜索底层 JVM (Java 虚拟机) 具有的同一路径。如果某个程序在 web 应用程序服务器 (如 JBoss 和 Tomcat) 上运行, 则 ClassPool 对象可能无法找到用户类, 因为这样的 web 应用程序服务器使用多个类加载器以及系统类加载程序。在这种情况下, 必须将附加的类路径注册到 ClassPool。

CtClass

CtClass 是类文件的抽象表示形式,CtClass 对象是处理类文件的句柄。

  • isFrozen:判断一个类是否已被冻结。
  • isModified:如果类被修改过的话就返回true,否则返回false。
  • isPrimitive:是否为Java原始类型:boolean, byte, char, short, int, long, float, double, or void。
  • prune:删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用。
  • freeze:冻结一个类,使其不可修改。
  • defrost:解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法。
  • detach: 将该class从ClassPool中删除。
  • getName:获取CtClass代表的类的全限定类名。
  • setName:修改类名,必须以全限定类名的形式传递参数。
  • isInterfaceisAnnotationisEnum:CtClass代表的类对象是否为接口、注解、枚举类型。
  • getFieldgetMethodgetConstructor获取类的字段、方法、构造函数。
  • getFieldsgetMethodsgetConstructors获取类的字段、方法、构造函数的集合。
  • addFieldaddMethodaddConstructor:类中添加字段、方法、构造函数。
  • removeFieldremoveMethodremoveConstructor:从类中移除字段、方法、构造函数。
  • insturment:向类中注册CodeConverter或者ExprEditor,其中CodeConverter用于代码的转换,ExprEditor用于编辑方法体中满足条件的代码块,instrument(ExprEditor)是CtMethod和CtConstructor调用的方法。
  • toClasstoBytecodewriteFile:将CtClass对象转换为Class对象、bytecode字节码、写入.class文件中,调用这些方法后,CtClass对象将会被冻结,无法进行修改。
  • setSuperclass:设置父类。

CtField

CtField代表类的字段,既可以通过CtClass获取类中已经存在的字段,也可以新建一个CtField对象并添加到CtClass中。

  • make(String src, CtClass declaring):通过源码的形式构造CtField对象,例如src可以为:“public String name;”,不要忘记";",否则会编译失败,declaring代表我们要添加字段的类
  • getName:获取字段名
  • setName:修改字段名
  • getModifierssetModifiers:获取访问修饰符、设置访问修饰符,其中,设置访问修饰符可以通过Modifier中提供的常量字段设置,并且可以组合设置,例如要设置public static,可以使用如下设置:Modifier.PUBLIC | Modifier.STATIC
  • getAnnotationgetAnnotations:获取字段上的注解
  • getSignature:返回代表字段类型的字符串,如果是String类型的话,返回结果为:Ljava/lang/String;
  • getGenericSignaturesetGenericSignature:获取和设置字段的泛型类型
  • getConstantValue:只能获取Java基本类型的包装类型对应的常量值,如果是其他类型的常量的话则会返回null
  • getFieldInfo:获取字段信息,用于获取、修改、新增注解

在向CtClass类中添加新字段的时候,可以添加字段的初始值,在调用CtClass.addField方法时,向其中传入CtField.Initializer参数即可设置字段对应的初始值,例如:

CtField username = new CtField(pool.get("java.lang.String"), "username", ctClass);
username.setModifiers(Modifier.PRIVATE);

ctClass.addField(username, CtField.Initializer.constant("ZhangSan"));

示例1:创建新类,添加一个int类型的,名字为value的变量,以及getter/setter

public class TestBean2 {
    public static void main(String[] args) {
        //获取默认ClassPath 下的 ClassPool
        ClassPool pool = ClassPool.getDefault();

        // 创建一个新类
        CtClass ctClass = pool.makeClass("com.ymqx.动态增加属性和注解.CreateBean");

        //添加一个int类型的,名字为value的变量,以及getter/setter
        try {
            CtField ctField = new CtField(CtClass.intType,"value",ctClass);
            ctField.setModifiers(Modifier.PRIVATE);
            ctClass.addField(ctField);

            //使用CtNewMethod添加getter/setter
            CtMethod getter = CtNewMethod.getter("getValue", ctField);
            CtMethod setter = CtNewMethod.setter("setValue", ctField);
            ctClass.addMethod(getter);
            ctClass.addMethod(setter);
        } catch (CannotCompileException e) {
            e.printStackTrace();
        }

        //生成字节码文件,方便查看创建出来的类的结果
        try {
            ctClass.writeFile(System.getProperty("user.dir") + "\\target\\classes");
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行会生成新的.class文件

package com.ymqx.动态增加属性和注解;

public class CreateBean {
    private int value;

    public int getValue() {
        return this.value;
    }

    public void setValue(int var1) {
        this.value = var1;
    }

    public CreateBean() {
    }
}

CtConstructor

CtConstructor代表类的构造函数,也可能代表类的初始化函数;可以通过isClassInitializer()方法确定是否为类初始化函数。

  • getLongName:获取带有参数的名称(全限定类名和构造函数参数类型),如:javassist.CtConstructor(CtClass[],CtClass)
  • getName:只获取构造函数名也就是类名
  • isConstructor:是否为构造函数
  • isClassInitializer:是否为类的初始化函数
  • callSuper:是否调用父类构造函数
  • setBody:设置构造函数源码
  • insertBeforeBody:在构造函数开头插入代码
  • setModifiers:设置访问权限

通过上述方法,我们可以自定义构造函数并添加到CtClass对象中去,也可以访问现有构造函数的相关信息。如果需要创建CtConstructor对象,可以通过 CtNewConstructor

  • make:构造CtNewConsturctor对象
  • copy:利用原有的CtConstructor对象复制一个新的CtConstructor对象
  • defaultConstructor:创建默认构造函数
  • skeleton:创建一个带有参数的构造函数,但是该函数体只调用super()方法,其余代码需要自己编写插入
// 方法一,通过new CtConstructor()的方式创建CtConstructor对象
// new CtClass[]{}相当于构造函数的参数,ctClass为要添加构造函数的类对象
CtConstructor none = new CtConstructor(new CtClass[] {}, ctClass);
none.setBody("System.out.println(\"使用javassist产生的默认构造函数\");");
ctClass.addConstructor(none);

// 方法二,通过CtNewConstructor提供的静态方法创建CtConstructor对象
CtNewConstructor.make("public User() {System.out.println(\"使用javassist产生的默认构造函数\");}", ctClass);

CtMethod

CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。

  • make:通过源码的形式创建CtMethod对象
  • getName:获取函数名
  • setName:设置函数名(可以用于修改函数名称)
  • setBody:设置函数体
  • setWrappedBody:通过提供的CtMethod对象的函数体来设置当前CtMethod对象的函数体
  • setModifiers:设置函数的访问修饰符
  • insertBeforeinsertAfterinsertAt:在函数最开始 | 结束位置 | 任意位置(需要知道代码行号)插入代码
  • addLocalVariable:添加局部变量
  • addCatch:添加try catch语句

编译的时候,会把变量名抹掉,传递的参数会依次在局部变量表中的顺序。
比如,如果方法体是 public void test(int a,int b,int c){ … } ,那么a,b,c就对应本地变量表中的1,2,3的位置。Javassist中获取变量时就不能使用原始的名字,参数使用的是$1,$2,$3。

还可以通过 CtNewMethod生成方法

  • make:通过源码构造CtMethod对象
  • copy:利用原有的CtMethod对象复制一个新的CtMethod对象
  • abstractMethod:接口方法只能通过CtNewMethod.abstractMethod来创建抽象方法
  • getter:生成getter方法
  • setter:生成setter方法

示例2:创建新类,添加一个hello的方法

public class TestBean2 {
    public static void main(String[] args) {
        //获取默认ClassPath 下的 ClassPool
        ClassPool pool = ClassPool.getDefault();

        // 创建一个新类
        CtClass ctClass = pool.makeClass("com.ymqx.动态增加属性和注解.CreateBean");

        //添加一个hello的方法
        try {
            CtMethod ctMethod = new CtMethod(CtClass.intType, "hello", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
            ctMethod.setModifiers(Modifier.PUBLIC);
            ctMethod.setBody("return $1 + $2;");
            //在方法体的前后分别插入代码
            ctMethod.insertBefore("System.out.println(\"在前面插入了:\" + $1);");
            ctMethod.insertAfter("System.out.println(\"在最后插入了:\" + $1);");
            ctClass.addMethod(ctMethod);
        } catch (CannotCompileException e) {
            e.printStackTrace();
        }

        //生成字节码文件,方便查看创建出来的类的结果
        try {
            ctClass.writeFile(System.getProperty("user.dir") + "\\target\\classes");
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行会生成新的.class文件

package com.ymqx.动态增加属性和注解;

public class CreateBean {
    public int hello(int var1, double var2) {
        System.out.println("在前面插入了:" + var1);
        double var5 = (double)var1 + var2;
        System.out.println("在最后插入了:" + var1);
        return (int)var5;
    }

    public CreateBean() {
    }
}

示例3:创建新类,使用CtNewMethod添加方法

public class TestBean2 {
    public static void main(String[] args) {
        //获取默认ClassPath 下的 ClassPool
        ClassPool pool = ClassPool.getDefault();

        // 创建一个新类
        CtClass ctClass = pool.makeClass("com.ymqx.动态增加属性和注解.CreateBean");

        try {
            //添加一个int类型的,名字为value的变量,以及getter/setter
            CtField ctField = new CtField(CtClass.intType,"value",ctClass);
            ctField.setModifiers(Modifier.PRIVATE);
            ctClass.addField(ctField);

            //使用CtNewMethod添加getter/setter
            CtMethod getter = CtNewMethod.getter("getValue", ctField);
            CtMethod setter = CtNewMethod.setter("setValue", ctField);
            ctClass.addMethod(getter);
            ctClass.addMethod(setter);

            //添加一个hello2的方法
            CtMethod ctMethod = CtNewMethod.make("public int hello2(int num1, double num2) {\n" +
                    "    double sum = (double)num1 + num2;\n" +
                    "    return (int)sum;\n" +
                    "}", ctClass);
            ctClass.addMethod(ctMethod);

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

        //生成字节码文件,方便查看创建出来的类的结果
        try {
            ctClass.writeFile(System.getProperty("user.dir") + "\\target\\classes");
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

生成的 .class文件:

package com.ymqx.动态增加属性和注解;

public class CreateBean {
    private int value;

    public int getValue() {
        return this.value;
    }

    public void setValue(int var1) {
        this.value = var1;
    }

    public int hello2(int var1, double var2) {
        double var4 = (double)var1 + var2;
        return (int)var4;
    }

    public CreateBean() {
    }
}

CtNewMethod.getter()和CtNewMethod.setter()更简洁生成getter/setter方法,make()方法生成的方法,变量名都会抹掉,替换成var1、var2等

Javassist中特殊参数示例

标识符作用
$0this
$1、$2、 …方法参数(1-N是方法参数的顺序)
$args方法参数数组,类型为Object[]
$$所有方法参数,例如:m($$)相当于m($1,$2,…)
$cflow(…)control flow,这是一个只读变量,返回指定方法递归调用的深度。
$r返回结果的类型,在强制转换表达式中使用。
$w包装器类型,在强制转换表达式中使用。
$_返回的结果值
$sig类型为java.lang.Class的参数类型对象数组
$type类型为java.lang.Class的返回值类型
$class类型为java.lang.Class的正在修改的类

目前只用到简单方法,具体用法参考:
关于Java字节码编程javassist的详细介绍

ConstPool

创建属性相关实例时,比如,给字段属性增加注解等,都需要参数ConstPool常量池。

获取方式:

ClassPool pool = ClassPool.getDefault();
 //获取实体类
CtClass ctClass = pool.get("类名");
ConstPool constPool = ctClass.getClassFile().getConstPool();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会叫的狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值