在開始今天的學習前,我建議你先復習一下[專欄第 6 講](http://time.geekbang.org/column/article/7489)有關動態代理的內容。作為 Java 基礎模塊中的內容,考慮到不同基礎的同學以及一個循序漸進的學習過程,我當時并沒有在源碼層面介紹動態代理的實現技術,僅進行了相應的技術比較。但是,有了[上一講](http://time.geekbang.org/column/article/9946)的類加載的學習基礎后,我想是時候該進行深入分析了。
今天我要問你的問題是,有哪些方法可以在運行時動態生成一個 Java 類?
## 典型回答
我們可以從常見的 Java 類來源分析,通常的開發過程是,開發者編寫 Java 代碼,調用 javac 編譯成 class 文件,然后通過類加載機制載入 JVM,就成為應用運行時可以使用的 Java 類了。
從上面過程得到啟發,其中一個直接的方式是從源碼入手,可以利用 Java 程序生成一段源碼,然后保存到文件等,下面就只需要解決編譯問題了。
有一種笨辦法,直接用 ProcessBuilder 之類啟動 javac 進程,并指定上面生成的文件作為輸入,進行編譯。最后,再利用類加載器,在運行時加載即可。
前面的方法,本質上還是在當前程序進程之外編譯的,那么還有沒有不這么 low 的辦法呢?
你可以考慮使用 Java Compiler API,這是 JDK 提供的標準 API,里面提供了與 javac 對等的編譯器功能,具體請參考[java.compiler](https://docs.oracle.com/javase/9/docs/api/javax/tools/package-summary.html)相關文檔。
進一步思考,我們一直圍繞 Java 源碼編譯成為 JVM 可以理解的字節碼,換句話說,只要是符合 JVM 規范的字節碼,不管它是如何生成的,是不是都可以被 JVM 加載呢?我們能不能直接生成相應的字節碼,然后交給類加載器去加載呢?
當然也可以,不過直接去寫字節碼難度太大,通常我們可以利用 Java 字節碼操縱工具和類庫來實現,比如在[專欄第 6 講](http://time.geekbang.org/column/article/7489)中提到的[ASM](https://asm.ow2.io/)、[Javassist](http://www.javassist.org/)、cglib 等。
## 考點分析
雖然曾經被視為黑魔法,但在當前復雜多變的開發環境中,在運行時動態生成邏輯并不是什么罕見的場景。重新審視我們談到的動態代理,本質上不就是在特定的時機,去修改已有類型實現,或者創建新的類型。
明白了基本思路后,我還是圍繞類加載機制進行展開,面試過程中面試官很可能從技術原理或實踐的角度考察:
* 字節碼和類加載到底是怎么無縫進行轉換的?發生在整個類加載過程的哪一步?
* 如何利用字節碼操縱技術,實現基本的動態代理邏輯?
* 除了動態代理,字節碼操縱技術還有那些應用場景?
## 知識擴展
首先,我們來理解一下,類從字節碼到 Class 對象的轉換,在類加載過程中,這一步是通過下面的方法提供的功能,或者 defineClass 的其他本地對等實現。
~~~
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
~~~
我這里只選取了最基礎的兩個典型的 defineClass 實現,Java 重載了幾個不同的方法。
可以看出,只要能夠生成出規范的字節碼,不管是作為 byte 數組的形式,還是放到 ByteBuffer 里,都可以平滑地完成字節碼到 Java 對象的轉換過程。
JDK 提供的 defineClass 方法,最終都是本地代碼實現的。
~~~
static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);
static native Class<?> defineClass2(ClassLoader loader, String name, java.nio.ByteBuffer b,
int off, int len, ProtectionDomain pd,
String source);
~~~
更進一步,我們來看看 JDK dynamic proxy 的[實現代碼](http://hg.openjdk.java.net/jdk/jdk/file/29169633327c/src/java.base/share/classes/java/lang/reflect/Proxy.java)。你會發現,對應邏輯是實現在 ProxyBuilder 這個靜態內部類中,ProxyGenerator 生成字節碼,并以 byte 數組的形式保存,然后通過調用 Unsafe 提供的 defineClass 入口。
~~~
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces.toArray(EMPTY_CLASS_ARRAY), accessFlags);
try {
Class<?> pc = UNSAFE.defineClass(proxyName, proxyClassFile,
0, proxyClassFile.length,
loader, null);
reverseProxyCache.sub(pc).putIfAbsent(loader, Boolean.TRUE);
return pc;
} catch (ClassFormatError e) {
// 如果出現 ClassFormatError,很可能是輸入參數有問題,比如,ProxyGenerator 有 bug
}
~~~
前面理順了二進制的字節碼信息到 Class 對象的轉換過程,似乎我們還沒有分析如何生成自己需要的字節碼,接下來一起來看看相關的字節碼操縱邏輯。
JDK 內部動態代理的邏輯,可以參考[java.lang.reflect.ProxyGenerator](http://hg.openjdk.java.net/jdk/jdk/file/29169633327c/src/java.base/share/classes/java/lang/reflect/ProxyGenerator.java)的內部實現。我覺得可以認為這是種另類的字節碼操縱技術,其利用了[DataOutputStrem](https://docs.oracle.com/javase/9/docs/api/java/io/DataOutputStream.html)提供的能力,配合 hard-coded 的各種 JVM 指令實現方法,生成所需的字節碼數組。你可以參考下面的示例代碼。
~~~
private void codeLocalLoadStore(int lvar, int opcode, int opcode_0,
DataOutputStream out)
throws IOException
{
assert lvar >= 0 && lvar <= 0xFFFF;
// 根據變量數值,以不同格式,dump 操作碼
if (lvar <= 3) {
out.writeByte(opcode_0 + lvar);
} else if (lvar <= 0xFF) {
out.writeByte(opcode);
out.writeByte(lvar & 0xFF);
} else {
// 使用寬指令修飾符,如果變量索引不能用無符號 byte
out.writeByte(opc_wide);
out.writeByte(opcode);
out.writeShort(lvar & 0xFFFF);
}
}
~~~
這種實現方式的好處是沒有太多依賴關系,簡單實用,但是前提是你需要懂各種[JVM 指令](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5),知道怎么處理那些偏移地址等,實際門檻非常高,所以并不適合大多數的普通開發場景。
幸好,Java 社區專家提供了各種從底層到更高抽象水平的字節碼操作類庫,我們不需要什么都自己從頭做。JDK 內部就集成了 ASM 類庫,雖然并未作為公共 API 暴露出來,但是它廣泛應用在,如[java.lang.instrumentation](https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/package-summary.html)API 底層實現,或者[Lambda Call Site](https://docs.oracle.com/javase/9/docs/api/java/lang/invoke/CallSite.html)生成的內部邏輯中,這些代碼的實現我就不在這里展開了,如果你確實有興趣或有需要,可以參考類似 LamdaForm 的字節碼生成邏輯:[java.lang.invoke.InvokerBytecodeGenerator](http://hg.openjdk.java.net/jdk/jdk/file/29169633327c/src/java.base/share/classes/java/lang/invoke/InvokerBytecodeGenerator.java)[。](http://hg.openjdk.java.net/jdk/jdk/file/29169633327c/src/java.base/share/classes/java/lang/invoke/InvokerBytecodeGenerator.java)
從相對實用的角度思考一下,實現一個簡單的動態代理,都要做什么?如何使用字節碼操縱技術,走通這個過程呢?
對于一個普通的 Java 動態代理,其實現過程可以簡化成為:
* 提供一個基礎的接口,作為被調用類型(com.mycorp.HelloImpl)和代理類之間的統一入口,如 com.mycorp.Hello。
* 實現[InvocationHandler](https://docs.oracle.com/javase/9/docs/api/java/lang/reflect/InvocationHandler.html),對代理對象方法的調用,會被分派到其 invoke 方法來真正實現動作。
* 通過 Proxy 類,調用其 newProxyInstance 方法,生成一個實現了相應基礎接口的代理類實例,可以看下面的方法簽名。
~~~
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
~~~
我們分析一下,動態代碼生成是具體發生在什么階段呢?
不錯,就是在 newProxyInstance 生成代理類實例的時候。我選取了 JDK 自己采用的 ASM 作為示例,一起來看看用 ASM 實現的簡要過程,請參考下面的示例代碼片段。
第一步,生成對應的類,其實和我們去寫 Java 代碼很類似,只不過改為用 ASM 方法和指定參數,代替了我們書寫的源碼。
~~~
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_8, // 指定 Java 版本
ACC_PUBLIC, // 說明是 public 類型
"com/mycorp/HelloProxy", // 指定包和類的名稱
null, // 簽名,null 表示不是泛型
"java/lang/Object", // 指定父類
new String[]{ "com/mycorp/Hello" }); // 指定需要實現的接口
~~~
更進一步,我們可以按照需要為代理對象實例,生成需要的方法和邏輯。
~~~
MethodVisitor mv = cw.visitMethod(
ACC_PUBLIC, // 聲明公共方法
"sayHello", // 方法名稱
"()Ljava/lang/Object;", // 描述符
null, // 簽名,null 表示不是泛型
null); // 可能拋出的異常,如果有,則指定字符串數組
mv.visitCode();
// 省略代碼邏輯實現細節
cw.visitEnd(); // 結束類字節碼生成
~~~
上面的代碼雖然有些晦澀,但總體還是能多少理解其用意,不同的 visitX 方法提供了創建類型,創建各種方法等邏輯。ASM API,廣泛的使用了[Visitor](https://en.wikipedia.org/wiki/Visitor_pattern)模式,如果你熟悉這個模式,就會知道它所針對的場景是將算法和對象結構解耦,非常適合字節碼操縱的場合,因為我們大部分情況都是依賴于特定結構修改或者添加新的方法、變量或者類型等。
按照前面的分析,字節碼操作最后大都應該是生成 byte 數組,ClassWriter 提供了一個簡便的方法。
~~~
cw.toByteArray();
~~~
然后,就可以進入我們熟知的類加載過程了,我就不再贅述了,如果你對 ASM 的具體用法感興趣,可以參考這個[教程](http://www.baeldung.com/java-asm)。
最后一個問題,字節碼操縱技術,除了動態代理,還可以應用在什么地方?
這個技術似乎離我們日常開發遙遠,但其實已經深入到各個方面,也許很多你現在正在使用的框架、工具就應用該技術,下面是我能想到的幾個常見領域。
* 各種 Mock 框架
* ORM 框架
* IOC 容器
* 部分 Profiler 工具,或者運行時診斷工具等
* 生成形式化代碼的工具
甚至可以認為,字節碼操縱技術是工具和基礎框架必不可少的部分,大大減少了開發者的負擔。
今天我們探討了更加深入的類加載和字節碼操作方面技術。為了理解底層的原理,我選取的例子是比較偏底層的、能力全面的類庫,如果實際項目中需要進行基礎的字節碼操作,可以考慮使用更加高層次視角的類庫,例如[Byte Buddy](http://bytebuddy.net/#/)等。
## 一課一練
關于今天我們討論的題目你做到心中有數了嗎?試想,假如我們有這樣一個需求,需要添加某個功能,例如對某類型資源如網絡通信的消耗進行統計,重點要求是,不開啟時必須是**零開銷,而不是低開銷,**可以利用我們今天談到的或者相關的技術實現嗎?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?