Java 運行時動態生成class


轉載

http://www.liaoxuefeng.com/article/0014617596492474eea2227bf04477e83e6d094683e0536000

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不是夢!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM