1. 一個牛逼的 Java 字節(jié)碼類庫!

        共 12884字,需瀏覽 26分鐘

         ·

        2021-06-17 22:37

        點擊關注公眾號,Java干貨及時送達

        作者:rickiyang
        出處:www.cnblogs.com/rickiyang/p/11336268.html

        Java 字節(jié)碼以二進制的形式存儲在 .class 文件中,每一個 .class 文件包含一個 Java 類或接口。

        Javassist 就是一個用來處理 Java 字節(jié)碼的類庫。它可以在一個已經(jīng)編譯好的類中添加新的方法,或者是修改已有的方法,并且不需要對字節(jié)碼方面有深入的了解。同時也可以去生成一個新的類對象,通過完全手動的方式。

        1. 使用 Javassist 創(chuàng)建一個 class 文件

        首先需要引入jar包:

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

        編寫創(chuàng)建對象的類:

        package com.rickiyang.learn.javassist;

        import javassist.*;

        /**
         * @author rickiyang
         * @date 2019-08-06
         * @Desc
         */

        public class CreatePerson {

            /**
             * 創(chuàng)建一個Person 對象
             *
             * @throws Exception
             */

            public static void createPseson() throws Exception {
                ClassPool pool = ClassPool.getDefault();

                // 1. 創(chuàng)建一個空類
                CtClass cc = pool.makeClass("com.rickiyang.learn.javassist.Person");

                // 2. 新增一個字段 private String name;
                // 字段名為name
                CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
                // 訪問級別是 private
                param.setModifiers(Modifier.PRIVATE);
                // 初始值是 "xiaoming"
                cc.addField(param, CtField.Initializer.constant("xiaoming"));

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

                // 4. 添加無參的構造函數(shù)
                CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
                cons.setBody("{name = \"xiaohong\";}");
                cc.addConstructor(cons);

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

                // 6. 創(chuàng)建一個名為printName方法,無參數(shù),無返回值,輸出name值
                CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName"new CtClass[]{}, cc);
                ctMethod.setModifiers(Modifier.PUBLIC);
                ctMethod.setBody("{System.out.println(name);}");
                cc.addMethod(ctMethod);

                //這里會將這個創(chuàng)建的類對象編譯為.class文件
                cc.writeFile("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
            }

            public static void main(String[] args) {
                try {
                    createPseson();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        執(zhí)行上面的 main 函數(shù)之后,會在指定的目錄內(nèi)生成 Person.class 文件:

        //
        // Source code recreated from a .class file by IntelliJ IDEA
        // (powered by Fernflower decompiler)
        //

        package com.rickiyang.learn.javassist;

        public class Person {
            private String name = "xiaoming";

            public void setName(String var1) {
                this.name = var1;
            }

            public String getName() {
                return this.name;
            }

            public Person() {
                this.name = "xiaohong";
            }

            public Person(String var1) {
                this.name = var1;
            }

            public void printName() {
                System.out.println(this.name);
            }
        }

        跟咱們預想的一樣。

        在 Javassist 中,類 Javaassit.CtClass 表示 class 文件。一個 GtClass (編譯時類)對象可以處理一個 class 文件,ClassPoolCtClass 對象的容器。它按需讀取類文件來構造 CtClass 對象,并且保存 CtClass 對象以便以后使用。

        這個資料分享給你:46 張 PPT 弄懂 JVM。

        需要注意的是 ClassPool 會在內(nèi)存中維護所有被它創(chuàng)建過的 CtClass,當 CtClass 數(shù)量過多時,會占用大量的內(nèi)存,API中給出的解決方案是 有意識的調(diào)用CtClassdetach()方法以釋放內(nèi)存

        ClassPool需要關注的方法:

        1. getDefault : 返回默認的ClassPool 是單例模式的,一般通過該方法創(chuàng)建我們的ClassPool;
        2. appendClassPath, insertClassPath : 將一個ClassPath加到類搜索路徑的末尾位置 或 插入到起始位置。通常通過該方法寫入額外的類搜索路徑,以解決多個類加載器環(huán)境中找不到類的尷尬;
        3. toClass : 將修改后的CtClass加載至當前線程的上下文類加載器中,CtClass的toClass方法是通過調(diào)用本方法實現(xiàn)。需要注意的是一旦調(diào)用該方法,則無法繼續(xù)修改已經(jīng)被加載的class;
        4. get , getCtClass : 根據(jù)類路徑名獲取該類的CtClass對象,用于后續(xù)的編輯。

        CtClass需要關注的方法:

        1. freeze : 凍結一個類,使其不可修改;
        2. isFrozen : 判斷一個類是否已被凍結;
        3. prune : 刪除類不必要的屬性,以減少內(nèi)存占用。調(diào)用該方法后,許多方法無法將無法正常使用,慎用;
        4. defrost : 解凍一個類,使其可以被修改。如果事先知道一個類會被defrost, 則禁止調(diào)用 prune 方法;
        5. detach : 將該class從ClassPool中刪除;
        6. writeFile : 根據(jù)CtClass生成 .class 文件;
        7. toClass : 通過類加載器加載該CtClass。

        上面我們創(chuàng)建一個新的方法使用了CtMethod類。CtMthod代表類中的某個方法,可以通過CtClass提供的API獲取或者CtNewMethod新建,通過CtMethod對象可以實現(xiàn)對方法的修改。

        這個資料分享給你:Spring Boot 學習筆記。

        CtMethod中的一些重要方法:

        1. insertBefore : 在方法的起始位置插入代碼;
        2. insterAfter : 在方法的所有 return 語句前插入代碼以確保語句能夠被執(zhí)行,除非遇到exception;
        3. insertAt : 在指定的位置插入代碼;
        4. setBody : 將方法的內(nèi)容設置為要寫入的代碼,當方法被 abstract修飾時,該修飾符被移除;
        5. make : 創(chuàng)建一個新的方法。

        注意到在上面代碼中的:setBody()的時候我們使用了一些符號:

        // $0=this / $1,$2,$3... 代表方法參數(shù)
        cons.setBody("{$0.name = $1;}");

        具體還有很多的符號可以使用,但是不同符號在不同的場景下會有不同的含義,所以在這里就不在贅述,可以看javassist 的說明文檔:http://www.javassist.org/tutorial/tutorial2.html。

        Java 核心技術教程和示例源碼:https://github.com/javastacks/javastack

        2. 調(diào)用生成的類對象

        1. 通過反射的方式調(diào)用

        上面的案例是創(chuàng)建一個類對象然后輸出該對象編譯完之后的 .class 文件。那如果我們想調(diào)用生成的類對象中的屬性或者方法應該怎么去做呢?javassist也提供了相應的api,生成類對象的代碼還是和第一段一樣,將最后寫入文件的代碼替換為如下:

        // 這里不寫入文件,直接實例化
        Object person = cc.toClass().newInstance();
        // 設置值
        Method setName = person.getClass().getMethod("setName", String.class);
        setName.invoke(person, "cunhua");
        // 輸出值
        Method execute = person.getClass().getMethod("printName");
        execute.invoke(person);

        然后執(zhí)行main方法就可以看到調(diào)用了 printName方法。另外,微信搜索Java技術棧,在后臺發(fā)送:面試,可以獲取我整理的 Java 系列面試題和答案,非常齊全。

        2. 通過讀取 .class 文件的方式調(diào)用
        ClassPool pool = ClassPool.getDefault();
        // 設置類路徑
        pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
        CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
        Object person = ctClass.toClass().newInstance();
        //  ...... 下面和通過反射的方式一樣去使用
        3. 通過接口的方式

        上面兩種其實都是通過反射的方式去調(diào)用,問題在于我們的工程中其實并沒有這個類對象,所以反射的方式比較麻煩,并且開銷也很大。那么如果你的類對象可以抽象為一些方法得合集,就可以考慮為該類生成一個接口類。這樣在newInstance()的時候我們就可以強轉為接口,可以將反射的那一套省略掉了。

        還拿上面的Person類來說,新建一個PersonI接口類:

        package com.rickiyang.learn.javassist;

        /**
         * @author rickiyang
         * @date 2019-08-07
         * @Desc
         */

        public interface PersonI {

            void setName(String name);

            String getName();

            void printName();

        }

        實現(xiàn)部分的代碼如下:

        ClassPool pool = ClassPool.getDefault();
        pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");

        // 獲取接口
        CtClass codeClassI = pool.get("com.rickiyang.learn.javassist.PersonI");
        // 獲取上面生成的類
        CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
        // 使代碼生成的類,實現(xiàn) PersonI 接口
        ctClass.setInterfaces(new CtClass[]{codeClassI});

        // 以下通過接口直接調(diào)用 強轉
        PersonI person = (PersonI)ctClass.toClass().newInstance();
        System.out.println(person.getName());
        person.setName("xiaolv");
        person.printName();

        使用起來很輕松。

        2. 修改現(xiàn)有的類對象

        前面說到新增一個類對象。這個使用場景目前還沒有遇到過,一般會遇到的使用場景應該是修改已有的類。比如常見的日志切面,權限切面。我們利用javassist來實現(xiàn)這個功能。

        有如下類對象:

        package com.rickiyang.learn.javassist;

        /**
         * @author rickiyang
         * @date 2019-08-07
         * @Desc
         */

        public class PersonService {

            public void getPerson(){
                System.out.println("get Person");
            }

            public void personFly(){
                System.out.println("oh my god,I can fly");
            }
        }

        然后對他進行修改:

        package com.rickiyang.learn.javassist;

        import javassist.ClassPool;
        import javassist.CtClass;
        import javassist.CtMethod;
        import javassist.Modifier;

        import java.lang.reflect.Method;

        /**
         * @author rickiyang
         * @date 2019-08-07
         * @Desc
         */

        public class UpdatePerson {

            public static void update() throws Exception {
                ClassPool pool = ClassPool.getDefault();
                CtClass cc = pool.get("com.rickiyang.learn.javassist.PersonService");

                CtMethod personFly = cc.getDeclaredMethod("personFly");
                personFly.insertBefore("System.out.println(\"起飛之前準備降落傘\");");
                personFly.insertAfter("System.out.println(\"成功落地。。。。\");");


                //新增一個方法
                CtMethod ctMethod = new CtMethod(CtClass.voidType, "joinFriend"new CtClass[]{}, cc);
                ctMethod.setModifiers(Modifier.PUBLIC);
                ctMethod.setBody("{System.out.println(\"i want to be your friend\");}");
                cc.addMethod(ctMethod);

                Object person = cc.toClass().newInstance();
                // 調(diào)用 personFly 方法
                Method personFlyMethod = person.getClass().getMethod("personFly");
                personFlyMethod.invoke(person);
                //調(diào)用 joinFriend 方法
                Method execute = person.getClass().getMethod("joinFriend");
                execute.invoke(person);
            }

            public static void main(String[] args) {
                try {
                    update();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        personFly方法前后加上了打印日志然后新增了一個方法joinFriend。執(zhí)行main函數(shù)可以發(fā)現(xiàn)已經(jīng)添加上了。

        另外需要注意的是:上面的insertBefore()setBody()中的語句,如果你是單行語句可以直接用雙引號,但是有多行語句的情況下,你需要將多行語句用{}括起來。javassist只接受單個語句或用大括號括起來的語句塊。






        關注Java技術棧看更多干貨



        獲取 Spring Boot 實戰(zhàn)筆記!
        瀏覽 42
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 久久豆花视频 | 婷婷色五月在线 | 国产三级www | av簧片 | 情人把舌头伸进我的下面 |