探索 Roo 的架構

工程 | Ben Alex | 2009 年 6 月 18 日 | ...

上個月,我們發現使用 Spring Roo(我們為 Java 開發者提供的新生產力工具)構建一個功能齊全的企業應用只需幾分鐘時間,這非常容易。雖然許多 Java 開發者已經開始評估Roo,以幫助他們的專案節省時間,但我收到了很多來自好奇 Roo 實際工作原理的人的提問。在這篇部落格文章中,我將深入探討 Roo 的架構,包括其目標、原型備選方案、設計理念和實現細節。讀完本文,您將對 Roo 的運作機制及其方法為何對 Java 專案行之有效有一個很好的理解。

Roo 和 STS 新版本釋出

在深入探討 Roo 架構的細節之前,我應該簡要提及,我們今天釋出了 Spring Roo 1.0.0.M2。此新版本包含數十個 bug 修復和次要增強功能,還包括以下特性:

  • 非常出色的單元測試 Mock 能力(由 Rod Johnson 編寫)
  • Java 和 SQL 保留字檢測(這樣您就不會再意外地將欄位命名為“from”之類的名稱)
  • 能夠指定您希望使用的特定 Java 版本(對 Apple 使用者尤為重要)
  • 額外的 Spring Web Flow 配置(這樣您現在就可以使用一個合適的流程了)
  • 自動將動態查詢器暴露給 Web 層
  • 改進了對 Windows 使用者和非英語預設區域設定使用者的支援

自從我的上一篇部落格文章以來,我們還發布了 SpringSource Tool Suite (STS) 2.1.0.M2。STS 中對 Roo 的支援持續改進,您現在甚至可以將 STS 配置為指向單獨下載的 Roo 安裝。對於越來越多的編寫自己的 Roo 外掛或僅僅希望將最新 Roo 版本與 STS 一起使用的人來說,這是個好訊息。STS 中其他不錯的 Roo 功能包括 CTRL + R“Roo 命令”分派、內建的 Roo Shell、用於執行整合測試或部署(包括部署到雲環境!)的額外 Roo 命令等等。如果您還沒有下載 STS 2.1.0.M2,我強烈建議您下載。

Roo 核心與 Roo 外掛

Roo 的核心是一套允許使用“外掛”的核心服務。這些核心服務包括一個 shell、檔案系統管理器、檔案系統監視器、檔案撤銷功能、類路徑抽象、抽象語法樹(AST)解析和繫結、專案構建系統介面、元資料模型、程序管理、引導和實用工具服務。雖然我們稍後會間接探討其中一些核心服務,但終端使用者感興趣的絕大多數功能都來自外掛。如果沒有任何外掛,Roo 就只是一個複雜的控制檯。

當您下載 Roo 時,我們提供了核心服務以及一系列常用外掛。所有外掛都可以透過 JAR 名稱中出現的“addon”關鍵字來標識。隨 Roo 提供的每個外掛都是可選的,終端使用者可以自由地增強現有外掛或建立新外掛。實際上,我們非常歡迎社群開發和分享他們認為有用的外掛。

鑑於 Roo 的核心服務與使用者可能希望使用的外掛之間存在設計上的分離,我們在 Roo 1.0.0 中主要關注確保主流 Web 應用可以輕鬆高效地開發。在後續版本的 Roo 中,我們將提供越來越豐富的外掛,幫助使用者構建其他型別的應用。

我被問及頻繁的一個方面是 Roo 對 Maven 的使用。正如我在上一篇部落格文章中提到的,Roo 1.0.0 建立的專案使用 Maven。由於這種 Maven 使用是透過外掛實現的,因此很容易新增對其他專案構建系統的支援。實際上,我們收到了許多關於 Ant/Ivy 支援的請求,並且 Jira 中已經有一個關於此的功能請求(ROO-91)。

類似地,Roo 目前也提供了 JPAJSP 外掛。這兩者都是我們在 Roo 1.0.0 中為支援典型的 Web 應用開發而做出的實用選擇。從技術上講,沒有任何理由阻止開發 JDBCJDOJSFVelocityFreeMarker 外掛,我們希望隨著時間的推移能看到這些外掛出現。

由於這篇部落格文章專注於 Roo 的架構,我在此結束對單個外掛的討論。如果您想了解更多關於如何使用當前的 Roo 1.0.0 外掛構建應用的資訊,可以閱讀我的上一篇部落格文章。現在,讓我們更深入地探討一下 Roo 的實際工作原理。

Roo 的設計目標

無論何時評審任何技術,考慮影響其架構選擇的設計目標和目的都很重要。我在我的Roo 原創部落格文章中探討了其中一些目標,但我們在這裡更詳細地重新討論一下這個話題。

最重要的是,我們希望 Roo 成為 Java 開發者的生產力解決方案。有許多開發者更喜歡(或需要)使用 Java,而且 Java 仍然是全球使用最廣泛的程式語言。為這一龐大的開發者群體提供一流的生產力工具是 Roo 最基本的目標。

其次,我們希望確保消除採用 Roo 的障礙。如果人們不習慣(或根本不允許)使用一個很棒的生產力工具,那麼擁有它就沒有意義。具體來說,這意味著沒有鎖定(即容易移除 Roo),沒有執行時部分(以及許多組織中潛在的審批障礙),沒有不自然的開發技術,沒有 IDE 依賴,沒有許可成本,沒有奇怪的依賴使其工作,沒有陡峭的學習曲線,並且不對速度、效能或靈活性妥協。

第三,我們希望提供一個基於 Java 眾多優勢的解決方案。這些優勢包括極佳的執行時效能、標準可用性(如 JPABean ValidationRESTServlet API 等)、出色的 IDE 支援(如偵錯程式、程式碼輔助、重構等)、成熟的技術、型別安全,以及龐大的現有開發者知識、技能和經驗池(不僅包括 Java 本身,還包括 Spring、JSP、Hibernate 等事實上的 Java 構建模組)。

Roo 架構的備選方案

考慮到上述要求,我在 2008 年原型化了許多不同的技術,包括 JSR 269(Java 6 中的可插拔註解處理 API)、構建時原始碼生成、IDE 外掛、開發時位元組碼生成、執行時位元組碼生成以及高階反射方法(如 Spring Framework AOP 的擴充套件)。我沒有原型化其他 JVM 語言,因為 Roo 的核心動機是成為一個支援 Java 程式設計的工具。

我原型化的每種方法都或多或少存在問題,因此被排除。每種方法都需要特殊的執行時、特殊的 IDE 外掛或次優的構建步驟(或它們的組合)。大多數方法還會永久鎖定使用者,移除極其困難,從而造成了採用障礙,阻止許多 Java 開發者享受所提供的生產力提升。許多方法還依賴於執行時的反射技術,這會慢且難以除錯,並且大多數提供的 IDE 整合很少或根本沒有。我還特別偏愛提供一個輕量級的命令列工具,因為我堅信這將比 GUI 提供更好的可用性體驗。這就是我們沒有使用上述方法的原因。

Roo 架構概述

經過大量的原型設計,我們最終確定了 Roo 的架構,其關鍵要素包括:

  • 一個支援 Tab 補全、上下文感知、提示的命令列 Shell,使用者可以隨時載入和退出,並支援與文字編輯器和 IDE 同時使用
  • 使用 @Roo* 註解,這些註解只具有源級別保留(而非執行時保留)
  • 使用 AspectJ型別間宣告(ITDs,也稱為“引入”或“混合”),用於自動維護的 Java 成員(我們將在下面深入討論 ITDs)
  • 一個元資料模型,以便於輕鬆開發自定義 Roo 外掛(我們將在下面討論元資料模型)
  • 基於元資料模型和上述各種核心服務的完整雙向工程能力

這個架構不需要特殊的構建系統、執行時元件、IDE 外掛等。它也滿足了之前提到的所有設計要求。

Roo 的秘密武器

使這成為可能的“新想法”是自動將 ITDs 用作程式碼生成製品。以這種方式使用 ITDs 帶來了巨大的實際好處,因為它允許 Roo 生成的程式碼與開發者編寫的程式碼位於不同的編譯單元(即物理檔案)中。儘管位於單獨的檔案中,但在編譯時 ITDs 會被合併到同一個編譯後的 .class 檔案中。由於最終的類與開發者自己編寫所有程式碼所得的類本質上相同,因此傳統 Java 程式設計的所有優勢(如 IDE、偵錯程式支援、程式碼輔助、型別內省、型別安全等)都能正常工作。此外,由於編譯後的類只是一個 .class 檔案,一切在執行時都能完美執行。具體來說,您不必擔心諸如反射效能、記憶體使用、令人困惑且難以除錯的操作、可能需要審批和升級的額外庫等問題。

將 ITDs 用於程式碼生成的另一個令人興奮之處在於它實現了關注點分離。關注點分離使應用開發者受益,因為他們可以安全地忽略 Roo 建立的 ITD 檔案(因為開發者知道 Roo 會管理它們)。但是,關注點分離對 Roo 外掛也非常好。外掛的開發變得更加容易,因為外掛開發者知道他們控制著整個 ITD 編譯單元的內容。一個更微妙的好處是它提供的自動升級支援。在 Roo 的開發過程中,我們看到了許多這樣的例子:我們改進了一個外掛,然後隨後載入 Roo 的使用者會自動獲得一個升級後的 ITD。同樣,使用者可以從他們的環境中移除外掛,Roo 會自動移除相關的 ITDs。這是一種我們發現極其實用和有價值的技術。

ITDs 的最後一個主要好處是避免鎖定。正如我們稍後將看到的,ITDs 本質上是普通的 Java 原始檔。它們只是和所有其他原始碼一樣儲存在您的磁碟上,這意味著開發者可以選擇不再載入 Roo,而他們的專案仍然可以工作。那些想要更徹底移除的人可以使用諸如 Eclipse AJDT 的“推入重構”(push in refactoring)之類的功能。它的作用是將 ITDs 中的所有原始碼自動移動到正確的 Java 原始檔中。這意味著如果您不想再使用 Roo,只需對您的專案進行“推入重構”,您就擁有了一個完全正常的 Java 專案——就像您自己親手編寫所有程式碼一樣。這是個極好的訊息:

  • 只想快速啟動專案的人可以非常輕鬆地做到這一點,然後移除 Roo(另外,他們也可以隨時恢復使用 Roo,並且會正常工作)
  • 希望長期使用 Roo 以提高生產力的人可以完全放心地使用,因為他們知道將來只需點選幾下滑鼠就可以非常輕鬆地移除它

Roo 使用 AspectJ 提供的 ITDs。SpringSource 是 AspectJ 的大力支持者和使用者,以下是我們認為它非常適合基於 Roo 的專案的一些原因:

  • AspectJ 是一個活躍的專案,擁有龐大的社群
  • AspectJ 成熟、可靠且健壯,其起源可追溯到 2001 年的 PARC
  • AspectJ 得到了 Maven、Ant 和 IDE 等主流技術的廣泛支援
  • 使用 AspectJ 提供了現有的 IDE 支援,我們無需編寫額外的外掛
  • 我們的研究表明,大約有一半的 Spring 使用者無論如何都在使用 AspectJ
  • AspectJ 在執行時不使用(需要 AspectJ 執行時 JAR,但這自 Spring 2.0 起就是 Spring Framework 的依賴,因此已被使用 Spring 2.0 及以上版本的組織批准)
  • AspectJ 在構建時執行,因此確保 Java 的效能和永久代空間不受到影響
  • Roo 的 ITD 使用模式是自動、透明的,無需使用者具備任何 AspectJ(或 ITD)知識、技能或經驗
  • 如果開發者希望採用,使用 AspectJ 允許使用更高階的程式設計模式,例如領域驅動設計(DDD)和強制性方面(enforcement aspects)
  • SpringSource 聘請了 AspectJ (Andy Clement) 和 AJDT (Andrew Eisenberg) 的現任領導者,以及備受尊敬的 AspectJ 專家(如 Ramnivas Laddard 和 Adrian Colyer),因此我們知道我們擁有相當的內部技能來確保 AspectJ 能與 Roo 良好協作
  • SpringSource 的許多其他經過生產驗證的技術也基於或支援 AspectJ,包括 Spring FrameworkSpring SecuritySpringSource 應用管理套件SpringSource dm ServerSpringSource tc Server

Roo 用法詳情

讓我們透過建立一個新專案來探索 Roo 的 ITD 用法和元資料模型。假設您已經安裝了 Roo 1.0.0.M2,讓我們為我們的專案建立一個新目錄並啟動 Roo

$ mkdir architecture
$ cd architecture
$ roo

收到歡迎屏幕後,輸入以下命令

roo> project --topLevelPackage com.hello
roo> persistence setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY
roo> entity --name World 
roo> field string name 

圖形介面您的螢幕將看起來像這樣

first-commands

現在讓我們開啟一個文字編輯器,檢視 World.java 檔案內部


package com.hello; 

import javax.persistence.Entity; 
import org.springframework.roo.addon.javabean.RooJavaBean; 
import org.springframework.roo.addon.tostring.RooToString; 
import org.springframework.roo.addon.entity.RooEntity; 

@Entity 
@RooJavaBean 
@RooToString 
@RooEntity 
public class World { 
   private String name; 
} 

如所示,有幾個 @Roo* 註解。這些註解包含在 Roo 外掛中,指示 Roo 在需要時建立一個 ITD。@RooEntity 註解表示您希望 Roo 自動提供典型的 JPA 方法和欄位(包括識別符號和版本屬性)。@RooJavaBean 請求為每個欄位建立 getter 和 setter 方法。@RooToString 請求建立一個 toString() 方法。

Roo 建立的所有 ITDs 都遵循特定的命名約定。約定是 SimpleTypeName + "_Roo_" + AddOnSpecificKeyword + ".aj"。Roo 自動確保所有符合此格式的檔案都由相關的外掛妥善管理。如果某個特定關鍵字沒有安裝外掛,Roo 將刪除孤立的 ITD 檔案。這確保您可以隨時更改外掛配置,而無需手動清理。

讓我們看看 World_Roo_ToString.aj ITD 的內部


package com.hello; 

privileged aspect World_Roo_ToString { 

    public String World.toString() {    
        StringBuilder sb = new StringBuilder();        
        sb.append("id: ").append(getId()).append(", ");        
        sb.append("version: ").append(getVersion()).append(", ");        
        sb.append("name: ").append(getName());        
        return sb.toString();        
    }    
    
} 

正如您所見,一個 ITD 看起來就像一個普通的 Java 原始檔。只有一個區別:在方法簽名中,“toString()”方法名之前有一個“World.”字首。這指示 AspectJ 在編譯時將 toString() 方法引入 World.class 檔案。正如您所見,即使您之前從未接觸過 ITDs,它們也非常簡單。特別是,不需要任何切入點(pointcuts)。

讓我們編輯 World.java 檔案並向其中新增另一個欄位


private String comment;

如果您讓 Roo 保持執行,只要您儲存 World.java,您就會注意到它會立即修改 World_Roo_JavaBean.aj 和 World_Roo_ToString.aj 檔案。這是因為 Roo 監控檔案系統,檢測您在 Roo Shell 之外進行的任何更改,例如透過您偏好的 IDE。如果您願意,您也可以使用 Roo 的“add field string”命令。

如果您沒有執行 Roo,下次載入它時,會執行一個自動的啟動時掃描。這包括在相關外掛升級時自動升級任何現有的 ITDs(甚至在外掛不再存在時刪除 ITD)。重點是這一切都是自動且自然地發生,您無需擔心遵循關於 Roo 何時必須執行或如何更改檔案等的特殊規則和限制。

自定義 Roo 生成的內容

所有 @Roo* 註解都允許您控制使用的成員名稱,並允許您自己提供成員。讓我們編輯 World.java 檔案,並將 @RooToString 註解更改為


@RooToString(toStringMethod="rooIsFun")

如果您現在檢視 World_Roo_ToString.aj 檔案,您會看到方法名已自動更改


package com.hello; 

privileged aspect World_Roo_ToString { 
    
    public String World.rooIsFun() {    
        StringBuilder sb = new StringBuilder();        
        sb.append("id: ").append(getId()).append(", ");        
        sb.append("version: ").append(getVersion()).append(", ");        
        sb.append("comment: ").append(getComment()).append(", ");        
        sb.append("name: ").append(getName());        
        return sb.toString();        
    }    
    
} 

假設您不喜歡 Roo 的 toString() 方法(現在是 rooIsFun(),請記住!)。您有兩種方法可以移除它。您可以在 World.java 檔案中刪除或註釋掉 @RooToString 註解,或者您可以直接在 World.java 中提供您自己的 rooIsFun() 方法。您可以隨意嘗試這兩種技術。在這兩種情況下,您都會看到 Roo 自動刪除了 World_Roo_ToString.aj 檔案,因為它檢測到您不再需要 Roo 為您提供該方法了。這反映了 Roo 的方法:您始終擁有完全的控制權,並且沒有任何意外。

元資料模型

雖然您不需要了解 Roo 的內部工作原理就可以簡單地使用 Roo,但好奇的讀者可能想知道 World_Roo_ToString.aj 檔案怎麼會知道存在 getId()、getVersion()、getComment() 和 getName() 方法。考慮到這些方法甚至不在 World.java 檔案中,這尤其有趣。讓我們進一步探討這一點。

在 Roo Shell 中,輸入以下命令

roo> metadata for type --type com.hello.World

結果螢幕應該類似於

metadata

這是對 Roo 內部表示的 World.java 型別的總結。它是透過對 World.java 檔案進行 AST 解析和繫結構建的。您可能已經注意到列出了下游依賴項。這些代表了其他元資料項,它們希望在 World.java 元資料發生變化時收到通知。外掛通常會監聽其他元資料項的變化,然後相應地修改 ITDs(或 XML 檔案或 JSPs 等)。

您可以透過鍵入“metadata trace 1”,然後更改 World.java 檔案來觀察元資料事件通知的發生。通知訊息將類似於以下內容

tracing

在結束對 Roo 元資料模型的介紹之前,我要指出的是,Roo 不需要將元資料保留在記憶體中。這確保了非常大的專案仍然可以使用 Roo,而不會耗盡記憶體。Roo 會自動跟蹤快取統計資訊以及單個外掛的執行時配置檔案。記憶體充足的系統將享受到自動的LRU快取。如果您對 LRU 快取統計資訊感到好奇,可以透過“metadata status”命令檢視(請注意快取命中率令人滿意地高)。

roo> metadata status 
2: org.springframework.roo.addon.configurable.ConfigurableMetadata
5: org.springframework.roo.addon.javabean.JavaBeanMetadata
8: org.springframework.roo.addon.finder.FinderMetadata
35: org.springframework.roo.addon.plural.PluralMetadata
53: org.springframework.roo.addon.beaninfo.BeanInfoMetadata
64: org.springframework.roo.addon.entity.EntityMetadata
124: org.springframework.roo.addon.tostring.ToStringMetadata
862: org.springframework.roo.process.manager.internal.DefaultFileManager
[DefaultMetadataService@6030f9 providers = 14, validGets = 369, cachePuts = 17, cacheHits = 352, cacheMisses = 17, cacheEvictions = 0, cacheCurrentSize = 6, cacheMaximumSize = 1000]

結論

我希望您發現關於 Roo 工作原理的討論很有趣。我們已經看到 Roo 使用 ITDs 為 Java 開發者實現了可持續的生產力提升。我們探討了 Roo 的 ITD 方法的優勢,並深入瞭解了它的實際工作原理,包括如何自定義它們,它們如何在元資料層面運作,以及它們的生命週期如何透明且自動地與外掛升級關聯。我們還討論了 ITDs 如何在提供成熟且經過驗證的關注點分離的同時,避免鎖定、執行時影響以及在大型實際專案中重要的其他微妙問題。最後,我們回顧了 Roo 的元資料系統,並探討了它的一些事件通知、型別內省和可伸縮性特性。

我們期待支援社群參與 Roo 並開發新的外掛。我們邀請您試用 Roo,並且非常歡迎您的反饋bug 報告功能建議評論。希望您喜歡使用Roo

獲取 Spring 時事通訊

訂閱 Spring 時事通訊,保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速前進。

瞭解更多

獲取支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一個簡單的訂閱。

瞭解更多

近期活動

檢視 Spring 社群所有即將到來的活動。

檢視全部