天天看點

一個 Java 位元組碼類庫!

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

1. 使用 Javassist 建立一個 class 檔案

首先需要引入jar包:

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

編寫建立對象的類:

package com.rickiyang.learn.javassist;

import javassist.*;

/**
 * @author rickiyang
 * @date 2019-08-06
 * @Desc
 */
public class CreatePerson {

    /**
     * 建立一個Person 對象
     *
     * @throws Exception
     */
    public static void createPseson() throws Exception {
        ClassPool pool = ClassPool.getDefault();

        // 1. 建立一個空類
        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. 添加無參的構造函數
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        cons.setBody("{name = \"xiaohong\";}");
        cc.addConstructor(cons);

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

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

        //這裡會将這個建立的類對象編譯為.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();
        }
    }
}      

執行上面的 main 函數之後,會在指定的目錄内生成 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 檔案,ClassPool是 CtClass 對象的容器。它按需讀取類檔案來構造 CtClass 對象,并且儲存 CtClass 對象以便以後使用。

需要注意的是 ClassPool 會在記憶體中維護所有被它建立過的 CtClass,當 CtClass 數量過多時,會占用大量的記憶體,API中給出的解決方案是 有意識的調用CtClass的detach()方法以釋放記憶體。

ClassPool需要關注的方法:

getDefault : 傳回預設的ClassPool 是單例模式的,一般通過該方法建立我們的ClassPool;

appendClassPath, insertClassPath : 将一個ClassPath加到類搜尋路徑的末尾位置 或 插入到起始位置。通常通過該方法寫入額外的類搜尋路徑,以解決多個類加載器環境中找不到類的尴尬;

toClass : 将修改後的CtClass加載至目前線程的上下文類加載器中,CtClass的toClass方法是通過調用本方法實作。需要注意的是一旦調用該方法,則無法繼續修改已經被加載的class;

get , getCtClass : 根據類路徑名擷取該類的CtClass對象,用于後續的編輯。

CtClass需要關注的方法:

freeze : 當機一個類,使其不可修改;

isFrozen : 判斷一個類是否已被當機;

prune : 删除類不必要的屬性,以減少記憶體占用。調用該方法後,許多方法無法将無法正常使用,慎用;

defrost : 解凍一個類,使其可以被修改。如果事先知道一個類會被defrost, 則禁止調用 prune 方法;

detach : 将該class從ClassPool中删除;

writeFile : 根據CtClass生成 .class 檔案;

toClass : 通過類加載器加載該CtClass。

上面我們建立一個新的方法使用了CtMethod類。CtMthod代表類中的某個方法,可以通過CtClass提供的API擷取或者CtNewMethod建立,通過CtMethod對象可以實作對方法的修改。

CtMethod中的一些重要方法:

insertBefore : 在方法的起始位置插入代碼;

insterAfter : 在方法的所有 return 語句前插入代碼以確定語句能夠被執行,除非遇到exception;

insertAt : 在指定的位置插入代碼;

setBody : 将方法的内容設定為要寫入的代碼,當方法被 abstract修飾時,該修飾符被移除;

make : 建立一個新的方法。

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

// $0=this / $1,$2,$3... 代表方法參數

cons.setBody("{$0.name = $1;}");

1

具體還有很多的符号可以使用,但是不同符号在不同的場景下會有不同的含義,是以在這裡就不在贅述,可以看javassist 的說明文檔。

http://www.javassist.org/tutorial/tutorial2.html

Java 核心技術教程和示例源碼:

https://github.com/javastacks/javastack

2. 調用生成的類對象

1. 通過反射的方式調用

上面的案例是建立一個類對象然後輸出該對象編譯完之後的 .class 檔案。那如果我們想調用生成的類對象中的屬性或者方法應該怎麼去做呢?javassist也提供了相應的api,生成類對象的代碼還是和第一段一樣,将最後寫入檔案的代碼替換為如下:

一個 Java 位元組碼類庫!

3. 通過接口的方式

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

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

一個 Java 位元組碼類庫!

使用起來很輕松。

2. 修改現有的類對象 #

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

有如下類對象:

一個 Java 位元組碼類庫!
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();
        // 調用 personFly 方法
        Method personFlyMethod = person.getClass().getMethod("personFly");
        personFlyMethod.invoke(person);
        //調用 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。執行main函數可以發現已經添加上了。

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