轉載自: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
目前,可以操做字節碼的開源庫主要有CGLib和Javassist兩種,它們都提供了比較高級的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(); } }
上述代碼的幾個關鍵在於:
MemoryJavaFileManager
替換JDK默認的StandardJavaFileManager
,以便在編譯器請求源碼內容時,不是從文件讀取,而是直接返回String
;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不是夢