1. 程式人生 > >動態生成類並載入

動態生成類並載入

轉載自:Java執行時動態生成class的方法

Java是一門靜態語言,通常,我們需要的class在編譯的時候就已經生成了,為什麼有時候我們還想在執行時動態生成class呢?

因為在有些時候,我們還真得在執行時為一個類動態建立子類。比如,編寫一個ORM框架,如何得知一個簡單的JavaBean是否被使用者修改過呢?

User為例:

public class User {
    private String id;
    private String name;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

其實UserProxy實現起來很簡單,就是建立一個User的子類,覆寫所有setXxx()方法,做個標記就可以了:

public class UserProxy extends User {
    private boolean dirty;

    public boolean isDirty() {
        return this.dirty;
    }

    public void setDirty(boolean dirty) {
        this.dirty = dirty;
    }

    @Override
    public void setId(String id) {
        super.setId(id);
        setDirty(true);
    }

    @Override
    public void setName(String name) {
        super.setName(name);
        setDirty(true);
    }
}

但是這個UserProxy就必須在執行時動態創建出來了,因為編譯時ORM框架根本不知道User類。

現在問題來了,動態生成位元組碼,難度有多大?

如果我們要自己直接輸出二進位制格式的位元組碼,在完成這個任務前,必須先認真閱讀JVM規範第4章,詳細瞭解class檔案結構。估計讀完規範後,兩個月過去了。

所以,第一種方法,自己動手,從零開始建立位元組碼,理論上可行,實際上很難。

第二種方法,使用已有的一些能操作位元組碼的庫,幫助我們建立class。

目前,能夠操作位元組碼的開源庫主要有CGLibJavassist兩種,它們都提供了比較高階的API來操作位元組碼,最後輸出為class檔案。

比如CGLib,典型的用法如下:

Enhancer e = new Enhancer();
e.setSuperclass(...);
e.setStrategy(new DefaultGeneratorStrategy() {
    protected ClassGenerator transform(ClassGenerator cg) {
        return new TransformingGenerator(cg,
            new AddPropertyTransformer(new String[]{ "foo" },
                    new Class[] { Integer.TYPE }));
    }});
Object obj = e.create();

比自己生成class要簡單,但是,要學會它的API還是得花大量的時間,並且,上面的程式碼很難看懂對不對?

有木有更簡單的方法?

有!

換一個思路,如果我們能建立UserProxy.java這個原始檔,再呼叫Java編譯器,直接把原始碼編譯成class,再載入進虛擬機器,任務完成!

畢竟,建立一個字串格式的原始碼是很簡單的事情,就是拼字串嘛,高階點的做法可以用一個模版引擎。

如何編譯?

Java的編譯器是javac,但是,在很早很早的時候,Java的編譯器就已經用純Java重寫了,自己能編譯自己,行業黑話叫“自舉”。從Java 1.6開始,編譯器介面正式放到JDK的公開API中,於是,我們不需要建立新的程序來呼叫javac,而是直接使用編譯器API來編譯原始碼。

使用起來也很簡單:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int compilationResult = compiler.run(null, null, null, '/path/to/Test.java');

這麼寫編譯是沒啥問題,問題是我們在記憶體中建立了Java程式碼後,必須先寫到檔案,再編譯,最後還要手動讀取class檔案內容並用一個ClassLoader載入。

有木有更簡單的方法?

有!

其實Java編譯器根本不關心原始碼的內容是從哪來的,你給它一個String當作原始碼,它就可以輸出byte[]作為class的內容。

所以,我們需要參考Java Compiler API的文件,讓Compiler直接在記憶體中完成編譯,輸出的class內容就是byte[]

程式碼改造如下:

Map<String, byte[]> results;
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null);
try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
    JavaFileObject javaFileObject = manager.makeStringSource(fileName, source);
    CompilationTask task = compiler.getTask(null, manager, null, null, null, Arrays.asList(javaFileObject));
    if (task.call()) {
        results = manager.getClassBytes();
    }
}

上述程式碼的幾個關鍵在於:

  1. MemoryJavaFileManager替換JDK預設的StandardJavaFileManager,以便在編譯器請求原始碼內容時,不是從檔案讀取,而是直接返回String
  2. MemoryOutputJavaFileObject替換JDK預設的SimpleJavaFileObject,以便在接收到編譯器生成的byte[]內容時,不寫入class檔案,而是直接儲存在記憶體中。

最後,編譯的結果放在Map<String, byte[]>中,Key是類名,對應的byte[]是class的二進位制內容。

為什麼編譯後不是一個byte[]呢?

因為一個.java的原始檔編譯後可能有多個.class檔案!只要包含了靜態類、匿名類等,編譯出的class肯定多於一個。

如何載入編譯後的class呢?

載入class相對而言就容易多了,我們只需要建立一個ClassLoader,覆寫findClass()方法:

class MemoryClassLoader extends URLClassLoader {

    Map<String, byte[]> classBytes = new HashMap<String, byte[]>();

    public MemoryClassLoader(Map<String, byte[]> classBytes) {
        super(new URL[0], MemoryClassLoader.class.getClassLoader());
        this.classBytes.putAll(classBytes);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] buf = classBytes.get(name);
        if (buf == null) {
            return super.findClass(name);
        }
        classBytes.remove(name);
        return defineClass(name, buf, 0, buf.length);
    }
}

除了寫ORM用之外,還能幹什麼?

可以用它來做一個Java指令碼引擎。實際上本文的程式碼主要就是參考了Scripting專案的原始碼。

完整的原始碼呢?

在這裡:https://github.com/michaelliao/compiler,連Maven的包都給你準備好了!

也就200行程式碼吧!動態建立class不是夢