Java中的動態代碼生成

jopen 10年前發布 | 80K 次閱讀 Java Java開發

通過程序來生成代碼是Java平臺的固有特性。當Java程序編譯的時候,Java編譯器生成的是字節碼而不是可執行程序。字節碼是Java特有的格式,它本身并沒有太大的用處。為了能執行字節碼,它會在運行時被JVM的just-in-time編譯器翻譯成本地的機器代碼。

Java的導論就先講到這吧。大多數Java開發人員應該都聽說過JIT編譯,但它作為這個平臺最強大的功能之一,即便你不了解它的細節,也不影響你正常寫你的Java程序。

然而隨著POJO革命的進行,Java領域流行起了另一種代碼生成的形式。許多現代的Java庫和框架都在Java程序運行時通過定義自己的類來實現了很多技巧。

乍一聽,這個很有點學院派的感覺。不過檢查下你的業務應用的棧跟蹤信息吧,你肯定會發現許多運行時生成的類。并且除了JIT編譯代碼以外,運行時生成的類也是你程序運行的一部分,因此它也是你需要關心的。

為什么我們需要運行時代碼生成?

運行時的類定義并非意味著幫那些懶得敲代碼的人省了點事。運行時代碼生成解決的是Java類型系統的一個很大的短板。在深入細節之前,我們先來簡單回顧下它的類型系統有哪些特性。

Java是強類型并且是靜態類型的。這么說的話有的程序員會感覺迷糊。我們先跳過這個到處都在談論的“動態及靜態類型的比較”,先假設我們都喜歡強類型和靜態類型。這個類型的一大好處就是它的表現力。對于每個變量而言,我們可以立馬說出它有什么方法可以被調用。

只要我們不去強制進行類型轉化,靜態類型在編譯期就可以暴露許多程序的錯誤,甚至都不用啟動你的應用。

這個安全性對我們來說非常方便。然而,對于那些寫Java框架和庫的人來說,有時候就不那么方便了。靜態類型意味著應用只能調用它所明確知道的方法。

但這樣就和框架的初衷相悖了,它的目的是能不依賴特定的用戶領域來提供功能。對于自己開發的公司內部的框架而言,直接依賴于某個特定的領域模型這么做可能還可以接受。

然而,想像下像Spring這樣的框架,它得依賴于所有的用戶領域類型。這在邏輯上幾乎是不太可能的,框架的依賴關系圖和框架實際的目的是相左的。第三方代碼要去依賴框架的功能。而不是相反的方式。

Java反射來救場了

當然了,避免類似的編譯期的問題正是Java反射API在教科書上的經典案例。事實上,Java的反射比它的名聲其實要好得多。

Java反射的確會造成一定的運行時開銷。但是,這樣的開銷通常都是方法查找時產生的。盡管查找的方法可以內部緩存起來,但Method類的協議約定了方法實例必須是可訪問的,也就是說得是可修改的。這需要在查找方法時返回這些緩存的Method對象的淺拷貝,因此我們希望避免重復地去創建這些拷貝。

然而,一旦你獲取到一個方法的實例了,如果它運行的次數足夠多,Java運行時會去優化這些反射的調用。這個概念又被稱為膨脹,這也是代碼生成所順帶實現的一個功能。最后,其實反射調用通常并不會導致性能瓶頸,盡管流傳著許多這樣的謠言,但那都是老版本的Java上面的事了。

框架使用反射來和用戶的代碼進行交互確是一種解決方案。但是當用戶程序需要織入到框架中的時候,反射就變得沒那么有吸引力了。我們來考慮一個簡單的案例,讓這個問題更清晰一點。假設我們要實現一個非常基礎的,注解驅動的安全框架。這個框架是由一個注解來管理的。

@Retention(RetentionPolicy.RUNTIME)
@interface Secured { 
  String requiredUser();
}

使用這個迷你框架的時候,用戶可以用@Secured來注解方法以便使得這些方法只能在特定用戶登錄的條件下才能被調用。不過我們如何實現這個規則?簡單的校驗用戶的方法就是讀取方法的注解,將它和當前登錄用戶的狀態進行比較。使用反射可以很容易實現僅當正確的用戶登錄后才調用這個方法。但我們如何才能在框架外讓用戶代碼能訪問到這個邏輯?好吧,我們可以在框架的一個對象內封裝這次調用,通過傳遞一個被封裝方法參數的數組來調用這個方法。

interface SecuredMethod {
  Object invoke(Object… args);
}

POJO啟示

這樣做很簡單,我們也已經搞定了。真是這樣的嗎?這個方法確實是可行的,但我們實現的這個API估計只有它老媽才會喜歡它。

首先來說,當使用這個庫的實現時,用戶需要顯式地添加一個安全檢查到方法調用上。因此,安全庫會侵入到用戶的代碼里,如果不小心直接調用了這個方法,就會破壞掉這個注解過的方法的安全。這種東西你可不太愿意把它部署到生產環境去。更糟糕的是,選擇這個實現我們會破壞了Java引以為傲的類型安全。由于這個方法的簽名比較通用,我們現在可以使用任何參數來調用這個SecuredMethod接口,Java編譯器也不會去警告我們。同時,后面我們還得一頭扎到棧信息中去分析這里產生的運行時異常。

那么,還有什么方法?好吧,既然你讀的是一篇關于代碼生成的文章,你會猜到Java類的動態生成應該是一個辦法吧。JVM不允許通過任何打補丁的方式來增強一個方法的實現,但你可以利用語言的特性來在子類中去重寫方法。多虧了Java的動態方法分發,這使得你可以通過在運行時生成一個子類,把框架的任意功能給注入到用戶代碼中。并且方便的是,通過super你可以很簡單就能調用到用戶的方法。

有了這個方法,我們可以按需生成任意類型的子類來實現我們所說的這個安全庫了。我們可以將安全校驗的代碼注入到用戶代碼中,只有當我們確保的確是合法的時候才會實際去調用那個方法。這么做的話,我們發布的安全庫就只需要一個簡單的接口就好了:

interface SecurityLibrary {
  <T> Class<? extends T> secure(Class<T> instance);
}

通過代碼生成,我們可以很容易將某個SecuredUserType繼承UserType來成為它的子類,我們會去重寫這個方法并且實現安全校驗的邏輯。如果安全校驗通過了,這個方法調用會委托到父類的方法中,那里會包含實際的處理邏輯。

最終我們實現了一個POJO框架的基礎版本。沒有能比這個更透明的安全庫了。代碼生成的最大的好處在于它可以完整地保全用戶的類型。想像下這個API能讓你的生活變得多輕松。

由于它不依賴于框架的類型,這使得你可以不用mock框架代碼也可以寫出單元測試。如果你的需求變了,把這個安全庫替換掉也就和替換掉方法上的注解一樣非常簡單。如果你觀察下周圍你會發現其實許多應用框架走的都是這條路子。

未完待續。

原創文章轉載請注明出處:Java中的動態代碼生成

英文原文鏈接

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!