使用 Javassist 運行時生成泛型子類

z32xzh27 7年前發布 | 14K 次閱讀 泛型 Java開發

越是復雜的項目希望使用者能愉快的編碼的話,可能就要使用到字節碼增強工具來暗地里做些手腳。這方面的工具有 JDK 的 Instrumentation, ASM , BCEL , CGLib , Javassist , 還有 Byte Buddy . Javassist 和 Byte Buddy 更貼近我們編碼中的概念,使用起來也簡單,而其他幾個工具需要我們更多的了解字節碼指令,以及常量池等概念。所以我著重去了解怎么運用 Javassist 和 Byte Buddy 來動態修改來生成類文件。

所以本文是系列中的第一篇,旨在以一個 Javassist 的例子來了解它的基本使用方法。本例中在運行時動態生成一個類的子類,并且是泛型的,實現了一個方法,給類加上了一個注解,最終生成一個類文件。總之盡可能的讓這個例子具有代表性,同時又需控制它的復雜性。最后通過加載類文件的方式來驗證前面生成的類是否是正確的,也可以直接反編譯生成的類文件來查看源代碼,不過實際操作中我們可能會被反編譯出來的源代碼欺騙。

本例所使用的 Javassist 的版本是 3.21.0-GA, 是在一個 Maven 項目中測試的,所以 Maven 的依賴是

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

接著創建好基類 Repository 和注解 Scope, 它們的內容分別如下

泛型的 Repository 類

package cc.unmi;

public abstract class Repository<T> {
    abstract T findOne();
}

注解 Scope

package cc.unmi;

import java.lang.annotaion.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RUNTIME)
public @interface Scope {
    String value();
}

下面是動態生成子類以及測試的代碼

package cc.unmi;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.CtNewConstructor;
import javassist.CtNewMethod;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.SignatureAttribute;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.StringMemberValue;

public class Main {

    @SuppressWarnings("unchecked")
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass subClass = pool.makeClass("cc.unmi.UserRepository"); //類的全限名稱
        subClass.setSuperclass(pool.get(Repository.class.getName())); //指定父類,也可以在 makeClass() 的第二個參數指定

        //Javassist 對泛型的支持不甚友好,實現方法中還是會把類型擦除
        subClass.setGenericSignature(new SignatureAttribute.TypeVariable("Repository<String>").encode());

        //即使隱式行為默認的構造函數調用父類的構造函數也必須說明
        CtClass[] params = new CtClass[]{ };
        CtConstructor ctor = CtNewConstructor.make( params, null, CtNewConstructor.PASS_PARAMS, null, null, subClass );
        subClass.addConstructor(ctor);

        //使用了源代碼的方式來實現一個方法,注意這里的類型是被擦除的, 把 Object 改成 String 反而有問題,見后面的解釋
        CtMethod findOneMethod = CtNewMethod.make("public Object findOne(){return \"Yanbin\";}", subClass);
        subClass.addMethod(findOneMethod);

        //加個注解確實復雜,如果想一步創建 Annotation 用 new Annotation("Scope(value=\"Request\")", constPool)
        //產生成類文件反編譯后看起來也對的,但可能用反射 API 就是看不到它
        ConstPool constPool = subClass.getClassFile().getConstPool();
        AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
        Annotation scopeAnnotation = new Annotation(Scope.class.getName(), constPool);
        scopeAnnotation.addMemberValue("value", new StringMemberValue("Request", constPool));
        annotationsAttribute.addAnnotation(scopeAnnotation);
        subClass.getClassFile().addAttribute(annotationsAttribute);

        //由于是 Maven 項目,所以寫入到這個目錄中,最后的類文件是 target/classes/cc/unmi/UserRepository.class
        subClass.writeFile("target/classes");

        Class<Repository<String>> repositoryClass = (Class<Repository<String>>) Class.forName("cc.unmi.UserRepository");
        System.out.println(repositoryClass.getAnnotation(Scope.class).value()); //輸出 Request

        Repository<String> repository = repositoryClass.newInstance();
        System.out.println(repository.findOne()); //輸出 Yanbin

    }
}

從控制臺的輸出可以說明動態生成的類是我們期望的結果。詳情請參考源代碼中的注釋。

代碼中我們想要生成的類原型是 class UserRepository extends Repository<String>{} , 那么應該實現的就是

本文原始鏈接 http://unmi.cc/leverage-javassist-generate-generic-subclass/ , 來自隔葉黃鶯 Unmi Blog

public String findOne() { ... }

但要是把生成方法的那行代碼改成如下

CtNewMethod.make("public String findOne(){return \"Yanbin\";}, subClass);

這時候你要是查看生成類文件經反編譯的源代碼也很漂亮,顯示為返回類型是 String , 可是一加載調用 fineOne() 方法時就悲劇了,控制臺的輸出就成了

Request

Exception in thread "main" java.lang.AbstractMethodError: cc.unmi.Repository.findOne()Ljava/lang/Object;

at cc.unmi.Main.main(Main.java:45)

對于如何為類指定泛型,可參考 CtClass.setGenericSignature API

最后我們可以看一下生成的 target/classes/cc/unmi/UserRepository.class 文件在 IntelliJ IDEA 中反編譯后的樣子

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

package cc.unmi;

import cc.unmi.Scope;

@Scope("Request")
public class UserRepository extends Repository<String> {
    public UserRepository() {
    }

    public Object findOne() {
        return "Yanbin";
    }
}

應用延伸:

  1. 復雜的泛型,如 <List<User>>, class UserRepository<T extend User> extends Repository<T> 等,或是方法中的泛型參數
  2. 定義新的方法,較少情況,因為基本上我們用以生成字節碼的話是基于接口來編程
  3. 實現接口,或生成子接口, 相應的就是 makeInterface(...)
  4. 是否能更多使用 Java 代碼的方式來生成類各種部件
  5. 除了生成 Class 文件,我們也可以得到所生成類的引用; 或字節碼的內容,可用自定義的類加載器進行加載
  6. 是否能同時生成相應的源代碼到 Maven 項目的 generated-sources 目錄中?未曾試過,找到兩個相關的庫 RoasterSrcGen4Javassist .

相關鏈接: Javassist 官方指南 , 進去有幾頁內容。

 

來自:http://unmi.cc/leverage-javassist-generate-generic-subclass/

 

 本文由用戶 z32xzh27 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!