動態生成類並加載

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

Java是一門靜態語言,一般,咱們須要的class在編譯的時候就已經生成了,爲何有時候咱們還想在運行時動態生成class呢?java

由於在有些時候,咱們還真得在運行時爲一個類動態建立子類。好比,編寫一個ORM框架,如何得知一個簡單的JavaBean是否被用戶修改過呢?git

User爲例:github

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()方法,作個標記就能夠了:oracle

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

若是咱們要本身直接輸出二進制格式的字節碼,在完成這個任務前,必須先認真閱讀JVM規範第4章,詳細瞭解class文件結構。估計讀完規範後,兩個月過去了。ide

因此,第一種方法,本身動手,從零開始建立字節碼,理論上可行,實際上很難。this

第二種方法,使用已有的一些能操做字節碼的庫,幫助咱們建立class。spa

目前,可以操做字節碼的開源庫主要有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不是夢

相關文章
相關標籤/搜索