字节码增强之Javassist

字节码增强之Javassist

Javassist(Java Programming Assist)是编辑字节码的Java类库,它使Java字节码操作变得简单。通过使用Javassist可以使Java程序在运行时定义一个新的类,并且在JVM加载类文件时修改它。Javassist提供两个级别的API:源码级别和字节码级别。如果使用源码级别的API,我们可以在不知道Java字节码知识的情况下编辑Java类文件,就像我们编写Java源代码一样方便。如果使用字节码级别的API,那么需要我们详细了解Java字节码和类文件格式,因为字节码级别的API允许我们对类文件进行任意修改。

Javassist官方教程:Javassist Tutorial

本篇博客结合官方教程讲解了Javassist的几种用法,以及源码级别API的几个重要的工具类中包含的方法

1. start

要使用Javassist,我们需要引入对应的依赖,如果是maven项目的话,我们直接引用下面的代码即可:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.28.0-GA</version>
</dependency>

从一个简单的例子开始:生成一个新类

首先让我们回忆下我们自己如何编写一个Java类:

  1. 确定类名
  2. 添加字段
  3. 添加构造函数
  4. 添加成员函数

那么使用Javassist生成一个新类也包含类似的步骤:

  1. 获取ClassPool对象(ClassPool代表CtClass的容器)
  2. 通过ClassPool对象构建一个CtClass对象(一个CtClass代表一个Class的容器)(确定类名)
  3. 通过CtField对象向类中添加类字段(添加字段)
  4. 通过CtConstructor对象向类中添加构造函数(添加构造函数)
  5. 通过CtMethod对象向类中添加方法(添加成员函数)

其实使用Javassist生成一个新类和我们自己编写一个类的过程差不多,只是Javassist是通过API来完成了我们通过源码完成的事。

接下来看看如何使用Javassist创建一个新类吧:

	/*
	使用Javassist生成一个新的类
	*/
	public static void createUser() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        // 构造一个CtClass对象
        CtClass ctClass = pool.makeClass("cn.wygandwdn.javassist.learn.User");
        // 添加属性字段
        CtField username = new CtField(pool.get("java.lang.String"), "username", ctClass);
        username.setModifiers(Modifier.PRIVATE);
        ctClass.addField(username, CtField.Initializer.constant("ZhangSan"));

        CtField pass = new CtField(pool.get("java.lang.String"), "password", ctClass);
        pass.setModifiers(Modifier.PRIVATE);
        ctClass.addField(pass, CtField.Initializer.constant("123456"));

        // 添加构造函数
        // 默认构造函数
        CtConstructor none = new CtConstructor(new CtClass[] {}, ctClass);
        none.setBody("System.out.println(\"使用javassist产生的默认构造函数\");");
        ctClass.addConstructor(none);

        // 构造函数
        CtConstructor constructor = new CtConstructor(new CtClass[] {pool.get("java.lang.String"), pool.get("java.lang.String")},
                ctClass);
        constructor.setBody("{$0.username = $1;$0.password = $2;}");
        ctClass.addConstructor(constructor);

        // 添加getter和setter方法
        CtMethod setUsername = new CtMethod(CtClass.voidType, "setUsername", new CtClass[] {pool.get("java.lang.String")},
                ctClass);
        setUsername.setModifiers(Modifier.PUBLIC);
        setUsername.setBody("{$0.username = $1;}");
        ctClass.addMethod(setUsername);

        CtMethod getUsername = new CtMethod(pool.get("java.lang.String"), "getUsername", new CtClass[] {}, ctClass);
        getUsername.setModifiers(Modifier.PUBLIC);
        getUsername.setBody("{return $0.username;}");
        ctClass.addMethod(getUsername);
        
        CtMethod setPass = CtNewMethod.setter("setPass", pass);
        ctClass.addMethod(setPass);
        CtMethod getPass = CtNewMethod.getter("getPass", pass);
        ctClass.addMethod(getPass);

        Class<?> user = ctClass.toClass();
        Object instance = user.getDeclaredConstructors()[0].newInstance();
        System.out.println(instance);
    }

在上面的例子中,我们用到了ClassPool、CtClass、CtField、CtConstructor、CtMethod等工具类,接下来就看看这些工具类的用处吧

2. middle

本小节将介绍Javassist中常用的一些源码级别的API:

  • ClassPool:存放CtClass的容器
  • CtClass:代表一个class or interface or annotation
  • CtField:代表class中的某个字段
  • CtConstructor:代表class的构造函数
  • CtMethod:代表class的包含的方法

2.1 ClassPool

ClassPool是存放CtClass的容器,CtClass只能通过ClassPool提供的方法获取,其中包括get()&makeClass()方法;ClassPool通过HashTable来缓存已经创建好的CtClass对象,其中键为className,值为CtClass对象

下面是一些常用的方法:

  • getDefault():获取ClassPool的单例对象,并且添加SystemClassPath
  • get():根据全限定类名获取对应的CtClass对象,如果ClassPool中没有缓存对应的CtClass对象则新建一个CtClass对象缓存并返回
  • getAndRename():根据全限定类名获取CtClass对象,并且重命名类名
  • find():根据全限定类名获取资源位置
  • makeClass():根据全限定类名创造CtClass对象并返回
  • appendClassPath&insertClassPath:向类搜索路径列表尾部&头部插入ClassPath
  • toClass():将CtClass对象转换为Class对象
  • importPackage():将Java包导入到ClassPool中,方便其通过package查找到对应的类

ClassPool中如果缓存大量的CtClass对象,可能会消耗大量缓存;为了避免这种情况的发生,我们可以调用CtClass.detach()方法,该方法将会删除ClassPool中相应的CtClass对象,释放空间;如果我们调用get()方法获取被删除的CtClass对象,那么ClassPool会重新生成相应的CtClass对象

ClassPool可以构造类似于Java类加载器机制的级联ClassPool,当调用get()方法时,先委托给父ClassPool尝试获取CtClass对象,如果父类ClassPool无法获取到CtClass对象则子ClassPool会尝试加载类文件(只存在父子ClassPool的情况下),我们可以通过ClassPool.get()方法发现这个机制:

image-20220204202458974

上述代码中parent就相当于父类ClassPool,如果childFirstLookup为false并且parent非空,则会委托给父ClassPool检索CtClass对象

实现该机制非常简单:

public static void parentClassPool() throws NotFoundException {
    ClassPool parent = ClassPool.getDefault();
    ClassPool child = new ClassPool(parent);
    child.appendClassPath("D:/java_project/trace/learn-javassist/src/main/java");
}

通过向ClassPool的构造函数传入父ClassPool即可

2.2 CtClass

CtClass实例代表一个Java类,只能通过ClassPool获取它,我们可以通过ClassPool的get()&makeClass()方法来获取CtClass对象

CtClass是一个抽象类,并且它的绝大部分方法都是返回null或者抛出异常;这些方法会被其子类重写。CtClass的子类包括:CtClassType、CtPrimitiveType、CtArray

当我们获取到CtClass对象之后,就可以对其进行修改,可以修改、添加、删除类的字段和方法;其中对于方法的修改可以直接修改整个方法体,或者修改方法体中的某个表达式(也就是某行语句)

首先看看CtClass一些常用的方法:

  • isModified():如果类被修改过的话就返回true,否则返回false
  • isFrozen():如果类被冻结则返回true,否则返回false;类是否被冻结表明类是否能够修改,如果类被冻结了则不能进行修改,否则可以修改
  • freeze():冻结类,使其不能被修改
  • defrost():解冻被冻结的类,使其可以被修改
  • isPrimitive():是否为Java原始类型:boolean, byte, char, short, int, long, float, double, or void.
  • getName():获取CtClass代表的类的全限定类名
  • setName():修改类名,必须以全限定类名的形式传递参数
  • isInterface()&isAnnotation()&isEnum():CtClass代表的类对象是否为接口、注解、枚举类型
  • getField()&getMethod()&getConstructor():获取类的字段、方法、构造函数
  • addField()&addMethod()&addConstructor():向类中添加字段、方法、构造函数
  • removeField()&removeMethod()&removeConstructor():从类中移除字段、方法、构造函数
  • insturment():向类中注册CodeConverter或者ExprEditor,其中CodeConverter用于代码的转换,ExprEditor用于编辑方法体中满足条件的代码块,instrument(ExprEditor)是CtMethod和CtConstructor调用的方法
  • toClass()&toBytecoded()&writeFile():将CtClass对象转换为Class对象、bytecode字节码、写入.class文件中,调用这些方法后,CtClass对象将会被冻结,无法进行修改
  • detach():从ClassPool中移除CtClass对象,释放内存

对于CodeConverter和ExprEditor来说,可以通过如下例子加深理解:

/*
CodeConverter为方法体的简单转换器
此类的实例指定如何修改表示方法体的字节码
*/
ClassPool cp = ClassPool.getDefault();
CtClass point = cp.get("Point");
CtClass singleton = cp.get("Singleton");
CtClass client = cp.get("Client");
CodeConverter conv = new CodeConverter();
conv.replaceNew(point, singleton, "makePoint");
client.instrument(conv);

上述代码的作用为:将Client类的所有方法中出现的"new Point()“替换为"Singleton.makePoint()”

   /*
   ExprEditor为方法体转换器
   可以定义该类的子类自定义修改方法体,如果调用CtMethod.instrument方法,会从头到尾扫描方法体
   当发现表达式,例如方法调用、new Object()等被发现,可以调用ExprEdit.edit()方法修改这些表达式
   这些修改会作用到原始方法体
   */
public class Func {
    public static String getName() {
        String action = "开始";
        int length = action.length();
        System.out.println(length);
        return "张三!!!";
    }
    
    public static void updateMethod() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get("Func");
        CtMethod getName = ctClass.getDeclaredMethod("getName", new CtClass[]{});
        getName.instrument(
                new ExprEditor() {
                    @Override
                    public void edit(MethodCall m) throws CannotCompileException {
                        System.out.println(m.getClassName());
                        if (m.getClassName().equals("java.lang.String")
                        && m.getMethodName().equals("length")) {
                            m.replace("$_ = 100;");
                        }
                    }
                }
        );
        Class<?> aClass = ctClass.toClass();
        Method method = aClass.getMethod("getName", new Class[]{});
        Object invoke = method.invoke(aClass, null);
        System.out.println(invoke);
    }
    
    public static void main(String[] args) {
        // 该方法会输出100,因为这里int length = action.length()被改为了int length = 100
        updateMethod();
    }
}

通过CtClass对象,我们可以根据实际业务需求对真实类对象进行修改

有关更多CtClass对象的用法,我们可以在实际的业务需求中进行探索

2.3 CtField

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

CtField的常用方法如下:

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

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

ctClass.addField(field, CtField.Initializer.constant("张三"));

此外,如果我们不使用CtField.make()方法创造CtField对象的话,我们直接调用CtField的构造函数即可,然后调用setModifiers设置字段的访问修饰符即可:

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

2.4 CtConstructor

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

我们可以通过CtClass对象获取其对应的CtConstructor,也可以直接新建一个CtConstructor并添加到CtClass对象中;新建CtConsturctor有两种方式:

// 方法一,通过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);

CtConstructor常用方法如下:

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

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

CtNewConstructor这个工具类也非常有用,我们通常使用它来创建CtConstructor对象,其常用方法如下:

  • make():构造CtNewConsturctor对象
  • copy():利用原有的CtConstructor对象复制一个新的CtConstructor对象
  • defaultConstructor():创建默认构造函数
  • skeleton:创建一个带有参数的构造函数,但是该函数体只调用super()方法,其余代码需要自己编写插入

通过上述代码我们很容易自定义构造函数

2.5 CtMethod

CtMethod代表Java类中的方法,我们通常写的Java类是通过各种各样的方法来实现想要的功能的,所以CtMethod至关重要

CtMethod的创建方法和CtConstructor的创建方法类似,也可以通过源码形式直接创建;或者先定义函数名称和参数类型,然后再设置函数体

CtMethod的一些常用方法如下:

  • make():通过源码的形式创建CtMethod对象
  • getName()&setName():获取函数名,设置函数名(可以用于修改函数名称)
  • setBody():设置函数体
  • setWrappedBody():通过提供的CtMethod对象的函数体来设置当前CtMethod对象的函数体
  • setModifiers():设置函数的访问修饰符
  • insertBefore()&insertAfter()&insertAt():在函数最开始|结束位置|任意位置(需要知道代码行号)插入代码
  • addCatch()&addLocalVariable():添加try catch语句 | 添加局部变量
  • instrument():根据提供的CodeConverter|ExprEditor对函数体中符合条件的代码进行编辑,详情可以参考2.2节

传递给方法 insertBefore() ,insertAfter() ,addCatch() 和 insertAt() 的 String 对象是由Javassist 的编译器编译的。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:

符号含义
$0,$1,$2,…$0代表this,$1,$2,…代表方法的第1个参数、第2个参数…
$args方法参数数组,$args的类型是Object[]
$$代表全部方法参数,例如m($$)=m($1,$2,…)
$cflowcflow变量,此只读变量返回特定方法的递归调用的深度
$r函数返回结果类型,用于强制类型转换
$w包装类型,用于强制类型转换,例如:Integer i = ($w)5;
$_函数返回结果
$sigClass类型的数组,代表形参的类型
$type代表函数返回结果的Class类型
$class代表当前编辑的类的Class类型
$proceed代表源码中调用的方法名、构造函数名…
2.5.1 $cflow

上述标识符中,对于 c f l o w 这 个 标 识 符 , 我 们 初 看 可 能 会 云 里 雾 里 , cflow这个标识符,我们初看可能会云里雾里, cflowcflow表示控制流,此只读变量返回特定方法的递归调用的深度。

假设下面所示的方法由CtMethod对象cm表示:

int fact(int n) {
    if (n <= 1) {
        return n;
    } else {
        return n * fact(n - 1);
    }
}

要想使用cflow,首先需要声明使用cflow监视方法fact()的调用:

CtMethod cm = ...;
cm.useCflow("fact");

useCflow()的参数是$cflow变量的标识符,任何有效的Java名称都可以用作标识符。

c f l o w ( f a c t ) 表 示 由 c m 指 定 的 方 法 的 递 归 调 用 深 度 。 cflow(fact)表示由cm指定的方法的递归调用深度。 cflow(fact)cmcflow(fact)的值在方法第一次调用时为0,而当方法在方法中递归调用时为1.

$cflow的值是当前线程的最顶层堆栈下与cm相关联的堆栈帧数。cflow也可以不在cm方法中访问。

2.5.2 addCatch

如何在方法中插入try catch语句呢,通过下面这个例子就能轻松解决:

CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
// 在插入的源代码中,异常用$e表示
m.addCatch("{System.out.println($e); throw $e;}", etype);

上述代码转换成的java代码如下(请注意,插入的代码片段必须以 throw 或 return 语句结束):

try {
    // the original method body
} catch (java.io.IOException e) {
    System.out.println(e);
    throw e;
}
2.5.3 相互递归的方法

有时候我们可能需要向代码中插入相互递归的方法,Javassist也帮我们想到了对应的解决方法

在Javassist中不能有这样的方法:如果它调用另一个方法,而另一个方法没有被添加到一个类(Javassist可以编译一个以递归方式调用的方法)。如果要向类添加相互递归的方法,需要使用下面的技巧。

假设我们想要将方法m()和n()添加到由ctClass表示的类中:

CtClass ctClass = ...;
CtMethod m = CtNewMethod.make("public abstract int m(int i);", ctClass);
CtMethod n = CtNewMethod.make("public abstract int n(int i);", ctClass);

ctClass.addMethod(m);
ctClass.addMethod(n);
m.setBody("{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }");
n.setBody("{ return m($1); }");
cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);

此时,我们必须先创建两个抽象的方法,并将它们添加到类中,然后设置它们的方法体,最后将类改为非抽象类

3. other

如果事先知道要修改哪些类,那么最简单的方法如下:

  1. 通过ClassPool.get()获取CtClass对象
  2. 然后对CtClass对象进行修改
  3. 调用CtClass对象的writeFile()或者toBytecode()获得修改过的类文件

如果在加载时,可以确定是否要修改某个类,就必须使Javassist与类加载器协作,以便在加载时修改字节码;同时也可以自定义类加载器,或者使用Javassist提供的类加载器

3.1 CtClass.toClass()

CtClass 的 toClass() 方法请求当前线程的上下文类加载器,加载 CtClass 对象所表示的类。要调用此方法,调用者必须具有相关的权限; 否则,可能会抛出 SecurityException。例如:

public class Hello {
    public void say() {
        System.out.println("Hello");
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("Hello");
        CtMethod m = cc.getDeclaredMethod("say");
        m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
        Class c = cc.toClass();
        Hello h = (Hello)c.newInstance();
        h.say();
    }
}

Test.main()向Hello的say()方法中插入一个println(),然后构造一个修改过的Hello类的实例,并在该实例上调用say()方法

上面的程序要正常运行,Hello类在调用toClass()之前就不能被加载;如果JVM在toClass()调用之前加载了原始的Hello类,后续加载修改的Hello类将会失败(抛出LinkageError)

例如,如果Test.main()方法如下:

public static void main(String[] args) throws Exception {
    Hello orig = new Hello();
    ClassPool cp = ClassPool.getDefault();
    CtClass cc = cp.get("Hello");
        :
}

那么原始的Hello类在main的第一行被加载,toClass()调用会抛出一个异常,因为类加载器不能同时加载两个不同版本的Hello类。

如果程序在某些应用程序服务器(如JBoss和Tomcat)上运行,toClass() 使用的上下文类加载器可能是不合适的。在这种情况下,你会看到一个意想不到的 ClassCastException。为了避免这个异常,必须给 toClass() 指定一个合适的类加载器。

例如,如果 ‘bean’ 是你的会话 bean 对象,那么下面的代码:

CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());

可以工作。你应该给 toClass() 传递加载了你的程序的类加载器(上例中,bean对象的类)

3.2 javassist.Loader

Javassit 提供一个类加载器 javassist.Loader。它使用 javassist.ClassPool 对象来读取类文件。

例如,javassist.Loader 可以用于加载用 Javassist 修改过的类。

import javassist.*;
import test.Rectangle;

public class Main {
  public static void main(String[] args) throws Throwable {
     ClassPool pool = ClassPool.getDefault();
     Loader cl = new Loader(pool);

     CtClass ct = pool.get("test.Rectangle");
     ct.setSuperclass(pool.get("test.Point"));

     Class c = cl.loadClass("test.Rectangle");
     Object rect = c.newInstance();
         :
  }
}

如果用户希望在加载时按需修改类,则可以向 javassist.Loader 添加事件监听器。当类加载器加载类时会通知监听器。事件监听器类必须实现以下接口:

public interface Translator {
    public void start(ClassPool pool)
        throws NotFoundException, CannotCompileException;
    public void onLoad(ClassPool pool, String classname)
        throws NotFoundException, CannotCompileException;
}

当事件监听器通过 addTranslator() 添加到 javassist.Loader 对象时,start() 方法会被调用。在 javassist.Loader 加载类之前,会调用 onLoad() 方法。可以在 onLoad() 方法中修改被加载的类的定义。

例如,下面的事件监听器在类加载之前,修改方法getId()的返回值。

public class Func {

    public static void main(String[] args) throws NoSuchMethodException {
        System.out.println(getId());
    }
    
    public static Integer getId() {
        return 512;
    }
}

public class MyTranslator implements Translator {
    void start(ClassPool pool) throws NotFoundException, CannotCompileException {}
    void onLoad(ClassPool pool, String classname) throws NotFoundException, CannotCompileException {
        CtClass ctClass = pool.get(classname);
        CtMethod getId = ctClass.getDeclaredMethod("getId");
        getId.setBody("return ($w)64;");
    }
}

注意,onLoad() 不必调用 toBytecode() 或 writeFile(),因为 javassist.Loader 会调用这些方法来获取类文件。

接下来使用MyTranslator对象运行类Func,main()方法如下:

public static void main(String[] args) throws Throwable {
    Translator translator = new MyTranslator();
    ClassPool pool = ClassPool.getDefault();
    Loader loader = new Loader();
    loader.addTranslator(pool, translator);
    loader.run("cn.wygandwdn.javassist.learn.Func", args);
}

类Func会被MyTranslator监听,在通过loader加载类Func时会修改其中getId()方法的返回值

javassist.Loader 以不同的顺序从 java.lang.ClassLoader 中搜索类。ClassLoader 首先将加载操作委托给父类加载器,只有当父类加载器无法找到它们时才尝试自己加载类。另一方面,javassist.Loader 尝试在委托给父类加载器之前加载类。它仅在以下情况下进行委派:

  1. 在 ClassPool 对象上调用 get() 找不到这个类;
  2. 这些类已经通过 delegateLoadingOf() 来指定由父类加载器加载。

此搜索顺序允许 Javassist 加载修改过的类。但是,如果找不到修改的类,它将委托父类加载器来加载。一旦一个类被父类加载器加载,那个类中引用的其他类也将被父类加载器加载,因此它们是没有被修改的。回想一下,C 类引用的所有类都由 C 的实际加载器加载的。如果你的程序无法加载修改的类,你应该确保所有使用该类的类都是由 javassist 加载的。

3.3 自定义类加载器

自定义类加载器需要继承ClassLoader,并实现findClass方法,这里我们通过ClassPool获取到类名对应的CtClass对象,并生成字节码,通过字节码生成新的Class

import javassist.*;

public class SampleLoader extends ClassLoader {
    /* Call MyApp.main(). */
    public static void main(String[] args) throws Throwable {
        SampleLoader s = new SampleLoader();
        Class c = s.loadClass("MyApp");
        c.getDeclaredMethod("main", new Class[] { String[].class })
         .invoke(null, new Object[] { args });
    }

    private ClassPool pool;

    public SampleLoader() throws NotFoundException {
        pool = new ClassPool();
        // 插入类的存放路径
        pool.insertClassPath("./class"); // MyApp.class must be there.
    }

    /* 
     * Finds a specified class.
     * The bytecode for that class can be modified.
     */
    protected Class findClass(String name) throws ClassNotFoundException {
        try {
            CtClass cc = pool.get(name);
            // *modify the CtClass object here*
            byte[] b = cc.toBytecode();
            return defineClass(name, b, 0, b.length);
        } catch (NotFoundException e) {
            throw new ClassNotFoundException();
        } catch (IOException e) {
            throw new ClassNotFoundException();
        } catch (CannotCompileException e) {
            throw new ClassNotFoundException();
        }
    }
}

这是使用 Javassist 的最简单的方法。 但是,如果你编写一个更复杂的类加载器,你可能需要更详细地了解 Java 的类加载机制。

4. example

前面几小节详细介绍了Javassist的各种用法及API,接下来看几个简单的例子

创建一个接口:

	public static void createInterface() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        // 创造接口
        CtClass ctClass = pool.makeInterface("cn.wygandwdn.javassist.learn.BlogService");

        // 创造虚函数
        CtMethod method = CtNewMethod.abstractMethod(pool.get("java.lang.String"), "getBlogById",
                new CtClass[]{pool.get("java.lang.Integer")}, new CtClass[]{pool.get("java.lang.Exception")}, ctClass);

        // 向接口中添加方法
        ctClass.addMethod(method);

        ctClass.writeFile("D:/java_project/trace/learn-javassist/src/main/java");
    }

创建一个注解:

	public static void createAnnotation() throws CannotCompileException, IOException, NotFoundException {
        ClassPool pool = ClassPool.getDefault();
        CtClass annotation = pool.makeAnnotation("cn.wygandwdn.javassist.learn.Author");
        CtMethod name = CtNewMethod.make("String name();", annotation);
        annotation.addMethod(name);
        CtMethod age = CtNewMethod.make("int age();", annotation);
        annotation.addMethod(age);

        ClassFile classFile = annotation.getClassFile();
        ConstPool constPool = classFile.getConstPool();

        AnnotationsAttribute attributes = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);

        // 设置元注解@Target
        Annotation target = new Annotation("java.lang.annotation.Target", constPool);
        EnumMemberValue targetEnum = new EnumMemberValue(constPool);
        targetEnum.setType("java.lang.annotation.ElementType");
        targetEnum.setValue("TYPE");
        target.addMemberValue("value", targetEnum);
        // 设置元注解@Retention
        Annotation retention = new Annotation("java.lang.annotation.Retention", constPool);
        EnumMemberValue retentionEnum = new EnumMemberValue(constPool);
        retentionEnum.setType("java.lang.annotation.RetentionPolicy");
        retentionEnum.setValue("RUNTIME");
        retention.addMemberValue("value", retentionEnum);
        // 将注解添加到AnnotationAttribute中
        attributes.addAnnotation(target);
        attributes.addAnnotation(retention);

        classFile.addAttribute(attributes);

        annotation.writeFile("D:/java_project/trace/learn-javassist/src/main/java");
    }

4.1 关于注解的全部用法(类、字段、方法、参数)

接下来我们在第一节的基础之上,看看如何在类、字段、方法、参数上添加注解:

public static void createUser() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        // 构造一个CtClass对象
        CtClass ctClass = pool.makeClass("cn.wygandwdn.javassist.learn.User");

        ClassFile classFile = ctClass.getClassFile();
        // 获取类的常量池
        ConstPool constPool = classFile.getConstPool();

        // 添加类注解
        AnnotationsAttribute attributes = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
        Annotation deprecated = new Annotation(Deprecated.class.getCanonicalName(), constPool);
        deprecated.addMemberValue("since", new StringMemberValue("1.1", constPool));
        attributes.addAnnotation(deprecated);
        classFile.addAttribute(attributes);


        // 添加属性字段
        CtField username = new CtField(pool.get("java.lang.String"), "username", ctClass);
        username.setModifiers(Modifier.PRIVATE);
        ctClass.addField(username, CtField.Initializer.constant("ZhangSan"));


        CtField pass = new CtField(pool.get("java.lang.String"), "password", ctClass);
        pass.setModifiers(Modifier.PRIVATE);
        ctClass.addField(pass, CtField.Initializer.constant("123456"));


        // 添加字段注解
        AnnotationsAttribute field = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
        Annotation author = new Annotation(Author.class.getCanonicalName(), constPool);
        author.addMemberValue("name", new StringMemberValue("java", constPool));
        author.addMemberValue("age", new IntegerMemberValue(constPool, 23));
        field.addAnnotation(author);
        username.getFieldInfo().addAttribute(field);
        pass.getFieldInfo().addAttribute(field);


        // 添加构造函数
        // 默认构造函数
        CtConstructor none = new CtConstructor(new CtClass[] {}, ctClass);
        none.setBody("System.out.println(\"使用javassist产生的默认构造函数\");");
        ctClass.addConstructor(none);


        // 构造函数
        CtConstructor constructor = new CtConstructor(new CtClass[] {pool.get("java.lang.String"), pool.get("java.lang.String")},
                ctClass);
        constructor.setBody("{$0.username = $1;$0.password = $2;}");
        ctClass.addConstructor(constructor);

        // 添加getter和setter方法
        CtMethod setUsername = new CtMethod(CtClass.voidType, "setUsername", new CtClass[] {pool.get("java.lang.String")},
                ctClass);
        setUsername.setModifiers(Modifier.PUBLIC);
        setUsername.setBody("{$0.username = $1;}");
        ctClass.addMethod(setUsername);
        CtMethod getUsername = new CtMethod(pool.get("java.lang.String"), "getUsername", new CtClass[] {}, ctClass);
        getUsername.setModifiers(Modifier.PUBLIC);
        getUsername.setBody("{return $0.username;}");
        ctClass.addMethod(getUsername);

        CtMethod setPass = CtNewMethod.setter("setPass", pass);
        ctClass.addMethod(setPass);
        CtMethod getPass = CtNewMethod.getter("getPass", pass);
        ctClass.addMethod(getPass);

        // 添加方法注解
        CtMethod test = CtNewMethod.make("public void testAnnotation(String test, String test1) {" +
                "System.out.println(\"测试使用Javassist向方法添加注解\");}", ctClass);
        AnnotationsAttribute methodAnnotation = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
        Annotation methodAuthor = new Annotation(Author.class.getCanonicalName(), constPool);
        methodAuthor.addMemberValue("name", new StringMemberValue("java", constPool));
        methodAuthor.addMemberValue("age", new IntegerMemberValue(constPool, 23));
        methodAnnotation.addAnnotation(methodAuthor);
        test.getMethodInfo().addAttribute(methodAnnotation);

        // 添加参数注解
        ParameterAnnotationsAttribute parameter = new ParameterAnnotationsAttribute(constPool,
                ParameterAnnotationsAttribute.visibleTag);
        Annotation parameterAnno = new Annotation(Author.class.getCanonicalName(), constPool);
        parameterAnno.addMemberValue("name", new StringMemberValue("java", constPool));
        parameterAnno.addMemberValue("age", new IntegerMemberValue(constPool, 23));
        // 这里二维数组的含义如下:行数为参数的个数,列数为每个参数上注解的个数
        // 二维数组对应位置就是某个参数的注解,如果行数小于参数个数,那么注解就从第一个参数开始标注,直到使用完
        Annotation[][] paramArrays = new Annotation[1][1];
        paramArrays[0][0] = parameterAnno;
        parameter.setAnnotations(paramArrays);
        test.getMethodInfo().addAttribute(parameter);

        ctClass.addMethod(test);

        ctClass.writeFile("D:/java_project/trace/learn-javassist/src/main/java");
    }

上述代码总结了如何使用Javassist添加注解的各种用法,有相关需求的小伙伴模仿着来就可以

5. summary

Javassist学习起来相对比较轻松,不需要掌握太多关于字节码和类文件格式的知识就能够轻松对Java类进行编辑,以此来完成我们的业务需求

Javassist大部分API都提供了一定的示例代码,如果遇到一些新的API而不知其作用,那么阅读它的API文档或者源码是个不错的选择,因为这里面都提供了示例

加油!

有什么问题可以评论区讨论哦!

参考:

javassist动态创建类并添加注解

Javassist生成class(生成类,方法,字段,注解)

利用Javassist编译期创建注解类(Annotation Class) - 掘金 (juejin.cn)

Javassist注解(Annotation)的使用:CXF WebService动态生成 - coshaho - 博客园 (cnblogs.com)

Javassist 使用指南(一) - 简书 (jianshu.com)

Javassist Tutorial

Overview (Javassist API)

欢迎访问我的个人博客:风在哪个人博客

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值