前言

我们在之前学习了CommonsCollections1CommonsCollections1,这篇内容我们需要学习的Javassist动态编程,为我们我们学习CommonsCollections2打一个良好的基础

测试环境

  • JDK 8u66
  • IDEA 2021.3

Maven依赖

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

Javassist介绍

Javassist是一个开源的分析、编辑和创建Java字节码的类库,能够在运行时定义、编译新类,并在JVM加载时修改类文件,且生成新字节码非常方便,直接拼java源码就行了。

Javassist使用

Javassist中最为重要的是ClassPool,CtClassCtMethod 以及 CtField这几个类,这里我们重点学习这几个类

ClassPool类

ClassPool是一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。

常用方法

static ClassPool getDefault()
    返回默认的类池。
ClassPath insertClassPath(java.lang.String pathname)    
    在搜索路径的开头插入目录或jar(或zip)文件。
ClassPath insertClassPath(ClassPath cp)    
    ClassPath在搜索路径的开头插入一个对象。
java.lang.ClassLoade getClassLoader()    
    获取类加载器toClass(),getAnnotations()在 CtClass等
CtClass    get(java.lang.String classname)    
    从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用。
ClassPath appendClassPath(ClassPath cp)    
    将ClassPath对象附加到搜索路径的末尾。
CtClass    makeClass(java.lang.String classname)
    创建一个新的public类

CtClass类

CtClass是一个class文件的抽象表示。一个CtClass(compile-time class)对象可以用来处理一个class文件,这些CtClass对象可以从ClassPoold的一些方法获得。

常用方法

void setSuperclass(CtClass clazz)
 更改超类,除非此对象表示接口。
java.lang.Class<?> toClass(java.lang.invoke.MethodHandles.Lookup lookup) 
 将此类转换为java.lang.Class对象。
byte[] toBytecode() 
 将该类转换为类文件。
void writeFile() 
 将由此CtClass 对象表示的类文件写入当前目录。
void writeFile(java.lang.String directoryName) 
 将由此CtClass 对象表示的类文件写入本地磁盘。
CtConstructor makeClassInitializer() 
 制作一个空的类初始化程序(静态构造函数)。

CtMethod

CtMethod的一个实例代表一个方法。

常用方法

public void setBody(CtMethod src, ClassMap map)
    从另一个方法中复制一个方法体。

CtConstructor

CtConstructor的一个实例代表一个构造函数。它可以代表一个静态构造函数(类初始化器)

常用方法

void setBody(java.lang.String src)    
    设置构造函数主体。
void setBody(CtConstructor src, ClassMap map)    
    从另一个构造函数复制一个构造函数主体。
CtMethod toMethod(java.lang.String name, CtClass declaring)    
    复制此构造函数并将其转换为方法。

ClassClassPath

通过java.lang.Class中的getResourceAsStream()获得一个类文件的搜索路径。

常用方法

ClassClassPath(java.lang.Class<?> c)
    构造方法,创建一个搜索路径。
java.net.UR find (java.lang.String classname)    
    获取指定类文件的URL。
java.io.InputStream    openClassfile(java.lang.String classname)    
    通过获取类文getResourceAsStream()。

CtField

表示类中的字段。

测试示例

动态生成类

1.创建一个新的Class

ClassPool pool = ClassPool.getDefault();
//定义类
CtClass stuClass = pool.makeClass("com.ricky.Student");

如果某个类已经存在,可以直接加载它,如下:

CtClass cc = pool.get("java.lang.String");

2.构造类成员变量

//id属性
CtField idField = new CtField(CtClass.longType, "id", stuClass);
//目标类中添加属性
stuClass.addField(idField);

3.构造类方法

CtMethod getMethod = CtNewMethod.make("public int getAge() { return this.age;}", stuClass);
CtMethod setMethod = CtNewMethod.make("public void setAge(int age) { this.age = age;}", stuClass);
stuClass.addMethod(getMethod);
stuClass.addMethod(setMethod);

动态生成类的完整代码

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

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class JavassistDemo {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        //获取默认类池
        ClassPool pool = ClassPool.getDefault();
        //定义类
        CtClass stuClass = pool.makeClass("com.alexsel.javassit.Student");

        //加载类
//        CtClass cc = pool.get("java.lang.String");
        
        //让该类实现序列化接口
        stuClass.setInterfaces(new CtClass[]{pool.makeInterface("java.io.Serializable")});
        

        //id属性
        CtField idField = new CtField(CtClass.intType, "id", stuClass);
        //将id属性设置为public
        idField.setModifiers(AccessFlag.PUBLIC);
        CtField ageField = new CtField(CtClass.intType, "age", stuClass);
        ageField.setModifiers(AccessFlag.PUBLIC);
        //目标类中添加属性
        stuClass.addField(idField);
        stuClass.addField(ageField);

        //添加无参构造方法
        CtConstructor ctConstructor = CtNewConstructor.make("public ClassDemo(){};", stuClass);
        stuClass.addConstructor(ctConstructor);
        //添加有参构造方法
        CtConstructor ctConstructor1 = CtNewConstructor.make("public ClassDemo(int id){this.id = id;}", stuClass);
        stuClass.addConstructor(ctConstructor1);
        //创建一个普通方法
        CtMethod getAgeMethod = CtNewMethod.make("public int getAge() { return this.age;}",stuClass);
        CtMethod setAgeMethod = CtNewMethod.make("public void setAge(int age) { this.age = age;}", stuClass);
        CtMethod calcMethod = CtNewMethod.make("public void calcTest(){java.lang.Runtime.getRuntime().exec(\"calc\");}", stuClass);
        //添加普通方法
        stuClass.addMethod(getAgeMethod);
        stuClass.addMethod(setAgeMethod);
        stuClass.addMethod(calcMethod);

        //将class文件写入磁盘
        //转换成字节流
        byte[] bytes = stuClass.toBytecode();
        //写入磁盘
        File classPath = new File(new File(System.getProperty("user.dir"), "/src/main/java/com/alexsel/javassist"), "Student.class");
        FileOutputStream fos = new FileOutputStream(classPath);
        fos.write(bytes);
        fos.close();

        //获取javassist的classloader
        ClassLoader loader = new Loader(pool);
        System.out.println("loading");
        //通过该classloader加载才是新的一个class
        Class<?> clazz = loader.loadClass("com.alexsel.javassit.Student");

        //反射调用

        Object stu = clazz.newInstance();
        Method setMethod = clazz.getDeclaredMethod("setAge",int.class);
        setMethod.invoke(stu,1);

        System.out.println(clazz.getDeclaredMethod("getAge").invoke(stu));
        //反射调用calc
        clazz.getDeclaredMethod("calcTest").invoke(stu);
    }
}

1667920194016

写入磁盘

这里写入磁盘可以用如下两种方法

  • javassist自带的ctClass.writeFile();可指定绝对路径写入
  • 也可转换为byte流通过FileOutputStream等写入磁盘

常见问题(参考):

  • 这里注意javassist.CannotCompileException异常: 因为同个 Class 是不能在同个 ClassLoader 中加载两次的,所以在输出 CtClass 的时候需要注意下,可以使用javassist自带的classloader解决此问题
  • 反射时newInstance()抛出了java.lang.InstantiationException异常可能是因为没有写无参构造
  • 如果已经加载了通过javassist生成的类,即便是通过反射(如class.forName())或者new都不是加载一个"新类",只有换一个ClassLoader加载才会是生成一个"新类"

动态获取类

1.获取默认类池ClassPool pool = ClassPool.getDefault();

2.获取目标类CtClass cc = cp.get();

3.获取类的方法CtMethod m = cc.getDeclaredMethod();

4.向方法中插入代码m.insertBefore("{java.lang.Runtime.getRuntime().exec(\"calc\");}");

5.转换为class对象Class c = cc.toClass();

6.反射调用对象JavassistDemo2 j= (JavassistDemo)c.newInstance();

7.执行方法j.hello

JavassistDemo2

public class JavassistDemo3 extends People implements InterfaceTest{
    public void hello(){
        System.out.println("hello");
    }
    public void hello2(String name){
        System.out.println("hello bob");
    }
}

JavassistTest

import javassist.*;

public class JavassistTest {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException {
        JavassistTest j = new JavassistTest();
        j.toGetClass();
    }

    public void toGetClass() throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("com.alexsel.javassist.JavassistDemo2");
        CtMethod m = cc.getDeclaredMethod("hello");
        //插入代码
        m.insertAfter("{java.lang.Runtime.getRuntime().exec(\"calc\");}");
        Class c = cc.toClass();
        //反射调用对象
        JavassistDemo2 j = (JavassistDemo2)c.newInstance();
        j.hello();

    }
}

常见问题(参考):

  • 如果目标类未加载过,可以直接调用toClass()方法之后new一个该类的对象即可调用该类。
  • 如果目标类已加载过,就需要用上面的方法,通过javassistClassLoader去加载后进行调用。

Javassist特殊参数

在动态修改类的时候,遇到代码中带有$1这种表示,其中的$1代表的是函数中的第一个形参

setBody会用参数中代码替换原有函数中的代码。

import javassist.*;

public class Test2 {
    public static void main(String[] args) throws Exception {
        ClassPool cPool = ClassPool.getDefault();
        CtClass ctClass = cPool.get("com.alexsel.javassist.JavassistDemo2");
        CtMethod hello = ctClass.getDeclaredMethod("hello2",new CtClass[]{cPool.get("java.lang.String")});
        hello.setBody("{System.out.println(\"hello \"+$1);}");

        ctClass.writeFile();
        ctClass.toClass();

        new JavassistDemo2().hello2("alexsel");
    }
}

1667922858063

其他对应的标识

$0, $1, $2, ...                 this和实际的参数
$args                             一个参数的数组。$args的类型是Object[]。
$$                                所有的实参,例如,m($$)等同于m(1,2,...)
$cflow(…)                        control flow 变量
$r                                返回结果的类型,在强制转换表达式中使用。
$w                                包装器类型,在强制转换表达式中使用。
$_                                返回的结果值
$sig                            类型为java.lang.Class的参数类型对象数组
$type                            类型为java.lang.Class的返回值类型
$class                            一个java.lang.Class对象,代表当前编辑的类。

获取类信息

javassistDemo3

public class JavassistDemo3 extends People implements InterfaceTest{
    public void hello(){
        System.out.println("hello");
    }
    public void hello2(String name){
        System.out.println("hello bob");
    }
}

People

public class People {
    private String name;
    private int age;

    public People(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

InterfaceTest

interface InterfaceTest {
    void hello();
}

测试类


import javassist.*;

import java.util.Arrays;

public class Test3 {
    public static void main(String[] args) throws Exception {
        ClassPool classPool = ClassPool.getDefault();
        //修改方法的名称
        // 需要修改的方法名称
        String mname = "hello";
        CtMethod mold = ctClass.getDeclaredMethod(mname);
        // 修改原有的方法名称
        String nname = mname + "hello123";
        mold.setName(nname);
        CtClass ctClass = classPool.get("com.alexsel.javassist.JavassistDemo2");
        byte[] bytes = ctClass.toBytecode();
        //获取长度
        System.out.println(bytes.length);
        //获取带包名的类名
        System.out.println(ctClass.getName());
        //获取不带包名的类名
        System.out.println(ctClass.getSimpleName());
        //获取父类的名
        System.out.println(ctClass.getSuperclass().getName());
        //获取实现的接口
        System.out.println(Arrays.toString(ctClass.getInterfaces()));
           //输出类的所有非私有构造函数。
        for (CtConstructor constructor : ctClass.getConstructors()) {
            System.out.println(constructor);
        }
        //输出类所有的非私有函数。
        for (CtMethod method : ctClass.getMethods()) {
            System.out.println(method);
        }
    }
}

1667924891050

参考文章

https://www.cnblogs.com/CoLo/p/15383642.html

https://www.shuzhiduo.com/A/amd0q9rLzg/

http://www.javassist.org/html/javassist/CtMethod.html

最后修改:2022 年 11 月 11 日
如果觉得我的文章对你有用,请随意赞赏