字节码增强之Javassist

Posted 风在哪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字节码增强之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()方法发现这个机制:

上述代码中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 字节码增强之Javassist

Javassist | 字节码增强技术

动态字节码技术 javassist 初探

字节码编程 | 使用Javassist动态生成Hello World

字节码增强技术探索

Java动态字节技术之Javassist