Java 超程式設計及其應用
首先,我們且不說超程式設計是什麼,他能做什麼.我們先來談談生產力.
同樣是實現一個投票系統,一個是python程式設計師,基於django-framework,用了半小時就搭建了一個完整系統,另外一個是標準的SSM(Spring-SpringMVC-Mybatis)Java程式設計師,用了半天,才把環境剛剛搭好.
可以說,社群內,成功的web框架中基本沒有不強依賴超程式設計技術的,框架做的工作越多,應用編寫就越輕鬆.
那什麼是超程式設計
超程式設計是寫出編寫程式碼的程式碼
試想以下,如果那些原本需要我們手動編寫的程式碼,可以自動生成,我們是不是又更多的時間來做更加有意義的事情?有些框架之所以開發效率高,其原因也是因為框架層面,把大量的需要重複編寫的程式碼,採用超程式設計的方式給自動生成了.
甚至,我們可以大膽在想一步,如果有個更加智慧的機器人,幫我們寫程式碼,那麼我們是不是又可以省掉更多的精力,來做更加有意義的事情?
如果我們的應用框架有這樣一種能力,那麼可以省掉我們大部分的重複工作.
比如經常被Java程式設計師詬病的大段大段的setter/getter/toString/hashCode/equals方法,這些方法其實在模型欄位定義好了之後,這些方法其實基本上就已經標準化了,比如常用的IDE(eclipse,IDEA)都支援自動生成這些方法,這樣挺好,可以省掉我們好多精力. 但是這樣做的還不夠好,當我們嘗試去理解一個模型的時候,視線裡有大量這些的冗餘方法,會增加我們對於模型理解的負擔. lombok給出了一個解決方案通過註解的方法,來自動為模型生成setter/getter/toString/hashCode方法,使我們的程式碼精簡了很多.
比如另外一個Java程式設計師詬病的地方,用mybatis訪問資料庫,即使我們的對資料庫的操作僅僅是簡單的增刪查改,我們也需要對每一個操作的定義sql,我們需要編寫
- 領域模型物件
- DAO的interface
- mybatis的mapper檔案
程式設計師世界有個撓癢癢定理
當一個東西令你覺得癢了,那麼很有可能,這個東西也令其他程式設計師癢了,而且github上面也許已經有了現成的專案可以借鑑.
比如 mybatis generator就可以根據資料庫結構自動生成上面這些檔案, 他大大減少了初次搭建專案的負擔.
但是檔案生成了,我麼就得維護,我們會往裡面加其它東西,比如加欄位,增加其它操作. 這樣當資料庫的表結構有變動之後,我們就要維護所有涉及到的檔案,這個工作量其實也不小. 有沒有更好的方法?本文後面會提出一種解決方案.
Java超程式設計的幾種姿勢
反射(reflection)
自省
我們要生成程式碼,我至少得知道我們現有的程式碼長什麼樣子吧?
正如,我們要化妝(給自己化妝,亦或是給別人化妝)我們至少得看得清楚我們的容貌,別人的容貌吧.
reflection
這個名字起得真有意思,把程式的自省比喻成照鏡子,對著這個鏡子,程式就知道,喲,
- 這是一個
Class
- 這個
Class
有幾個Field
- 這個
Field
是什麼型別的 - 這個
Field
是否static
,是否是final
的 - 這個
Class
還有幾個Method
- 這個
Method
的返回型別是什麼 - 這個
Method
的引數列表型別什麼 - 每個引數有什麼註解
- …
引數的名字在執行時已經擦除了,獲取不到
反射的API除了提供了以上的讀
能力之外,還提供了一個動態代理的功能.
動態代理
所謂動態代理,它的動態其實是相對於靜態代理而言的.在靜態代理裡面,代理物件與被代理物件的型別都實現了同樣的介面,這樣當客戶端持有一個介面物件的時候,就可以用代理的物件來替換這個真實物件,同時這個代理物件就像在扮演真實物件的祕書,很多需要真實物件處理的東西,其實都是這個代理做的.大部分場景下,他會直接把問題轉給真實物件處理,同時,他還做了其它事情
- 比如記錄一下日誌啊
- 比如
選擇性拒絕
啊(我們老闆太忙,這個請求我替我們老闆拒絕了) - 甚至還可以通過請求其它服務,來偽造結果(mock)
所有的這些代理工作的實現,都是在寫程式碼的時候,手動實現好的. 明顯,這很不超程式設計
動態代理的神奇之處在於,本來老闆
是沒有祕書
的,只是突然決定要請一個祕書
,就臨時變了一個祕書出來
,老闆
能做的事情,他都能做(Proxy.newProxyInstance()
需要傳一個介面列表,這個新生成的類,就會實現這些介面)
有了這種變化能力
,我們不僅僅可以動態變出AA
的代理AAProxy
,而且還能動態變出BB
的代理BBProxy
,甚至更多. 看出區別了嗎?
如果有10個需要代理的類,在靜態代理中,我們就需要編寫10個代理類;而在動態代理中,我們可以僅需要編寫一個實現了java.lang.reflect.InvocationHandler
介面的類即可.
我們編寫的不是程式碼,而是生成程式碼的程式碼
甚至更誇張的是,本來公司沒老闆
(被代理類),現在決定要一個老闆
,我們描述一下這個老闆需要什麼能力(實現的介面
),就能動態的變一個類似於老闆的東西(代理物件
),而這個東西,還挺像個老闆的(實現了老闆的介面,並且能夠符合人們預期工作)
就像retrofit這個專案實現的一樣,通過一個介面,以及這個介面上的註解,就能動態生成一個符合預期的,http介面的Java SDK.(程式碼就不貼了,有興趣自己到官網參觀).我之前,也借鑑這種模式,寫了一個公司內部http介面的生成器. 這種編碼方式,更加乾淨,更加直觀.
其它使用動態代理技術的專案
- Spring的基於介面的AOP
- dubbo reference物件的生成
- …
位元組碼增強(bytecode enhancement)
我們知道,Java的類是編譯成位元組碼存在class檔案中的,類的載入,其實就是位元組碼被讀取,生成Class類的過程.
我們是否能夠通過某種途徑,改變這個位元組碼呢?
要回答這個問題,我們可以先反問一句,我們是否有改變一個已經載入了的Class
的需求呢?還真有,比如我們想給一個類的某些標記了@Log
註解的方法進行打日誌記錄,我們想統計一個標記了@Perf
註解的方法的執行時間. 如果我們無法改變一個類,那麼我們就必須在每個類裡面加類似的程式碼,這顯然不環保. 由於這是個強需求,如果Java不允許修改意見載入的類,那麼Java無疑會被實現了這些feature其它技術所淘汰,基於這個反向推理,由於Java現在還那麼火,所以可以推測,Java應該支援這種feature.
載入時
為了實現上面這種需求,Java5就推出了java.lang.instrument
並且在jdk6進一步加強.
要實現一個類的轉換,我們需要執行如下步驟:
- 就像我們編寫Java程式入口
main
方法一樣,我們通過編寫一個public static void premain(String agentArgs, Instrumentation inst);
方法 - 然後再方法體裡面註冊一個
java.lang.instrument.ClassFileTransformer
- 然後實現這個transformer
- 然後將整個程式打包,並且在
META-INF/MANIFEST.MF
註明實現了premain方法的類名 - 最終在程式啟動的時候,
java -javaagent:myagent.jar
JVM就會載入myagent.jar中的META-INF/MANIFEST.MF
, 讀取Premain-Class
的值,並且載入我們的Premain-class
類,然後在main方法執行之前,執行這個方法,由於我們在方法體重註冊了transformer,這樣後續一旦有類在載入之前,都會先執行我們的transformer的transform方法,進行位元組碼增強.
在java.lang.instrument.ClassFileTransformer
的介面有一個方法
byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException;
我們可以利用一些位元組碼增強的類庫,對傳入的位元組碼陣列進行解析,然後修改,然後序列化成位元組碼,作為方法結果返回
常用的位元組碼增強類庫
- ASM
- cglib
- javassist
其中javassist因為API易於使用,且專案一直活躍,所以推薦使用.
執行時
Java也可以在類已經載入到記憶體中的情況,對類進行修改,不過這個修改有個限制,只能修改方法體的實現,不能對類的結構進行修改.
類似的eclipse以及IDEA的動態載入,就是這個原理.
Annotation Processing
執行時或者載入時的位元組碼增強,雖然牛逼,但是其有個致命性短板,它增加的方法,無法在編譯時被程式碼感知,也就是說,我們在執行時給MyObj
類增加的方法getSomeThing(Param param)
,無法在其它原始碼中,通過myObj.getSomeThing(param)
這種方式進行呼叫,而只能通過反射的方式進行呼叫,這無疑醜陋了很多.也許Java也是考慮到這種需求,才發明了Annotation Processing
這種編譯過程
Java編譯過程
如圖所示,Java的編譯過程分為三步
- Parse & Enter: 這一步主要負責將Java的原始碼解析成抽象語法樹(AST)
- Annotation Processing: 這一步就會執行使用者定義的AnnotationProcessing邏輯,生成
新的程式碼
/資源,然後重複執行過程1,直到沒有新的原始碼生成 - Analyse & Generate: 這一步才是真正的生成位元組碼的過程
這個編譯過程中,我們可以擴充套件的是,第二部,我們可以自己實現一個javax.annotation.processing.Processor
類,然後將這個類告訴編譯器,然後編譯器就會在編譯原始碼的時候,呼叫介面的process邏輯,我們就可以在這裡生成新的原始檔與資原始檔!
遺憾的是,編譯器並沒有顯示的API提供給我們,允許我們修改已有class的抽象語法樹,也就是說,我們無法在通過正規途徑
在編譯時
給一個類增加成員;這裡強調了正規途徑
是因為確認是存在一些非正規途徑,可以讓我們去修改這棵樹. lombok就是這麼做的
lombok是做什麼的?
lombok允許我們通過簡易的註解,來自動生成我們模型的getter,setter,constructor,toString等常用方法,可以讓我們的模型程式碼更加乾淨.
瞭解了上述的Java的編譯過程,我們其實就可以想想,是否可以通過程式碼生成的方式,來去掉我們平時詬病,卻一直難以根除的痛?
基於Annotation Processing的MybatisDAO & mapper檔案自動生成
分析
對於一個model而已,常用的操作包括以下幾種
- insert(model)
- selectByXXX(model)
- countByXXX(model)
- updateByXXXAndYYY(model)
- deleteByXXX(model)
如果僅僅提供model,是不是就足以生成對應的DAO介面申明以及對應mapper配置?
- 表名: 簡單點,可以直接根據模型名來推斷,也可以通過註解增加方法,來允許自定義表名
- insert/update的欄位列表: 直接去模型的欄位列表即可
- select/update/delete的時候,我們是需要知道我們根據什麼欄位進行過濾,這個資訊我們是需要告訴
Processor
的,因為我們可以考慮增加一個註解@Index
來告訴Processor
,這些欄位是索引欄位,可以根據這些欄位進行過濾
基於上面分析,我們有了以下大致思路
- 我們首先定義一個
@DAO
,用於標記我們的模型class - 然後定義一個
@Index
,用於標記模型的欄位 - 然後定義一個
DAOGeneratorProcessor
繼承自AbstractProcessor
,並且申明支援DAO
- process方法的實現中,我們會分析模型的語法書,提取出類名,欄位列表
- 找出標記了
@Index
的欄位列表,然後對涉及到過濾的方法生成所有的組合,比如- selectByOrderAndSellerId
- selectBySellerId
- selectByOrderNo
- 生成對應的介面宣告,以及mapper檔案
這種組合索引欄位,生成方法名的方式比較粗暴,比如如果有N個@Index欄位,對應的selectByXXX方法就會有
2**N
,大部分場景下,這個N都不會超過3個,比如訂單表,就是orderno,商品表,就是itemid
由於annotation是編譯器的擴充套件,這一點體驗比較好,一旦我們定義好了模型(比如Order.class),然後編譯模型,我們就可以在程式碼其它地方,就可以直接引用OrderDAO
這個物件類(這個類是生成的哦),可以回顧一下Java的編譯過程.
實踐
實踐中,雖然生成的DAO可以覆蓋我們大部分的用例,但是並不能覆蓋所有我們的需求場景,因此,我們推薦將生成的DAO統一叫做BasicDAO,這樣有些個性化的需求,我們仍然可以同自己書寫SQL的方式來自定義,這樣在解決重複冗餘的前提下,也能很好的適應複雜的業務場景.
總結
Java本身是一門靜態語言,程式從原始碼,到執行的程式,中間會經歷很多的環節.
這些環節都可以作為我們超程式設計的切入點,不同的環節,可以發揮不同的威力,使用得當,可以幫助我們提供生產力的同時,也能很好優化我們的程式碼效能。
相關文章
- 天河二號:中國的超級計算機及其超級應用計算機
- 函數語言程式設計及其在react中的應用函數程式設計React
- 設計模式及其在spring中的應用(含程式碼)設計模式Spring
- Java程式設計——伺服器設計方案之應用限流Java程式設計伺服器
- Java 組合模式及其應用Java模式
- Vector在Java程式設計中的應用 (轉)Java程式設計
- Java桌面應用程式設計:SWT 簡介(轉)Java程式設計
- Java併發程式設計:Synchronized及其實現原理Java程式設計synchronized
- Go Web 程式設計--超詳細的模板庫應用指南GoWeb程式設計
- 細說 Java 泛型及其應用Java泛型
- Java知多少(78)Java向量(Vector)及其應用Java
- Java8函數語言程式設計應用Java函數程式設計
- 實驗3 轉移指令跳轉原理及其簡單應用程式設計程式設計
- Java 併發程式設計:volatile的使用及其原理Java程式設計
- 設計複合應用程式:元件設計元件
- 設計複合應用程式:設計模式設計模式
- 設計模式之--策略模式及其在JDK中的應用設計模式JDK
- 設計模式學習筆記(十六)迭代器模式及其在Java 容器中的應用設計模式筆記Java
- Java中的非同步程式設計與CompletableFuture應用Java非同步程式設計
- 設計模式 - 命令模式詳解及其在JdbcTemplate中的應用設計模式JDBC
- 設計模式學習筆記(九)橋接模式及其應用設計模式筆記橋接
- 設計模式學習筆記(十二)享元模式及其應用設計模式筆記
- 世界級大牛對程式設計師超實用的程式設計箴言(上)程式設計師箴言
- 世界級大牛對程式設計師超實用的程式設計箴言(下)程式設計師箴言
- framebuffer應用程式設計實踐程式設計
- [譯] 設計大型 JavaScript 應用程式JavaScript
- ADO程式設計應用 (轉)程式設計
- Ajax及其應用
- 超讚!iOS7應用介面設計深度剖析iOS
- C++ 程式設計入門指南:深入瞭解 C++ 語言及其應用領域C++程式設計
- 用超程式設計來判斷STL型別程式設計型別
- 設計模式 - 迭代器模式詳解及其在ArrayList中的應用設計模式
- 設計模式學習筆記(十)裝飾器模式及其應用設計模式筆記
- 深入解析 Java 物件導向程式設計與類屬性應用Java物件程式設計
- CAS原子操作以及其在Java中的應用Java
- Java 註解及其在 Android 中的應用JavaAndroid
- Java學習--異常處理及其應用類Java
- Java 併發程式設計:ThreadLocal 的使用及其原始碼實現Java程式設計thread原始碼