2021年12月5日 星期日

1. 認識Java模組化

從Java 9開始,套件(package)可以使用模組(module)進行分類。在本章中我們將解釋模組的用途以及如何建立自己的模組。我們還將展示如何發現現有模組並執行它們。本章將涵蓋認證考試中需要了解的模組的基礎知識。

介紹模組(Module)

模組化的需求

一般書上範例或認證考試的範例通常都是小類別,但真正參與專案開發時,類別數量和內容將大得多。一個大型的專案會把數百甚至上千個類別規劃為套件(package),這些套件再群組為JAR(Java archive)檔案。這種檔案類型副檔名為.jar,是一個壓縮檔,可以使用7-zip等軟體打開並檢視內容。

此外,除了自家團隊開發的程式碼外,大多數應用程式還使用其他團隊編寫的程式碼。開源程式碼如Java更是明顯,很多都有合法授權與免費使用;這些程式碼蒐錄在函式庫內,並以JAR檔案的形式提供,可以用於讀取微軟的Office文件、連接到資料庫等。

一些開源專案或是函式庫也常依賴於其他開源專案的功能。比如Spring是一個常用的框架,JUnit是一個常用的測試函式庫。要使用任何一種,都需要先確保在執行時擁有所有相關JAR的相容版本。這些複雜的依賴關係和最低版本通常被開源社群(community)稱為JAR地獄(hell),因為一旦錯誤的函式庫版本被載入,輕則在執行時期拋出ClassNotFoundException,更麻煩的是一些隨機的異常,無法釐清是函式庫的bug還是不相容造成。

Java 9中導入了「Java Platform Module System (JPMS)」,以一個更高層級的觀點對程式碼進行分組,試圖解決Java從一開始就存在的一些困擾。模組化的主要目的是藉由群組相關的套件,以向開發人員提供一組特定的功能;它就像是一個可以讓開發人員設定開放那些套件的更大的JAR檔案,後續內容將說明模組化旨在解決那些問題。JPMS包括以下內容:

  1. 模組的JAR檔案格式。
  2. 模組化JDK套件。
  3. 提供模組化相關指令列(command-line)。

本章範例專案說明

本章範例是一個小型的Zoo應用程式。它原本只有一個類別,並且只列印出一些文字。現在我們有一大批程式開發人員,目標是讓動物園的運營可以自動化,因此必須開發很多功能,包括與動物的互動、訪客、公共網站和服務推廣。

模組可以是一個或多個套件加上一個檔名為module-info.java的特殊文件。下圖初步列出了Zoo專案可能需要的幾個模組,同時關注範例中模組間的相互作用:

 

專案Zoo的模組設計

完整的Zoo專案有更多模組。上圖僅列出4個:

  1. zoo.animal.feeding
  2. zoo.animal.care
  3. zoo.animal.talks
  4. zoo.staff

注意途中許多模組之間有箭頭,這些代表依賴關係,表示一個模組依賴於另一個模組中的程式碼。例如工作人員(staff)需要餵養(feed)動物以維持工作,因此從 zoo.staff到zoo.animal.feeding的箭頭顯示前者依賴於後者。

後續我們逐一深入研究這些模組。下圖顯示了zoo.animal.talks模組中的內容,一共有3個套件,每個套件有2個類別;還有一個特別的檔案module-info.java,每個模組都需要該檔案,將在本章後面更詳細地解釋這一點。

 

模組zoo.animal.talks

模組化的效益

模組是開發程式時需要了解的另一層內容。雖然是否模組化並非強制,但了解它們旨在解決的問題很重要,這也是認證考試的重點!

更好的存取控制

Java對類別提供傳統的4個層級存取控制,有private、package(default)、protected和public等修飾詞。這些級別的存取控制可以限制對某個類別或套件的存取,甚至可以允許存取子類別而不將它們暴露給外部。

但是,如果我們編寫了一些複雜的邏輯,但卻只想限制在某些套件中呢?例如我們希望zoo.animal.talks模組中的套件只對zoo.staff模組中的套件開放,其他模組的套件則拒絕存取。

開發者可能會嘗試建立一個供內部使用的套件zoo.animal.internal,或者再把需要隱藏的類別命名為Unsafe;但實際上只要存在一個外部套件可以存取套件zoo.animal.internal,就表示其他套件也可以;用類別名稱提醒開發者也只是參考性質,不具備實質約束。因此傳統的存取修飾詞無法處理這種情況。

模組藉由充當第五級存取控制來解決這個問題,可以將模組化的JAR中的套件公開給特定的其他套件。這種更強的封裝形式確實建立了內部套件,在本章稍後討論 module-info.java檔案時將予說明。

更清晰的依賴管理

函式庫之間互相依賴是很常見的,例如JUnit測試庫可以使用Hamcrest函式庫來改進斷言測試時的可讀性。

函式庫之間的相依性,通常只能由開發人員閱讀使用文件得知,或是在程式碼執行到相依流程時才因為函式庫不在類別路徑(classpath)上而拋出ClassNotFoundException的例外錯誤,這也是先前我們提到的JAR地獄的情境。

然而在一個完全模組化的環境中,每個開源專案都會在module-info.java檔案中指定專案的依賴項目。在啟動程式時Java會告知相依函式庫不在模組路徑(module path)中,所以開發者馬上就會清楚知道。

自定義Java構建(build)內容

Java開發工具包(Java Development Kit , JDK)相當龐大,即便是Java執行環境(Java Runtime Environment, JRE)都不小。以jdk-8u301-windows-x64.exe為例,大小為169.46 MB。為了能讓除了電腦之外的更多小裝置,如行動與嵌入式裝置等,都能安裝Java,在SE8時使用「緊實的配置文件 (compact profile)」,或是簡稱「profile」,以完整的Java SE平台API為基礎,精簡出3個層級的子集合。最精簡的是compact1,再多一點API之後為compact2,完整的Java SE平台API則為compact3,如此使用compact1就可以安裝在較小的存儲空的裝置上。不過這3個層級的安裝包只影響可用API數量,不影響JVM和一些Java工具。讀者若有興趣了解3個層級各自定義的API內容,可參考

https://docs.oracle.com/javase/8/docs/technotes/guides/compactprofiles/compactprofiles.html

然而這3個層級所需要的API種類畢竟是Java自己定義,運用到開發人員時可能又有不同。例如Java Native Interface (JNI)用於處理特定於作業系統的程式、JDBC用於資料庫存取,不見得所有特定層級的Java程式都會使用,因此籠統的3個層級畢竟還是缺乏靈活性。

使用JPMS的指令工具「jlink」讓開發人員可以自定義自己需要的API,這讓打包更小的執行映像檔(runtime image)變的可能;除了較小規模的API之外,這種方法還提高了安全性。如果不使用AWT套件且AWT存在安全漏洞,則打包沒有AWT的執行映像的應用程式將不存在AWT安全漏洞。

提升效能

模組化後由於Java知道需要哪些模組,因此在類別加載時可以只關注需要的模組。這改善了大型程式的啟動時間,並且減少記憶體的浪費。

雖然這些好處對於小程式來說似乎並不重要,但對於大型應用程式則舉足輕重,大型Web應用程式很容易花一分鐘的時間來啟動。此外對於某些金融應用程式,每一毫秒的性能都很重要。

避免套件重複

JAR地獄的常見另一種情境是同一個套件出現在2個JAR裡。導致這類問題的原因有很多,包括被重新命名的JAR導致專案內存在2個實質相同的JAR,或是在classpath上有2個JAR內容相同但版本不同。

JPMS可以避免這種情況,因為一個套件只允許由一個模組提供。在執行時不會再有關於套件的麻煩意外。

現有程式碼的模組化

雖然使用模組有很多好處,但要模組化現有的大型應用程式也需要大量工作, 特別是應用程式通常依賴尚未模組化的舊開源函式庫上。一旦需要模組化,就等同要清償所有技術債務。

雖然並非所有開源專案都已經模組化,但陸續增加中。可以參考以下網頁: https://github.com/sormuras/modules/blob/master/README.md

內容是以Maven Central的統計結果,同時有建議的模組化策略。


1 則留言: