理解Java 9 的模組. 模組是什麼及什麼時候要使用它們 - Medium

文章推薦指數: 80 %
投票人數:10人

本文中,我將介紹Java 9 Platform Module System (JPMS) [譯註:這專案名稱就不翻譯了,翻成中文也沒什麼意思],自Java 出現以來,最重要的新軟體工程 ... GetunlimitedaccessOpeninappHomeNotificationsListsStoriesWritePublishedinJavaMagazine翻譯系列理解Java9的模組模組是什麼及什麼時候要使用它們Translatedfrom“UnderstandingJava9Modules”ByPaulDeitel,JavaMagazineSeptember/October2017,page18–32.CopyrightOracleCorporation.本文中,我將介紹Java9PlatformModuleSystem(JPMS)[譯註:這專案名稱就不翻譯了,翻成中文也沒什麼意思],自Java出現以來,最重要的新軟體工程技術。

模組化(Modularity)—Jigsaw專案的延續—全面性幫助開發者在建置、維護及發展軟體系統更有效率,特別是開發大型系統時。

什麼是模組?模組化在套件(package)之上提供更高層次的聚合,關鍵的新語言元素是模組(module),具有獨一無二的名稱,可重複使用的相關套件組合,還有資源(圖片與XML檔案)及一個模組描述器說明模組的名稱模組的相依性(即這模組相依的其他模組)明確可以讓其他模組使用的套件(隱含著模組中其它套件是無法讓其他模組使用)提供的服務使用的服務允許什麼模組使用reflection歷史JavaSE平台從1995年到現在,有近千萬開發者用它建置任何事,從資源有限的設備—像是物聯網(IoT)或嵌入式裝置—的小應用程式,到大型關鍵商業系統及緊急任務系統。

大量的遺留程式碼在那邊,到目前為止,Java平台主要是單一一體式的解決方案,多年來,有許多努力試著模組化Java,但沒有一個被廣泛使用,也無法用來模組化Java平台。

模組化JavaSE平台的實現一直是具挑戰性的,且已經花了很多年的努力,最初在2005年針對Java7提出的JSR277:JavaModuleSystem,之後被JSR376:JavaPlatformModuleSystem取代,作為Java8的目標,JavaSE平台現在在Java9已模組化,但僅在Java9延遲到2017年九月之後[譯註:JSR376曾在JCP投票中被否決]。

目標根據JSR376,模組化JavaSE平台的關鍵目標有可靠的配置—模組提供機制明確宣告模組間的相依關係,能在編譯期與執行期加以識別,系統能通過這相依關係確保所有模組的子集合能滿足程式的需求。

嚴謹的封裝—模組中的套件只有在模組明確匯出它們時才能被其他模組存取,即使如此,其他的模組在沒聲明它需要其他模組的功能前亦不能使用那些套件。

這改善平台的安全性,因為潛在的功能只能存存取更少的類別,您會發現模組化的思考可以幫助您做出更簡潔、更符合邏輯的設計。

可擴展的Java平台—在之前,Java平台是一整個龐然大物,包含大量的套件,這使其難以開發、維護和發展,不容易取其子集合。

現在,平台被模組化成為95個模組(這數字可能隨著Java發展變化),您可以建立客製的執行環境,只包含您的應用程式或是您目標裝置所需的模組。

例如,如果一個裝置不支援GUI,您可以建立一個不含GUI模組的執行環境,大幅減少執行環境的大小[譯註:在將JRE一起打包的情境中特別有用]。

更佳的平台完整性—在Java9之前,使用平台中許多不預期被應用程式使用的類別是有可能的,在嚴謹的封裝下,這些內部API會被真正地封裝並讓使用平台的應用程式看不見。

如果您的程式依賴這些內部API,這可能會對轉移遺留程式碼到以模組化的Java9造成問題。

改善的性能—JVM使用各式優化技術改善應用程式的性能,JSR376指出,當事先知道某技術僅在特定模組中被使用,這些技術會更有效。

Table1.JavaModularityJEPsandJSRs列出JDK的模組Java9的一個重要觀念是將JDK切割成模組以支持各式配置(參閱JEP200:TheModularJDK,與Java模組化有關的JEP和JSR都在Table1中)。

使用JDKbin目錄中的java指令搭配--list-modules選項,如java--list-modules列出JDK的模組集合,其中包含實現Java語言SE規範的標準模組(名稱以java開頭)、JavaFX模組(名稱以javafx開頭)、JDK特定的模組(名稱以jdk開頭)及Oracle特定的模組(名稱以oracle開頭)。

每個模組名稱都接著一個版本字串—@9表示這模組屬於Java9。

模組宣告如我們提到,一個模組必須提供一個模組描述器—描述模組相依性、該模組能讓其他模組使用的套件等中介資料。

模組描述器是一個經過編譯的模組宣告,定義於module-info.java檔案中,每個模組宣告從關鍵字module開始,緊接著一個獨一無二的模組名稱,以及括在大括弧內的模組內容:modulemodulename{}模組宣告的主體可以是空的,或是包含各種模組指令(directive),像是requires、exports、provides…with、uses及opens(稍後會討論到每一個),如您稍後會見到,編譯模組宣告會建立模組描述器,存放在模組根目錄的module-info.class檔案中,這裡將簡單介紹每個模組指令,之後,我們將呈現實際的模組宣告。

我們稍後介紹的關鍵字exports、module、open、opens、provides、requires、uses、with以及to和transitive都是限制關鍵字,只能用在模組宣告中,仍可在您的程式中任何地方作為變數名稱。

requires。

require模組指令指定該模組依賴其他模組—這關係稱作模組相依性,每個模組必須明確聲明它的相依性,當模組A需要模組B,模組A稱為讀取模組B,模組B則是給模組A讀取。

要指定與另一個模組的相依性,使用requires,如下:requiresmodulename;亦有一個requiresstatic指令表示在編譯期間需要一個模組,但在執行期間是非必需的,這是所謂的可選相依性,不在本介紹中討論。

requirestransitive—隱含的可讀性,指定相依另一個模組,並確保其他模組讀取您的模組時亦能讀取這相依模組,所謂的隱含的可讀性,如下使用requirestransitive:requirestransitivemodulename;思考java.desktop模組宣告中的下列指令:requirestransitivejava.xml;在這情況下,任何讀取java.desktop的模組也隱含地能讀取java.xml。

例如:一個java.desktop模組中的函式還傳一個java.xml模組中的型別,那讀取java.desktop模組的程式變成依賴java.xml。

若java.desktop的模組宣告中沒有requirestransitive指令,相依的模組則無法編直到牠們明確讀取java.xml。

根據JSR379,JavaSE的標準模組必須在任何情況下給予隱含可讀性,如前所述般,另外,即便一個JavaSE標準模組可能依賴一個非標準模組,它不該給予隱含可讀性,這確保程式只依賴JavaSE標準模組,可以執行於任何JavaSE的實作版本中[譯註:OpenJDK或是IBM的實作版本]。

exports及exports…to。

exports指令指定模組中的某套件的public型別(及其內部的public及protected型別)可以被所有其他模組存取,而exports…to指令能讓您以逗號分隔的列表精確地指定哪些模組能存取匯出的套件—所謂有限制的匯出。

uses。

uses模組指令指定該模組使用的服務—讓此模組成為一個服務使用者。

服務是一個實作或繼承uses指令指定的介面或抽象類別的物件。

provides…with。

provides…with模組指令指定模組提供服務的實作—讓該模組成為一個服務提供者。

指令中provides的部分指定列在uses指令中的介面或抽象類別,with的部分指定實作介面或繼承抽象類別以提供服務的類別名稱。

open、opens及opens…to。

在Java9之前,reflection可以用來得知套件中所有的型別以及一個型別中所有的成員,即便是它私有的成員,不論您是否允許此能力,因此,沒有什麼是真正被封裝的。

模組系統的關鍵驅動力是可靠的封裝,預設情況下,一個模組中的型別是無法讓其他模組存取的,除非它是一個公開型別且您匯出他所屬的套件,您只需公開您希望公開的套件,在Java9中,這同樣適用於reflection。

只允許在執行期間能存取特定套件,open模組指令的形式:openspackage表示特定套件的public型別(及它內部的public和protected型別)只能在執行期間讓其他模組的程式存取。

同樣,特定套件中的所有型別(及其成員)可以透過reflection存取。

只允許在執行期間讓指定模組能存取特定套件,opens…to指令的形式:openspackagetocomma-separated-list-of-modules表示特定套件的public型別(及它內部的public和protected型別)只能在執行期間讓在條列中模組的程式所存取。

同樣,指定的模組可以透過reflection存取特定套件中的所有型別(及其成員)。

只允許在執行期間能存取模組中所有套件,如果某模組中的所有套件能在執行期間或透過reflection讓其他模組存取,您可以公開整個模組:openmodulemodulename{//moduledirectives}Reflection預設行為預設情況下,一個模組有在執行期間存取一個套件的能力時,能看到該套件中public型別(及其內部的public及protected型別)。

然而,其他模組可以存取公開套件中的所有型別,包含透過setAccessible存取私有成員,和先的Java版本相同,更多關於setAccessible和reflection的資訊請參考Oracle’s文件。

關係注入,reflection常用在注入關係,一個這類的例子是基於FXML的JavaFX應用程式[編按:關於更多使用FXML的資訊請參閱本期DefineCustomBehaviorinFXMLwithFXMLLoader],當一個FXML應用程式載入時,相依的controller物件與GUI元件以下列順序動態建立:首先,由於應用程式依賴一個controller物件處理GUI互動,FXMLLoader注入controller物件到執行中的應用程式—FXMLLoader利用reflection取得並載入controller的類別到記憶體中,然後建立該類別的物件實體。

接著,由於controller依賴宣告於FXML中的GUI元件,FXMLLoader建立宣告於FXML中的GUI元件,然後將它們注入到controller物件中,指派給有對應@FXML的成員變數,此外,用@FXML宣告的controller的事件處理程序會綁定到宣告於FXML的元件上。

當這步驟完成,controller可以和GUI互動並處理其事件,我們用opens…to指令允許FXMLLoader在JavaFX應用程式中透過reflection使用自訂模組。

模組化Welcome應用程式這一節中,我們建立一個簡單的Welcome應用程式說明模組的基礎,我們將建立一個位於模組中的類別提供一個模組宣告編譯模組宣告及Welcome類別成為一個模組執行在模組中有main函式的類別在涵蓋到這些基礎後,我們同樣示範將Welcome應用程式打包成一個模組化的JAR檔案從JAR檔案啟動應用程式Welcome應用程式的結構。

在這一節呈現的應用程式有二個.java檔案:包含Welcome應用程式主類別的Welcome.java,及包含模組宣告的module-info.java。

照慣例,一個模組化的應用程式有以下目錄結構:AppFoldersrcModuleNameFolderPackageFoldersJavaSourceCodeFilesmodule-info.java我們的應用程式將會放在com.deitel.welcome套件中,目錄結構如Figure1所示。

Figure1.FolderstructurefortheWelcomeappsrc目錄有應用程式所有原始碼,它包含模組的根目錄,名稱同模組名稱com.deitel.welcome(稍後討論模組命名),模組根目錄包含巢狀的目錄呈現套件的層次結構com/deitel/welcome以對應套件com.deitel.welcome。

最底層目錄包含Welcome.java,根目錄包含必要的模組宣告module-info.java。

模組命名慣例,如同套件名稱,模組名稱必須是唯一的,為確保獨一無二的套件名稱,您通常以您組織網域名稱的反轉作為前綴,我們的網域名稱是deitel.com,所以我們的套件名稱以com.deitel開始,比照慣例,模組名稱一樣用網域名稱反轉的慣例。

編譯時,如果有多個模組有相同的名稱會出現編譯錯誤,執行期間,若有多個模組有相同名稱則會拋出例外。

這例子中模組名稱和其所屬的套件名稱使用相同的名稱,因為這模組只有一個套件,這不是必需的,但是一個常見慣例,在一個模組化的應用程式中,Java將模組的名稱、套件名稱及套件中的型別名稱分開維護,所以允許模組名稱和套件名稱是相同的。

模組通常組合相關的套件,因此,這些套件的名稱時常有相同的部分,例如,有個模組包含這些套件:com.deitel.sample.firstpackage;com.deitel.sample.secondpackage;com.deitel.sample.thirdpackage;您通常會用套件共同的部分作為模組的名稱—com.deitel.sample。

如果沒有共同的部分,您可以選擇一個代表這模組用途的名稱,例如,java.base模組包含對Java程式來說是基礎的核心套件(譬如java.lang、java.io、java.time及java.util),java.sql模組包含透過JDBC與資料庫互動的套件(像是java.sql及javax.sql)。

這只是眾多標準模組的其中兩個,每個模組(例如java.base)的線上文件提供模組公開的套件列表。

Welcome類別。

下列程式碼是一個Welcome應用程式,簡單顯示一個字串在命令列上,當定義一個要被放在模組中的型別,每個型別必須被放在一個套件中(如程式中的第三行):模組宣告。

下列程式碼是module-info.java檔案中針對com.deitel.welcome模組的宣告。

再次,模組宣告由關鍵字module開始,緊接著模組的名稱以及由大括弧括住的本體,這模組宣告包含一個requires模組指令,表示這應用程式相依java.base模組中的型別,實際上,所有模組都相依java.base,所以這模組指令是隱含在所有模組宣告中,可以省略,如:編譯一個模組。

要編譯Welcome應用程式模組,開啟命令視窗,用cd指令進到WelcomeApp目錄,然後輸入:javac-dmods/com.deitel.welcome^src/com.deitel.welcome/module-info.java^src/com.deitel.welcome/com/deitel/welcome/Welcome.java[注意:^符號是MicrosoftWindows行接續字元,上述指令可以輸入成單獨一行沒有任何行接續字元,Linux和macOS的使用者,當要把指令拆成數行,可以把^替換成反斜線\]。

-d選項指示javac把編譯後的程式碼放到指定的目錄,這例子中,一個mods目錄包含名為com.deitel.welcome的子目錄,表示編譯的模組,mods是放模組的目錄的慣例命名。

編譯後Welcome程式的目錄結構。

若原始碼正確地編譯過,WelcomeApp目錄中mods的子目錄結構應包含編譯過後的程式(參見Figure2),這是所謂已展開模組(exploded-module)目錄,因為這資料夾及.class檔案不在JAR檔中。

已展開模組結構與上述應用程式的src目錄相似,很快我們將會把應用程式打包成一個JAR檔。

已展開模組目錄或模組化的JAR檔稱為模組成品,當編譯或執行模組化的程式時,可以被在模組路徑(一串模組成品的位置)中。

Figure2.Welcomeapp’smodsfolderstructure檢視模組描述器。

您可以使用java指令搭配--describe-module選項顯示com.deitel.welcome的模組,在WelcomeApp目錄輸入下列指令:java--module-pathmods--describe-modulecom.deitel.welcome輸出結果是:com.deitel.welcomepathContainingModsFolder/mods/com.deitel.welcome/requiresjava.basecontainscom.deitel.welcome輸出結果從模組的名稱和位置開始,剩下的部分顯示這模組需要標準模組java.base及包含一個套件com.deitel.welcome,雖然這模組包含此套件,但沒有被匯出,因此,它的內容無法被其他模組使用,這例子中,模組宣告明確要求java.base,所以前面的輸出包含requiresjava.base如果模組宣告中是以隱含的方式要求java.base,則列表會換成requiresjava.basemandated並沒有mandated模組指令,它出現在--describe-module的輸出中,單純指示所有的模組都相依於java.base。

從模組的已展開目錄啟動程式。

要從模組的已展開目錄啟動Welcome應用程式,在WelcomeApp目錄中輸入以下指令(同樣,macOS/Linux的使用者要將^換成\):java--module-pathmods^--modulecom.deitel.welcome/com.deitel.welcome.Welcome--module-path選項指定模組的位置,在這例子中是mods目錄,--module選項指定模組的名稱以及應用程式進入點(包含main的類別)完整的類別名稱,該程式執行後應顯示WelcometotheJavaPlatformModuleSystem!在前述的指令中,--module-path可縮寫成-p,而-module可縮寫成-m。

將模組打包成一個模組化的JAR檔。

您可以用jar指令將已展開的模組目錄打包成一個模組化的JAR檔包含模組的所有檔案,module-info.class包含在內,放在JAR裡的根目錄。

當啟動應用程式時,您在模組路徑中指定JAR檔,您希望放置輸出JAR檔的目錄須在執行jar指令前就存在。

如果一個模組包含應用程式地進入點,您可以用jar指令的--main-class選項指定該類別,例如:jar--create-fjars/com.deitel.welcome.jar^--main-classcom.deitel.welcome.Welcome^-Cmods/com.deitel.welcome.選項解釋如下:--create指示指令應該建立一個新的JAR檔-f指定JAR檔的名稱並緊接著著模組名稱,此例中,會在jars目錄中建立com.deitel.welcome.jar檔案。

--main-class指定程式進入點(包含main函式的類別)的完整名稱。

-C指定包含將被打包進JAR中的檔案的目錄,緊接著是要被包進去的檔案,那一點(.)是指所有目錄中的檔案都要被包進去。

從模組化的JAR檔啟動Welcome應用程式。

當您將一個應用程式放在一個模組化並已經指定進入點的JAR檔中,您可以如下的方式啟動程式:java--module-pathjarscom.deitel.welcome或是以縮寫的形式:java-pjars-mcom.deitel.welcome如果您建立JAR檔時未指定進入點,您仍可透過指定模組的名稱及類別的全命來啟動應用程式:java—module-pathjars^com.deitel.welcome/com.deitel.welcome.Welcome或是java-pjars-mcom.deitel.welcome/com.deitel.welcome.Welcome類別路徑(classpath)vs.模組路徑(modulepath)。

在Java9之前,編譯器與執行環境透過類別路徑(含有編譯後的Java類別的目錄或JAR檔的列表)尋找型別,在早期的Java版本中,類別路徑的定義是由CLASSPATH環境變數、JRE某個特殊目錄中的擴充,以及javac和java指令選項提供的資訊組成。

由於型別可能從不同的位置載入,那些路徑的搜尋順序造成脆弱的應用程式,例如:幾年前,我在我系統上安裝一個第三方廠商的應用程式,這應用程式的安裝管理員在JRE的擴充路徑中放了一個舊版的第三方Java函式庫,我系統上的幾個其他Java程式依賴該函式庫較新的版本,其中有額外的型別和該函式庫舊型別的改良版本,由於JRE擴充目錄中的類別會比在類別目錄中的類別早載入,相依於新版函式庫的應用程式停止運作,在執行期因NoClassDefFoundErrors及NoSuchMethodErrors而失敗,或是在應用程式已經執行很久後停止運作(更多關於類別載入的資訊請參閱UnderstandingExtensionClassLoading)。

模組與模組描述器提供的可靠配置有助於改善許多執行期間類別路徑引起的問題,每個模組明確聲明自己的相依關係,並在啟動時解決。

模組路徑中每個模組只會有一個,每個套件只能被定義在一個模組中,如果有兩個或多個模組有相同的名稱或匯出相同的套件,執行環境在啟動程式前立即中止。

learnmoreProjectJigsaw:ModuleSystemQuick-StartGuidePaulDeitel’sliveonlinecourseIntroductiontoModularitywiththeJava9PlatformModuleSystem(JPMS),availableatnoadditionalchargetoSafariBooksOnline.comsubscribersJava9Modularity:PatternsandPracticesforDevelopingMaintainableApplicationsbySanderMakandPaulBakker(O’ReillyMedia,2017)譯者的告白這一篇很長,不過看完很開心,當初看到JCP針對JPMS投票沒過時我有點意外,畢竟它解決我長久寫Java程式時一個苦惱的問題:我不想讓其他程式使用我內部的實作類別。

確實,就相依性管理上,Maven已經做得很出色了,JPMS加入的新方式,讓很多既有的建置工具(Maven或Gradle都要)和程式(Eclipse要額外安裝Java9Support)都需要搭配的調整(還好在執行既有舊程式上,Java9有做向下相容的處理),但我不覺得這是該讓Java止步不前的理由,Java是個不錯的語言,希望之後能導入更多語言特性,讓開發更具生產力。

MorefromJavaMagazine翻譯系列一系列JavaMagazine的文章翻譯ReadmorefromJavaMagazine翻譯系列AboutHelpTermsPrivacyGettheMediumappGetstartedDuSpirit567FollowersFollowHelpStatusWritersBlogCareersPrivacyTermsAboutKnowable



請為這篇文章評分?