Javassist实现Java字节码的动态编译

Javassist是用来处理java字节码的类库, java字节码一般存放在后缀名称为class的二进制文件中。每个二进制文件都包含一个java类或者是java接口。
Javassist主要涉及ClassPool、CtClass、CtMethod、 CtConstructor、CtField等几个类,下面看代码及注释体会各类的含义:
首先在pom.xml中添加Javassist依赖:

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

一、新增一个类并保存:

public static void  buildUser() throws Exception {
    
    
        ClassPool pool = ClassPool.getDefault();
        //导入包,告诉编译器在解析类名时搜索其他包
        //pool.importPackage("ml.code.UserUtils");
        //创建一个空类
        CtClass clazz = pool.makeClass("ml.code.User");
        //继承类
       // clazz.setSuperclass(pool.get("ml.code.UserUtils"));
        //新增一个字段 private String name
        CtField ctField = new CtField(pool.get("java.lang.String"), "name", clazz);
        // 访问级别是 private
        ctField.setModifiers(Modifier.PRIVATE);
        // 初始值是 "毛毛"
        clazz.addField(ctField, CtField.Initializer.constant("毛毛3"));

        // 3. 生成 getter、setter 方法
        clazz.addMethod(CtNewMethod.setter("setName", ctField));
        clazz.addMethod(CtNewMethod.getter("getName", ctField));

        // 4. 添加无参的构造函数
        CtConstructor cons = new CtConstructor(new CtClass[]{
    
    }, clazz);
        cons.setBody("{name = \"小张\";}");
        clazz.addConstructor(cons);

        // 5. 添加有参的构造函数
        cons = new CtConstructor(new CtClass[]{
    
    pool.get("java.lang.String")}, clazz);
        // $0=this / $1,$2,$3... 代表方法参数
        cons.setBody("{$0.name = $1;}");
        clazz.addConstructor(cons);

        // 6. 创建一个名为printName方法,无参数,无返回值,输出name值
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{
    
    }, clazz);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(name);}");
        clazz.addMethod(ctMethod);

        //这里会将这个创建的类对象编译为.class文件,生成之后在Java\IdeaProjects\comm\esa2000\target\classes\\ml\\code这个文件夹下面可以找到编译后的类
        //保存时此处的路径会自动加上pool.makeClass("ml.code.User")中的包路径
        clazz.writeFile();
       // clazz.writeFile("E:\\Java\\IdeaProjects\\comm\\target\\classes");
       // pool.appendClassPath("E:\\Java\\IdeaProjects\\comm\\target\\classes\\");
        System.out.println(clazz.isFrozen());
        pool=null;
        pool=ClassPool.getDefault();
        clazz = pool.get("ml.code.User");
        // 这里不写入文件,直接实例化
        Object person = clazz.toClass().newInstance();
        // 设置值
        Method setName = person.getClass().getMethod("setName", String.class);
        setName.invoke(person, "小王3");
        // 输出值
        Method execute = person.getClass().getMethod("printName");
        execute.invoke(person);
    }

如果在新增类中使用其他类,需要使用pool.importPackage("ml.code.UserUtils");先进行包的导入;
如果CtClass对象被writeFile(),toClass()或者toBytecode()转换成了类对象,Javassist将会冻结此CtClass对象。任何对此对象的后续更改都是不允许的。之所以这样做,主要是因为此类已经被JVM加载,由于JVM本身不支持类的重复加载操作,所以不允许更改。
所以在writeFile(),toClass()或者toBytecode()之后需要继续修改类,然后我在调用pool.get()之前,先调用代码:

if(ctClass.isFrozen()){
    
    
    ctClass.defrost();
}

clazz.writeFile();方法是直接保存在程序类目录下,等同于clazz.writeFile(".");注意是保存在程序类目录下,具体生成位置在类目录下的新增类的包路径下。
如果修改保存,可以使用下面语句获程序的classPath路径

String path=clazz.toClass().getResource("/log4j2.xml").getPath();

然后在path中截取classPath路径。注意不使用"/" 这就是相对路径。也就是log4j2.xml所在的包路径名称都会被获取。

二、修改一个类

public static void modifyUser()throws Exception{
    
    
        ClassPool pool = ClassPool.getDefault();
        /*
         *由静态方法ClassPool.getDefault()返回的默认ClassPool与JVM有相同的扫描路径。
         * 如果程序在JBoss或者Tomcat之类的web应用程序中,
         * ClassPoll对象可能无法找到用户的类,因为这些Web应用服务器使用多个类加载器以及系统类加载器。
         * 在这种情况下,就必须注册额外的类扫描路径到ClassPool中。可以使用如下方式进行注册:
         * 参考:https://www.jianshu.com/p/7323e7bc9a3c
         */
       // pool.insertClassPath(new ClassClassPath(UserUtils.class.getClass()));

        CtClass clazz = pool.get("ml.code.User");
        System.out.println(clazz.isFrozen());
        //防止CtClass被精简,手动开启关闭精简,具体含义看最后引用文章
        clazz.stopPruning(true);
        CtMethod madeUser = clazz.getDeclaredMethod("madeUser");
        madeUser.insertBefore("System.out.println(\"制造开始前\");");
        madeUser.insertAfter("System.out.println(\"成功制造。。。。\");");


        //新增一个方法
        CtMethod ctMethod = new CtMethod(pool.get("java.lang.String"), "modifyAge", new CtClass[]{
    
    }, clazz);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(\"我的年龄是:20\");System.out.println(\"我的年龄是:20\");return \"ss\";}");
        clazz.addMethod(ctMethod);
        //clazz.writeFile();
        clazz.writeFile("E:\\Java\\IdeaProjects\\comm\\target\\classes");
      //  clazz.writeFile("E:\\Java\\IdeaProjects\\comm\\target\\classes");
        //clazz.detach();
       // clazz = clazz.;
      //ClassLoader l=  clazz.toClass().getClassLoader();
        Object user = clazz.toClass().newInstance();
        // 调用 personFly 方法
        Method personFlyMethod = user.getClass().getMethod("madeUser");
        personFlyMethod.invoke(user);
        //调用 joinFriend 方法
        Method execute = user.getClass().getMethod("modifyAge");
        execute.invoke(user);
    }

新增类时使用CtClass clazz = pool.makeClass("ml.code.User");修改一个已存在类时使用CtClass clazz = pool.get("ml.code.User");

两个例子里最后的代码都是使用反射去调取修改后的类,进行执行测试。
看两个引用介绍,写的比较详细,一个是中文简单翻译,一个是原文。

https://www.cnblogs.com/scy251147/p/11100961.html
http://www.javassist.org/tutorial/tutorial.html

猜你喜欢

转载自blog.csdn.net/u011930054/article/details/107223308
今日推荐