Java 9模塊化開發(fā)(核心原則與實踐)第一部分_第1頁
Java 9模塊化開發(fā)(核心原則與實踐)第一部分_第2頁
Java 9模塊化開發(fā)(核心原則與實踐)第一部分_第3頁
Java 9模塊化開發(fā)(核心原則與實踐)第一部分_第4頁
Java 9模塊化開發(fā)(核心原則與實踐)第一部分_第5頁
已閱讀5頁,還剩110頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)

文檔簡介

Java9模塊化開發(fā)核心原則與實踐(第一部分)目錄\h第一部分Java模塊系統(tǒng)介紹\h第1章模塊化概述\h1.1什么是模塊化\h1.2在Java9之前\h1.3Java9模塊\h第2章模塊和模塊化JDK\h2.1模塊化JDK\h2.2模塊描述符\h2.3可讀性\h2.4可訪問性\h2.5隱式可讀性\h2.6限制導(dǎo)出\h2.7模塊解析和模塊路徑\h2.8在不使用模塊的情況下使用模塊化JDK\h第3章使用模塊\h3.1第一個模塊\h3.2任何模塊都不是一座孤島\h3.3使用平臺模塊\h3.4封裝的限制\h第4章服務(wù)\h4.1工廠模式\h4.2用于實現(xiàn)隱藏的服務(wù)\h4.3工廠模式回顧\h4.4默認服務(wù)實現(xiàn)\h4.5服務(wù)實現(xiàn)的選擇\h4.6具有服務(wù)綁定的模塊解析\h4.7服務(wù)和鏈接\h第5章模塊化模式\h5.1確定模塊邊界\h5.2精益化模塊\h5.3API模塊\h5.4聚合器模塊\h5.5避免循環(huán)依賴\h5.6可選的依賴關(guān)系\h5.7版本化模塊\h5.8資源封裝\h第6章高級模塊化模式\h6.1重溫強封裝性\h6.2對模塊的反射\h6.3容器應(yīng)用程序模式第一部分Java模塊系統(tǒng)介紹第1章模塊化概述

你是否曾經(jīng)因為困惑而不停地撓頭,并問自己:“代碼為什么在這個位置?它們?nèi)绾闻c其他龐大的代碼塊相關(guān)聯(lián)?我該從哪里開始呢?”或者在看完應(yīng)用程序代碼所捆綁的大量Java歸檔文件(JavaArchive,JAR)后,目光是否會呆滯?答案是肯定的。構(gòu)建大型代碼庫是一項被低估的技術(shù)。這既不是一個新問題,也不是Java所特有的。然而,Java一直是構(gòu)建大型應(yīng)用程序的主流語言之一,且通常會大量使用Java生態(tài)系統(tǒng)中的許多庫。在這種情況下,系統(tǒng)可能會超出我們理解和有效開發(fā)的能力范圍。經(jīng)驗表明,從長期來看,缺乏結(jié)構(gòu)性所付出的代價是非常高的。模塊化是用來管理和減少這種復(fù)雜性的技術(shù)之一。Java9引入了一個新的模塊系統(tǒng),從而可以更容易地創(chuàng)建和訪問模塊。該系統(tǒng)建立在Java已用于模塊化開發(fā)的抽象基礎(chǔ)之上。從某種意義上講,它促使了現(xiàn)有的大型Java開發(fā)最佳實踐成為Java語言的一部分。Java模塊系統(tǒng)對Java開發(fā)產(chǎn)生了深刻的影響。它代表了模塊化成為整個Java平臺高級功能這一根本轉(zhuǎn)變,從根本上解決了模塊化的問題,對語言、JVM(JavaVirtualMachine,Java虛擬機)以及標(biāo)準(zhǔn)庫都進行了更改。雖然完成這些更改付出了巨大的努力,但卻并不像Java8中添加流和Lambda表達式那樣“華而不實”。諸如Lambda表達式之類的功能與Java模塊系統(tǒng)之間還存在另一個根本區(qū)別。模塊系統(tǒng)關(guān)注的是整個應(yīng)用程序的大型結(jié)構(gòu),而將內(nèi)部類轉(zhuǎn)換為一個Lambda表達式則是單個類中相當(dāng)小的局部變化。對一個應(yīng)用程序進行模塊化會影響設(shè)計、編譯、打包、部署等過程,顯然,這不僅僅是另一種語言功能。隨著新Java版本的發(fā)布,你可能會迫不及待地想使用這個新功能了。為了充分利用模塊系統(tǒng),首先后退一步,先了解一下什么是模塊化,更重要的是為什么要關(guān)注模塊化。1.1什么是模塊化到目前為止,所討論的只是實現(xiàn)模塊化的目標(biāo)(管理和減少復(fù)雜性),卻沒有介紹實現(xiàn)模塊化需要什么?從本質(zhì)上講,模塊化(modularization)是指將系統(tǒng)分解成獨立且相互連接的模塊的行為。模塊(module)是包含代碼的可識別工件,使用了元數(shù)據(jù)來描述模塊及其與其他模塊的關(guān)系。在理想情況下,這些工件從編譯時到運行時都是可識別的。一個應(yīng)用程序由多個模塊協(xié)作組成。因此,模塊對代碼進行了分組,但不僅于此。模塊必須遵循以下三個核心原則:1.強封裝性一個模塊必須能夠?qū)ζ渌K隱藏其部分代碼。這樣一來,就可以在可公開使用的代碼和被視為內(nèi)部實現(xiàn)細節(jié)的代碼之間劃定一條清晰的界限,從而防止模塊之間發(fā)生意外或不必要的耦合,即無法使用被封裝的內(nèi)容。因此,可以在不影響模塊用戶的情況下自由地對封裝代碼進行更改。2.定義良好的接口雖然封裝是很好的做法,但如果模塊需要一起工作,那么就不能將所有的內(nèi)容都進行封裝。從定義上講,沒有封裝的代碼是模塊公共API的一部分。由于其他模塊可以使用這些公共代碼,因此必須非常小心地管理它們。未封裝代碼中任何一個重大更改都可能會破壞依賴該代碼的其他模塊。因此,模塊應(yīng)該向其他模塊公開定義良好且穩(wěn)定的接口。3.顯式依賴一個模塊通常需要使用其他模塊來完成自己的工作,這些依賴關(guān)系必須是模塊定義的一部分,以便使模塊能夠獨立運行。顯式依賴會產(chǎn)生一個模塊圖:節(jié)點表示模塊,而邊緣表示模塊之間的依賴關(guān)系。擁有模塊圖對于了解應(yīng)用程序以及運行所有必要的模塊是非常重要的。它為模塊的可靠配置提供了基礎(chǔ)。模塊具有靈活性、可理解性和可重用性。模塊可以靈活地組合成不同的配置,利用顯式依賴來確保一切工作正常。封裝確保不必知道實現(xiàn)細節(jié),也不會造成無意識間對這些細節(jié)的依賴。如果想要使用一個模塊,只需知道其公共API就可以了。此外,如果模塊在封裝了實現(xiàn)細節(jié)的同時公開了定義良好的接口,那么就可以很容易地使用符合相同API的實現(xiàn)過程來替換被封裝的實現(xiàn)過程。模塊化應(yīng)用程序擁有諸多優(yōu)點。經(jīng)驗豐富的開發(fā)人員都知道使用非模塊化的代碼庫會發(fā)生什么事情。諸如意大利面架構(gòu)(spaghettiarchitecture)、凌亂的巨石(messymonolith)以及大泥球(bigballofmud)之類的術(shù)語都描述了由此所帶來的痛苦。但模塊化也不是一種萬能的方法。它是一種架構(gòu)原則,如果使用正確則可以在很大程度上防止上述問題的產(chǎn)生。也就是說,本節(jié)中所提供的模塊化定義是刻意抽象化的,這可能會讓你想到基于組件的開發(fā)(20世紀(jì)曾經(jīng)風(fēng)靡一時)、面向服務(wù)的體系結(jié)構(gòu)或當(dāng)前的微服務(wù)架構(gòu)。事實上,這些范例都試圖在各種抽象層面上解決類似問題。在Java中實現(xiàn)模塊需要什么呢?建議先花時間思考一下在Java中已經(jīng)存在哪些模塊化的核心原則以及缺少哪些原則。思考完了嗎?如果想好了,就可以進入下一節(jié)的學(xué)習(xí)。1.2在Java9之前Java可用于開發(fā)各種類型和規(guī)模的應(yīng)用程序,開發(fā)包含數(shù)百萬行代碼的應(yīng)用程序也是很常見的。顯然,在構(gòu)建大規(guī)模系統(tǒng)方面,Java已經(jīng)做了一些正確的事情——即使在Java9出現(xiàn)之前。讓我們再來看一下Java9模塊系統(tǒng)出現(xiàn)之前Java模塊化的三個核心原則。通過組合使用包(package)和訪問修飾符(比如private、protected或public),可以實現(xiàn)類型封裝。例如,如果將一個類設(shè)置為protected,那么就可以防止其他類訪問該類,除非這些類與該類位于相同的包中。但這樣做會產(chǎn)生一個有趣的問題:如果想從組件的另一個包中訪問該類,同時仍然防止其他類使用該類,那么應(yīng)該怎么做呢?事實是無法做到。當(dāng)然,可以讓類公開,但公開意味著對系統(tǒng)中的所有類型都是公開的,也就意味著沒有封裝。雖然可以將類放置到.impl或.internal包中,從而暗示使用此類是不明智的,但誰會在意呢?只要類可用,人們就會使用它。因此沒有辦法隱藏這樣的實現(xiàn)包。在定義良好接口方面,Java自誕生以來就一直做得很好。你可能已經(jīng)猜到了,我們所談?wù)摰氖荍ava自己的interface關(guān)鍵字。公開公共接口是一種經(jīng)過驗證的方法。它同時將實現(xiàn)類隱藏在工廠類后面或通過依賴注入完成。正如在本書中所看到的,接口在模塊系統(tǒng)中起到了核心作用。顯式依賴是事情開始出問題的地方。Java確實使用了顯式的import語句。但不幸的是,從嚴(yán)格意義上講,這些導(dǎo)入是編譯時結(jié)構(gòu),一旦將代碼打包到JAR中,就無法確定哪些JAR包含當(dāng)前JAR運行所需的類型。事實上,這個問題非常糟糕,許多外部工具與Java語言一起發(fā)展以解決這個問題。以下欄目提供了更多的細節(jié)。用來管理依賴關(guān)系的外部工具:Maven和OSGiMaven使用Maven構(gòu)建工具所解決的一個問題是實現(xiàn)編譯時依賴關(guān)系管理。JAR之間的依賴關(guān)系在一個外部的POM(ProjectObjectModel,項目對象模型)文件中定義。Maven真正成功之處不在于構(gòu)建工具本身,而是生成了一個名為MavenCentral的規(guī)范存儲庫。幾乎所有的Java庫都與它們的POM一起發(fā)布到MavenCentral。各種其他構(gòu)建工具,比如Gradle或Ant(使用Ivy)都使用相同的存儲庫和元數(shù)據(jù),它們在編譯時會自動解析(傳遞)依賴關(guān)系。OSGiMaven在編譯時做了什么,OSGi在運行時就會做什么。OSGi要求將導(dǎo)入的包在JAR中列為元數(shù)據(jù),稱之為捆綁包(bundle)。此外,還必須顯式定義導(dǎo)出哪些包,即對其他捆綁包可見的包。在應(yīng)用程序開始運行時,會檢查所有的捆綁包:每個導(dǎo)入的捆綁包都可以連接到一個導(dǎo)出的捆綁包嗎?自定義類加載器的巧妙設(shè)置可以確保在運行時,除了元數(shù)據(jù)所允許的類型以外,沒有任何其他類型加載到捆綁包中。與Maven一樣,需要在JAR中提供正確的OSGi元數(shù)據(jù)。然而,通過使用MavenCentral和POM,Maven取得了巨大的成功,但支持OSGi的JAR的出現(xiàn)卻沒有給人留下太深刻的印象。Maven和OSGi構(gòu)建在JVM和Java語言之上,這些都是它們所無法控制的。Java9解決了JVM和Java語言的核心中存在的一些相同問題。模塊系統(tǒng)并不打算完全取代這些工具,Maven和OSGi(及類似工具)仍然有自己的一席之地,只不過現(xiàn)在它們可以建立在一個完全模塊化的Java平臺之上。就目前來看,Java為創(chuàng)建大型模塊化應(yīng)用程序提供了堅實的結(jié)構(gòu)。當(dāng)然,它還存在很多需要改進的地方。1.2.1將JAR作為模塊?在Java9出現(xiàn)之前,JAR文件似乎是最接近模塊的,它們擁有名稱、對相關(guān)代碼進行了分組并且提供了定義良好的公共接口。接下來看一個運行在JVM之上的典型Java應(yīng)用程序示例,研究一下JAR作為模塊的相關(guān)概念,如圖1-1所示。在圖1-1中,有一個名為MyApplication.jar的應(yīng)用程序JAR,其中包含了自定義的應(yīng)用程序代碼。該應(yīng)用程序使用了兩個庫:GoogleGuava和HibernateValidator。此外,還有三個額外的JAR。這些都是HibernateValidator的可傳遞依賴項,可能是由諸如Maven之類的構(gòu)建工具所創(chuàng)建的。MyApplication運行在Java9之前的運行時上(該運行時通過幾個捆綁的JAR公開了Java平臺類)。雖然Java9之前的運行時可能是一個JRE(JavaRuntimeEnvironment,Java運行時環(huán)境)或JDK(JavaDevelopmentKit,Java開發(fā)工具包),但無論如何,都包含了rt.jar(運行時庫),其中包含了Java標(biāo)準(zhǔn)庫的類。圖1-1:MyApplication是一個典型的Java應(yīng)用程序,其打包成一個JAR文件,并使用了其他庫當(dāng)仔細觀察圖1-1時,會發(fā)現(xiàn)一些JAR以斜體形式列出了相關(guān)類。這些類都是庫的內(nèi)部類。例如,雖然在Guava中使用了ernal.Finalizer,但它并不是官方API的一部分,而是一個公共類,其他Guava包也可以使用Finalizer。不幸的是,這也意味著無法阻止com.myapp.Main使用諸如Finalizer之類的類。換句話說,沒有實現(xiàn)強封裝性。對于Java平臺的內(nèi)部類來說也存在同樣的情況。諸如sun.misc之類的包經(jīng)常被應(yīng)用程序代碼所訪問,雖然相關(guān)文檔嚴(yán)重警告它們是不受支持的API,不應(yīng)該使用。盡管發(fā)出了警告,但諸如sun.misc.BASE64Encoder之類的實用工具類仍然經(jīng)常在應(yīng)用程序代碼中使用。從技術(shù)上講,使用了這些類的代碼可能會破壞Java運行時的更新,因為它們都是內(nèi)部實現(xiàn)類。缺乏封裝性實際上迫使這些類被認為是“半公共”API,因為Java高度重視向后兼容性。這是由于缺乏真正的封裝所造成的結(jié)果。什么是顯式依賴呢?你可能已經(jīng)看到,從嚴(yán)格意義上講,JAR不包含任何依賴信息。按照下面的步驟運行MyApplication:java-classpathlib/guava-19.0.jar:\

lib/hibernate-validator-5.3.1.jar:\

lib/jboss-logging-3.3.0Final.jar:\

lib/classmate-1.3.1.jar:\

lib/validation-api-1.1.0.Final.jar\

-jarMyApplication.jar

正確的類路徑都是由用戶設(shè)置的。由于缺少明確的依賴關(guān)系信息,因此完成設(shè)置并非易事。1.2.2類路徑地獄Java運行時使用類路徑(classpath)來查找類。上面的示例運行了Main,所有從該類直接或間接引用的類都需要加載。可以將類路徑視為可能在運行時加載的所有類的列表。雖然在“幕后”還有更多的內(nèi)容,但是以上觀點足以說明類路徑所存在的問題。為MyApplication所生成的類路徑簡圖如下所示:java.lang.Object

java.lang.String

...

sun.misc.BASE64Encoder

sun.misc.Unsafe

...

javax.crypto.Cypher

javax.crypto.SecretKey

...

com.myapp.Main

...

mon.base.Joiner

...

ernal.Joiner

org.hibernate.validator.HibernateValidator

org.hibernate.validator.constraints.NotEmpty

...

ernal.engine.ConfigurationImpl

...

javax.validation.Configuration

javax.validation.constraints.NotNull

此時,沒有JAR或邏輯分組的概念了。所有類按照-classpath參數(shù)定義的順序排列成一個平面列表。當(dāng)JVM加載一個類時,需要按照順序讀取類路徑,從而找到所需的類。一旦找到了類,搜索就會結(jié)束并加載類。如果在類路徑中沒有找到所需的類又會發(fā)生什么情況呢?此時會得到一個運行時異常。由于類會延遲加載,因此一些不幸的用戶在首次運行應(yīng)用程序并點擊一個按鈕時會出現(xiàn)找不到類的情況。JVM無法在應(yīng)用程序啟動時有效地驗證類路徑的完整性,即無法預(yù)先知道類路徑是否是完整的,或者是否應(yīng)該添加另一個JAR。顯然,這并不夠好。當(dāng)類路徑上有重復(fù)類時,則會出現(xiàn)更為隱蔽的問題。假設(shè)嘗試避免手動設(shè)置類路徑,而是由Maven根據(jù)POM中的顯式依賴信息構(gòu)建將放到類路徑中的JAR集合。由于Maven以傳遞的方式解決依賴關(guān)系問題,因此在該集合中出現(xiàn)相同庫的兩個版本(如Guava19和Guava18)是非常常見的,雖然這并不是你的過錯。現(xiàn)在,這兩個庫JAR以一種未定義的順序壓縮到類路徑中。庫類的任一版本都可能會被首先加載。此外,有些類還可能會使用來自(可能不兼容的)其他版本的類。此時就會導(dǎo)致運行時異常。一般來說,當(dāng)類路徑包含兩個具有相同(完全限定)名稱的類時,即使它們完全不相關(guān),也只有一個會“獲勝”?,F(xiàn)在你應(yīng)該明白為什么類路徑地獄(classpathhell,也被稱為JAR地獄)在Java世界如此臭名昭著了。一些人通過不斷地摸索,逐步完善了調(diào)整類路徑的方法——但該過程是相當(dāng)艱苦的。脆弱的類路徑仍然是導(dǎo)致問題和失敗的主要原因。如果能夠在運行時提供更多關(guān)于JAR之間關(guān)系的信息,那就最好了,就好像是一個隱藏在類路徑中并等待被發(fā)現(xiàn)和利用的依賴關(guān)系圖。接下來學(xué)習(xí)Java9模塊!1.3Java9模塊到目前為止,我們已經(jīng)全面了解了當(dāng)前Java在模塊化方面的優(yōu)勢和局限。隨著Java9的出現(xiàn),可以使用一個新的工具——Java模塊系統(tǒng)來開發(fā)結(jié)構(gòu)良好的應(yīng)用程序。在設(shè)計Java平臺模塊系統(tǒng)以克服當(dāng)前所存在的局限時,主要設(shè)定了兩個目標(biāo):·對JDK本身進行模塊化。·提供一個應(yīng)用程序可以使用的模塊系統(tǒng)。這兩個目標(biāo)是緊密相關(guān)的。JDK的模塊化可通過使用與應(yīng)用程序開發(fā)人員在Java9中使用的相同的模塊系統(tǒng)來實現(xiàn)。模塊系統(tǒng)將模塊的本質(zhì)概念引入Java語言和運行時。模塊既可以導(dǎo)出包,也可以強封裝包。此外,它們顯式地表達了與其他模塊的依賴關(guān)系??梢钥吹?,Java模塊系統(tǒng)遵循了模塊的三個核心原則。接下來回到前面的MyApplication示例,此時示例建立在Java9模塊系統(tǒng)之上,如圖1-2所示。圖1-2:建立在模塊化Java9之上的模塊化應(yīng)用程序MyApplication每個JAR都變成了一個模塊,并包含了對其他模塊的顯式引用。hibernate-validator的模塊描述符表明了該模塊使用了jboss-logging、classmate和validation-api。一個模塊擁有一個可公開訪問的部分(位于頂部)以及一個封裝部分(位于底部,并以一個掛鎖表示)。這也就是為什么MyApplication不能再使用Guava的Finalizer類的原因。通過圖1-2,會發(fā)現(xiàn)MyApplication也使用了validation-api來注釋它的類。此外,MyApplication還顯式依賴JDK中的一個名為java.sql的模塊。相比于圖1-1所示的類路徑圖,圖1-2告訴了我們關(guān)于應(yīng)用程序的更多信息??梢赃@么說,就像所有Java應(yīng)用程序一樣,MyApplication使用了來自rt.jar的類,并且運行了(可能不正確的)該類路徑上的一堆JAR。這只是在應(yīng)用層。模塊的概念一直向下延伸,在JDK層也使用了模塊(圖1-2顯示了一個小的子集)。與應(yīng)用層中的模塊一樣,JDK層中的模塊也具有顯式依賴,并在隱藏了一些包的同時公開了另一些包。在模塊化JDK中,最基本的平臺模塊是java.base。它公開了諸如java.lang和java.util之類的包,如果沒有這些包,其他模塊什么也做不了。由于無法避免使用這些包中的類型,因此每個模塊毫無疑問都需要java.base。如果應(yīng)用程序模塊需要任何來自java.base之外的平臺模塊的功能,那么這些依賴關(guān)系也必須是顯式的,就像MyApplication對java.sql的依賴一樣。最后,使用一種方法可以在Java語言的更高級別的粒度上表示代碼不同部分之間的依賴關(guān)系?,F(xiàn)在想象一下在編譯時和運行時獲得所有這些信息所帶來的優(yōu)勢。這可以防止對來自其他非引用模塊的代碼的意外依賴。通過檢查(傳遞)依賴關(guān)系,工具鏈可以知道運行模塊需要哪些附加模塊并進行優(yōu)化?,F(xiàn)在,強封裝性、定義良好的接口以及顯式依賴已經(jīng)成為Java平臺的一部分。總之,Java平臺模塊系統(tǒng)帶來了如下最重要的好處:1.可靠的配置在編譯或運行代碼之前,模塊系統(tǒng)會檢查給定的模塊組合是否滿足所有依賴關(guān)系,從而導(dǎo)致更少的運行時錯誤。2.強封裝型模塊顯式地選擇了向其他模塊公開的內(nèi)容,從而防止對內(nèi)部實現(xiàn)細節(jié)的意外依賴。3.可擴展開發(fā)顯式邊界能夠讓開發(fā)團隊并行工作,同時可創(chuàng)建可維護的代碼庫。只有顯式導(dǎo)出的公共類型是共享的,這創(chuàng)建了由模塊系統(tǒng)自動執(zhí)行的邊界。4.安全性在JVM的最深層次上執(zhí)行強封裝,從而減少Java運行時的攻擊面,同時無法獲得對敏感內(nèi)部類的反射訪問。5.優(yōu)化由于模塊系統(tǒng)知道哪些模塊是在一起的,包括平臺模塊,因此在JVM啟動期間不需要考慮其他代碼。同時,其也為創(chuàng)建模塊分發(fā)的最小配置提供了可能性。此外,還可以在一組模塊上應(yīng)用整個程序的優(yōu)化。在模塊出現(xiàn)之前,這樣做是非常困難的,因為沒有可用的顯式依賴信息,一個類可以引用類路徑中任何其他類。在下一章,將通過查看JDK中的模塊,學(xué)習(xí)如何定義模塊以及使用哪些概念管理模塊之間的交互。JDK包含了如圖1-2所示更多的平臺模塊。在第2章研究模塊化JDK是了解模塊系統(tǒng)概念的一個非常好的方法,同時還可以熟悉JDK中的模塊。畢竟我們將首先使用這些模塊創(chuàng)建自己的模塊化Java9應(yīng)用程序。隨后,在第3章我們將準(zhǔn)備開始編寫自己的模塊。第2章模塊和模塊化JDKJava有超過20年的發(fā)展歷史。作為一種語言它仍然很受歡迎,這表明Java一直保持很好的狀態(tài)。只要查看一下標(biāo)準(zhǔn)庫,就會很明顯地看到該平臺長期的演變過程。在Java模塊系統(tǒng)之前,JDK的運行時庫由一個龐大的rt.jar所組成(如前一章的圖1-1所示),其大小超過60MB,包含了Java大部分運行時類:即Java平臺的最終載體。為了獲得一個靈活且符合未來發(fā)展方向的平臺,JDK團隊著手對JDK進行模塊化——考慮到JDK的規(guī)模和結(jié)構(gòu),不得不說這是一個雄心勃勃的目標(biāo)。在過去20年里,增加了許多API,但幾乎沒有刪除任何API。以CORBA為例,它曾經(jīng)被認為是企業(yè)計算的未來,而現(xiàn)在是一種被遺忘的技術(shù)(對于那些仍然在使用CORBA的人,我們深表同情)。如今,JDK的rt.jar中仍然存在支持CORBA的類。無論運行什么應(yīng)用程序,Java的每次發(fā)布都會包含這些CORBA類。不管是否使用CORBA,這些類都在那里。在JDK中包含這些遺留類會浪費不必要的磁盤空間、內(nèi)存和CPU時間。在使用資源受限設(shè)備或者為云創(chuàng)建小容器時,這些資源都是供不應(yīng)求的。更不用說開發(fā)過程中在IDE自動完成和文檔中出現(xiàn)這些過時類所造成的認知超載(cognitiveoverhead)。但是,從JDK中刪除這些技術(shù)并不是一個可行的辦法。向后兼容性是Java最重要的指導(dǎo)原則之一,而移除API會破壞長久以來形成的向后兼容性。雖然這樣做只會影響一小部分用戶,但仍然有許多人在使用CORBA之類的技術(shù)。而在模塊化JDK中,不使用CORBA的人可以選擇忽略包含CORBA的模塊。另外,主動地棄用那些真正過時的技術(shù)也是可行的。只不過在JDK刪除這些技術(shù)之前,可能還會再發(fā)布多個主要版本。此外,決定什么技術(shù)是真正過時的取決于JDK團隊,這是一個非常難做的決定。具體到CORBA,該模塊被標(biāo)記為已棄用,這意味著它將有可能在隨后主要的Java版本中被刪除。但是,分解整體JDK的愿望并不僅僅是刪除過時的技術(shù)。很多技術(shù)對于某些類型的應(yīng)用程序來說是有用的,而對于其他應(yīng)用程序來說是無用的。JavaFX是繼AWT和Swing之后Java中最新的用戶界面技術(shù)。這些技術(shù)當(dāng)然是不能刪除的,但顯然它們不是每個應(yīng)用程序都需要的。例如,Web應(yīng)用程序不使用Java中任何GUI工具包。然而,如果沒有這些GUI工具包,就無法完成部署和運行。除了便利與避免浪費之外,還要從安全角度進行考慮。Java在過去經(jīng)歷過相當(dāng)多的安全漏洞。這些漏洞都有一個共同的特點:不知何故,攻擊者可以繞過JVM的安全沙盒并訪問JDK中的敏感類。從安全的角度來看,在JDK中對危險的內(nèi)部類進行強封裝是一個很大的改進。同時,減少運行時中可用類的數(shù)量會降低攻擊面。在應(yīng)用程序運行時中保留大量暫時不使用的類是一種不恰當(dāng)?shù)淖龇?。而通過使用模塊化JDK,可以確定應(yīng)用程序所需的模塊。目前可以清楚地看到:極需要一種對JDK本身進行模塊化的方法。2.1模塊化JDK邁向模塊化JDK的第一步是在Java8中采用了緊湊型配置文件(compactprofile)。配置文件定義了標(biāo)準(zhǔn)庫中可用于針對該配置文件的應(yīng)用程序的一個包子集。假設(shè)定義了三個配置文件,分別為compact1、compact2和compact3。每個配置文件都是前一個配置文件的超集,添加了更多可用的包。使用這些預(yù)定義配置文件更新Java編譯器和運行時。JavaSEEmbedded8(僅針對Linux)提供了與緊湊型配置文件相匹配的占用資源少的運行時。如果你的應(yīng)用程序符合表2-1中所描述的其中一個配置文件,那么就可以在一個較小的運行時上運行。但是,如果需要使用預(yù)定義配置文件之外的類,那么就比較麻煩了。從這個意義上講,緊湊型配置文件的靈活性非常差,并且也無法解決強封裝問題。作為一種中間解決方案,緊湊型配置文件實現(xiàn)了它的目的,但最終需要一種更靈活的辦法。表2-1:為Java8定義的配置文件從圖1-2中可以看到JDK9是如何被拆分為模塊的。目前,JDK由大約90個平臺模塊組成,而不是一個整體庫。與可由自己創(chuàng)建的應(yīng)用程序模塊不同的是,平臺模塊是JDK的一部分。從技術(shù)上講,平臺模塊和應(yīng)用模塊之間沒有任何技術(shù)區(qū)別。每個平臺模塊都構(gòu)成了JDK的一個定義良好的功能塊,從日志記錄到XML支持。所有模塊都顯式地定義了與其他模塊的依賴關(guān)系。圖2-1顯示了這些平臺模塊的子集及其依賴關(guān)系。每條邊表示模塊之間的單向依賴關(guān)系(稍后介紹實線和虛線之間的區(qū)別)。例如,java.xml依賴于java.base。如1.3節(jié)所述,每個模塊都隱式依賴于java.base。在圖2-1中,只有當(dāng)java.base是給定模塊的唯一依賴項時,才會顯示這個隱式依賴關(guān)系,比如java.xml。盡管依賴關(guān)系圖看起來有點讓人無所適從,但是可以從中提取很多信息。只需觀察一下該圖,就可以大概了解Java標(biāo)準(zhǔn)庫所提供的功能以及各項功能是如何關(guān)聯(lián)的。例如,java.logging有許多傳入依賴項(incomingdependencies),這意味著許多其他平臺模塊使用了該模塊。對于諸如日志之類的中心功能來說,這么做是很有意義的。模塊java.xml.bind(包含用于XML綁定的JAXBAPI)有許多傳出依賴項(outgoingdependencies),包括java.desktop(這是一個意料之外的依賴項)。事實上,我們通過查看生成的依賴關(guān)系圖并進行討論而發(fā)現(xiàn)這個奇異之處,這本身就是一個巨大的進步。由于JDK的模塊化,形成了清晰的模塊邊界以及顯式的依賴關(guān)系。根據(jù)顯式模塊信息了解JDK之類的大型代碼塊是非常有價值的。另一個需要注意的是,依賴關(guān)系圖中的所有箭頭都是向下的,圖中沒有循環(huán)。這是必然的:Java模塊系統(tǒng)不允許模塊之間存在編譯時循環(huán)依賴。循環(huán)依賴通常是一種非常不好的設(shè)計。在5.5.2節(jié)中,將討論如何識別和解決代碼庫中的循環(huán)依賴。除了jdk.httpserver和jdk.unsupported之外,圖2-1中的所有模塊都是JavaSE規(guī)范的一部分。它們的模塊名都共享了前綴java.*。每個認證的Java實現(xiàn)都必須包含這些模塊。諸如jdk.httpserver之類的模塊包含了工具和API的實現(xiàn),雖然這些實現(xiàn)不受JavaSE規(guī)范的約束,但是這些模塊對于一個功能完備的Java平臺來說是至關(guān)重要的。JDK中還有很多的模塊,其中大部分在jdk.*命名空間。通過運行java--list-modules,可以獲取平臺模塊的完整列表。圖2-1:JDK平臺模塊的子集在圖2-1的頂部可以找到兩個重要的模塊:java.se和java.se.ee。它們就是所謂的聚合器模塊(aggregatormodule),主要用于對其他模塊進行邏輯分組。本章稍后將介紹聚合器模塊的工作原理。將JDK分解成模塊需要完成大量的工作。將一個錯綜復(fù)雜、有機發(fā)展且包含數(shù)以萬計類的代碼庫分解成邊界清晰且保持向后兼容性的定義良好的模塊需要花費大量的時間,這也就是為什么花了如此長的時間才將模塊系統(tǒng)植入到Java中的原因。經(jīng)過20多年的傳統(tǒng)積累,許多存在疑問的依賴關(guān)系已經(jīng)解開。展望未來,這一努力將在JDK的快速發(fā)展以及更大靈活性方面得到豐厚的回報。孵化器模塊模塊所提供的另一個改進示例是JEP11(\h/jeps/11JEP表示JavaEnhancementProposal)中所描述的孵化器模塊(incubatormodule)概念。孵化器模塊是一種使用JDK提供實驗API的手段。例如,在使用Java9時,jdk.incubator.httpclient模塊中提供了一個新的HttpClientAPI(所有孵化器模塊都具有jdk.incubator前綴)。如果愿意,可以使用這樣的孵化器模塊,并且明確地知道模塊中API仍然可以更改。這樣一來,就可以讓這些API在真實環(huán)境中不斷變得成熟和穩(wěn)定,以便在日后的JDK版本中可以作為一個完全支持模塊來使用或者刪除(如果API在實踐中不成功)。2.2模塊描述符到目前為止,我們已經(jīng)大概了解了JDK模塊結(jié)構(gòu),接下來探討一下模塊的工作原理。什么是模塊,它是如何定義的?模塊擁有一個名稱,并對相關(guān)的代碼以及可能的其他資源進行分組,同時使用一個模塊描述符進行描述。模塊描述符保存在一個名為module-info.java的文件中。示例2-1顯示了java.prefs平臺模塊的模塊描述符。示例2-1:module-info.java①關(guān)鍵字requires表示一個依賴關(guān)系,此時表示對java.xml的依賴。②來自java.prefs模塊的單個包被導(dǎo)出到其他模塊。模塊都位于一個全局命名空間中,因此,模塊名稱必須是唯一的。與包名稱一樣,可以使用反向DNS符號(例如ject.somemodule)等約定來確保模塊的唯一性。模塊描述符始終以關(guān)鍵字module開頭,后跟模塊名稱。而module-info.java的主體描述了模塊的其他特征(如果有的話)。接下來看一下java.prefs模塊描述符的主體。java.prefs使用了java.xml中的代碼從,以XML文件中加載首選項。這種依賴關(guān)系必須在模塊描述符中表示。如果沒有這個依賴關(guān)系聲明,模塊系統(tǒng)就無法編譯(或運行)java.prefs模塊。聲明依賴關(guān)系首先使用關(guān)鍵字requires,然后緊跟模塊名稱(此時為java.xml)??梢詫ava.base的隱式依賴添加到模塊描述符中。但這樣做沒有任何價值,就好比是將“importjava.lang.String”添加到使用字符串的類中(通常并不需要這么做)。模塊描述符還可以包含exports語句。強封裝性是模塊的默認特性。只有當(dāng)顯式地導(dǎo)出一個包時(比如示例中的java.util.prefs),才可以從其他模塊中訪問該包。默認情況下,一個模塊中若沒有導(dǎo)出的包則無法被其他模塊所訪問。其他模塊不能引用封裝包中的類型,即使它們與該模塊存在依賴關(guān)系。從圖2-1中可以看到,java.desktop依賴java.prefs,這意味著java.desktop只能訪問java.prefs模塊的java.util.prefs包中的類型。2.3可讀性在推理模塊之間的依賴關(guān)系時,需要注意的一個重要的新概念是可讀性(readability),讀取其他模塊意味著可以訪問其導(dǎo)出包中的類型??梢栽谀K描述符中使用requires子句設(shè)置模塊之間的可讀性關(guān)系。根據(jù)定義,每個模塊都可以讀取自己。而一個模塊之所以讀取另一個模塊是因為需要該模塊。接下來,再次查看java.prefs模塊,了解一下可讀性的影響。在示例2-2的JDK模塊中,XmlSupport類導(dǎo)入并使用了java.xml模塊中的類。示例2-2:類java.util.prefs.XmlSupport的節(jié)選importorg.w3c.dom.Document;

//...

classXmlSupport{

staticvoidimportPreferences(InputStreamis)

throwsIOException,InvalidPreferencesFormatException

{

try{

Documentdoc=loadPrefsDoc(is);

//...

}

}

//...

}

此時,導(dǎo)入了org.w3c.dom.Document(以及其他類),該類來自java.xml模塊。如示例2-1所示,由于java.prefs模塊描述符包含了requiresjava.xml,因此代碼可以順利完成編譯。如果java.prefs模塊的作者省略了require子句,那么Java編譯器將報告錯誤。在模塊java.prefs中使用來自java.xml的代碼是一個經(jīng)過深思熟慮并顯式記錄的選擇。2.4可訪問性可讀性關(guān)系表明了哪些模塊可以讀取其他模塊,但讀取一個模塊并不意味著可以訪問其導(dǎo)出包中的所有內(nèi)容。在建立了可讀性之后,正常的Java可訪問性規(guī)則仍然會發(fā)揮作用。從一開始,Java就將可訪問性規(guī)則內(nèi)置到語言中。表2-2顯示當(dāng)前的訪問修飾符及其影響。表2-2:訪問修飾符及其影響范圍可訪問性在編譯時和運行時被強制執(zhí)行??稍L問性和可讀性的結(jié)合可以確保在模塊系統(tǒng)中實現(xiàn)強封裝性。是否可以在模塊M1中訪問模塊M2中的類型已經(jīng)成為一個雙重問題:1)M1是否可以讀取M2?2)如果可以,M2導(dǎo)出包中的類型是否可以訪問?在其他模塊中,只能訪問導(dǎo)出包中的公共類型。如果導(dǎo)出包中的一個類型不是公共的,那么傳統(tǒng)的可訪問性規(guī)則將不允許使用該類型。如果類型是公共的,但沒有導(dǎo)出,那么模塊系統(tǒng)的可讀性規(guī)則將阻止使用該類型。編譯時的違規(guī)會導(dǎo)致編譯器錯誤,而運行時的違規(guī)會導(dǎo)致IllegalAccessError。public仍然表示公開的嗎?其他模塊無法使用未導(dǎo)出包中的任何類型——即使包中的類型是公共的。這是對Java語言可訪問性規(guī)則的根本變化。在Java9出現(xiàn)之前,事情非常簡單明了。如果有一個公共類或接口,那么其他類就可以對它進行訪問。但自從Java9出現(xiàn)之后,public意味著僅對模塊中的其他包公開。只有當(dāng)導(dǎo)出包包含了公開類型時,其他模塊才可以使用這些類型。這就是強封裝的意義所在。它迫使開發(fā)人員仔細設(shè)計包結(jié)構(gòu),將需要外部使用的類型與內(nèi)部實現(xiàn)過程分離開來。在模塊出現(xiàn)之前,強封裝實現(xiàn)類的唯一方法是將這些類放置到單個包中,并標(biāo)記為私有。這種做法使得包變得非常笨重,實際上,將類公開只是為了實現(xiàn)不同包之間的訪問。通過使用模塊,可以以任何方式構(gòu)建包,并僅導(dǎo)出模塊使用者真正必須訪問的包。如果愿意的話,還可以將導(dǎo)出的包構(gòu)成模塊的API。關(guān)于可訪問性規(guī)則的另一個方面是反射(reflection)。在模塊系統(tǒng)出現(xiàn)之前,所有反射對象都有一個有趣而又危險的方法setAccessible。通過調(diào)用setAccessible(true),任何元素(不管元素是公共或是私有)都會變?yōu)榭稍L問。雖然該方法目前仍然可以使用,但必須遵守前面所討論的規(guī)則。想要在另一個模塊導(dǎo)出的任意元素上調(diào)用setAccessible并期望像以前一樣工作是不可能的了(即使反射不會破壞強封裝性)??梢允褂枚喾N方法實現(xiàn)模塊系統(tǒng)中新的可訪問性規(guī)則,其中大多數(shù)解決方法應(yīng)該被視為遷移的輔助工具,詳細內(nèi)容參見第二部分。2.5隱式可讀性默認情況下,可讀性是不可傳遞的??梢酝ㄟ^查看java.prefs的傳入和傳出“讀取邊”來說明這一點,如圖2-2所示。圖2-2:可讀性是不可傳遞的:java.desktop不能通過java.prefs讀取java.xml此時,java.desktop可以讀取java.prefs(為了簡單起見,排除了其他模塊)。這意味著java.desktop可以訪問java.util.prefs包中的公共類型。然而,java.desktop不能通過與java.prefs的依賴關(guān)系來訪問java.xml中的類型,但此時java.desktop確實使用了java.xml中的類型,這是因為java.desktop的模塊描述符中使用了requiresjava.xml子句。從圖2-1中也可以看到這個依賴關(guān)系。有時確實希望讀取關(guān)系是可傳遞的,例如,當(dāng)模塊M1導(dǎo)出包中的一個類型需要引用模塊M2中的一個類型時。此時,如果不能讀取M2,那么需要M1進而需要引用M2中類型的模塊就無法使用了。這聽起來非常抽象,所以下面依次提供示例加以說明??梢栽贘DK的java.sql模塊中找到該現(xiàn)象的一個好示例。該模塊包含兩個接口(如示例2-3所示的Driver以及如示例2-4所示的SQLXML),定義了結(jié)果類型來自其他模塊的方法簽名。示例2-3:Driver接口(部分顯示),允許檢索java.logging模塊中的Loggerpackagejava.sql;

importjava.util.logging.Logger;

publicinterfaceDriver{

publicLoggergetParentLogger();

//..

}

示例2-4SQLXML接口(部分顯示),來自模塊java.xml的源代碼,表示從數(shù)據(jù)庫返回的XMLpackagejava.sql;

importjavax.xml.transform.Source;

publicinterfaceSQLXML{

<TextendsSource>TgetSource(Class<T>sourceClass);

//..

}

如果在模塊描述符中添加對java.sql的依賴,那么就可以對這些接口進行編程,因為它們都位于導(dǎo)出包中。但每當(dāng)調(diào)用getParentLogger或getSource時,所獲取的并不是由java.sql導(dǎo)出的類型的值。在第一種情況下,獲取的是來自java.logging的java.util.logging.Logger;而在第二種情況下,獲取的是來自java.xml的javax.xml.transform.Source。為了使用這些返回值完成一些有用的事情(比如分配給一個局部變量、調(diào)用其方法),還需要讀取其他模塊。當(dāng)然,可以手動地在模塊描述符中分別添加對java.logging或java.xml的依賴。但這樣做是沒有必要的,因為java.sql的作者已經(jīng)意識到在其他模塊上沒有可讀性的接口是不可用的。隱式可讀性允許模塊的作者在模塊描述符中表達這種可傳遞的可讀性關(guān)系。對于java.sql,可完成如下修改:modulejava.sql{

requirestransitivejava.logging;

requirestransitivejava.xml;

exportsjava.sql;

exportsjavax.sql;

exportsjavax.transaction.xa;

}

現(xiàn)在,關(guān)鍵字requires后面緊跟著修飾符transitive,從而略微改變了一下語義。常見的requires只允許一個模塊訪問所需模塊導(dǎo)出包中的類型,而requirestransitive的語義更豐富。現(xiàn)在,任何需要java.sql的模塊都將自動需要java.logging和java.xml,這意味著可以通過隱式可讀性關(guān)系訪問這些模塊的導(dǎo)出包。通過使用requirestransitive,模塊作者可以為模塊用戶設(shè)置額外的可讀性關(guān)系。從使用者的角度來看,上述做法可以更容易地使用java.sql。當(dāng)你需要java.sql時,不僅可以訪問導(dǎo)出包java.sql、javax.sql和javax.transaction.xa(這些都直接由java.sql導(dǎo)出),還可以訪問由模塊java.logging和java.xml導(dǎo)出的所有包。這就好像這些包是由java.sql重新導(dǎo)出的一樣,這一切都是使用requirestransitive所設(shè)置的隱式可讀性關(guān)系實現(xiàn)的。很明顯,這并沒有真的從其他模塊重新導(dǎo)出包,但是這樣考慮有助于理解隱式可讀性的效果。對于使用了java.sql模塊的應(yīng)用程序來說,其模塊定義如下所示:moduleapp{

requiresjava.sql;

}

使用上述模塊描述符生成如圖2-3所示的隱式可讀性邊。圖2-3:隱式可讀性(requirestransitive)的效果,用粗體邊顯示java.xml和java.logging上的隱式可讀性被授予給app,因為java.sql針對這些模塊使用了requirestransitive(如圖2-3所示的實邊)。因為app沒有導(dǎo)出任何內(nèi)容,同時僅使用java.sql以完成封裝的實現(xiàn),所以使用常見的requires子句就足夠了(如圖2-3所示的虛線)。當(dāng)需要其他模塊供內(nèi)部使用時,使用requires就足夠了。但另一方面,如果需要在導(dǎo)出類型中使用另一個模塊中的類型,那么就要使用requirestransitive。在5.3節(jié)中,將會更加詳細地討論隱式可讀性的重要性及其何時對模塊重要。現(xiàn)在,可以再看看圖2-1。圖中所有的實線邊都是requirestransitive依賴關(guān)系。而虛線邊只是常見的requires依賴關(guān)系。非傳遞依賴(nontransitivedependency)意味著依賴關(guān)系是支持模塊內(nèi)部實現(xiàn)所必需的。可傳遞依賴(transitivedependency)則意味著依賴關(guān)系是支持模塊API所必需的。后一種依賴關(guān)系更為重要,因此它們在本書的圖形中用實線表示。使用以上新的觀點來觀察圖2-1,可以看到隱式可讀性的另一個用例:可用來將多個模塊聚合到一個新模塊中。以java.se為例,該模塊不包含任何代碼,只由一個模塊描述符構(gòu)成。在該模塊描述符中,針對每個模塊(JavaSE規(guī)則的一部分)都列出了一個requirestransitive子句。當(dāng)需要在一個模塊中使用java.se時,憑借隱式可讀性,可以訪問java.se所聚合的每個模塊導(dǎo)出的所有API:modulejava.se{

requirestransitivejava.desktop;

requirestransitivejava.sql;

requirestransitivejava.xml;

requirestransitivejava.prefs;

//省略

}

隱式可讀性自身也是可傳遞的。以平臺中另一個聚合器模塊java.se.ee為例。從圖2-1可以看到,相比于java.se,java.se.ee聚合了更多的模塊。它使用了requirestransitivejava.se并添加了多個模塊(包含了JavaEE規(guī)范的部分內(nèi)容)。Java.se.ee聚合器模塊描述符的內(nèi)容如下所示:modulejava.se.ee{

requirestransitivejava.se;

requirestransitivejava.xml.ws;

requirestransitivejava.xml.bind;

//省略

}

java.se上的requirestransitive確保一旦需要java.se.ee,就會為由java.se聚合的所有模塊建立隱式可讀性。此外,java.se.ee還會對多個EE模塊設(shè)置了隱式可讀性。最終,java.se和java.se.ee為大量的模塊提供了隱式可讀性,只需通過這些可傳遞依賴關(guān)系就可以訪問這些模塊。在應(yīng)用程序模塊中使用java.se.ee或java.se是不明智的,這意味著在模塊中復(fù)制了Java9之前可以訪問的rt.jar的所有行為。依賴關(guān)系應(yīng)盡可能細化。在模塊描述符中要盡可能精確同時只添加實際使用的模塊,這是非常重要的。在5.4節(jié)將探討聚合器模塊模式如何幫助模塊化庫設(shè)計。2.6限制導(dǎo)出在某些情況下,可能只需要將包暴露給特定的某些模塊。此時,可以在模塊描述符中使用限制導(dǎo)出。可以在java.xml模塊中找到限制導(dǎo)出的示例:modulejava.xml{

...

exportsernal.stream.writerstojava.xml.ws

...

}

此時可以看到一個與另一個平臺模塊共享有用的內(nèi)部代碼的平臺模塊。導(dǎo)出的包只能由to之后指定的模塊訪問。可以用由逗號分隔的多個模塊名稱作為限制導(dǎo)出的目標(biāo)。to子句中沒有提到的任何模塊都不能訪問包中的類型,即使在讀取模塊時也是如此。限制導(dǎo)出的存在并不意味著就一定要使用它們。一般來說,應(yīng)該避免在應(yīng)用程序的模塊之間使用限制導(dǎo)出。使用限制導(dǎo)出意味著在導(dǎo)出模塊和允許的使用者之間建立了直接的聯(lián)系。從模塊化的角度來看,這是不可取的。模塊之所以偉大,是因為它可以有效地對API的生產(chǎn)者與使用者進行解耦。而限制導(dǎo)出破壞了這個屬性,因為現(xiàn)在使用者模塊名稱成為提供者模塊描述符的一部分。然而,對于模塊化JDK來說,這只是一個小問題。如果想要使用遺留代碼對平臺進行模塊化,那么限制導(dǎo)出是不可或缺的。許多平臺模塊封裝了部分自身的代碼,并通過限制導(dǎo)出公開了一些內(nèi)部API,以選擇其他平臺模塊,同時對于應(yīng)用程序中使用的公共API則使用正常的導(dǎo)出機制。通過使用限制導(dǎo)出,平臺模塊可以在不重復(fù)代碼的情況下變得更細粒度。2.7模塊解析和模塊路徑在模塊之間建立顯式依賴關(guān)系不僅僅有助于生成漂亮的圖表。當(dāng)編譯和運行模塊時,Java編譯器和運行時使用模塊描述符來解析正確的模塊。模塊是從模塊路徑(modulepath)中解析出來的,而不是類路徑。類路徑是一個類型的平面列表(即使使用JAR文件也是如此),而模塊路徑只包含模塊。如前所述,這些模塊提供了導(dǎo)出包的顯式信息,從而能夠高效地對模塊路徑進行索引。當(dāng)從給定的包中查找類型時,Java運行時和編譯器可以準(zhǔn)確地知道從模塊路徑中解析哪個模塊。而在以前,對整個類路徑進行掃描是找到任意類型的唯一方法。當(dāng)需要運行一個打包成模塊的應(yīng)用程序時,還需要使用其所有的依賴項。模塊解析是根據(jù)給定的依賴關(guān)系圖并從該圖中選擇一個根模塊(rootmodule)來計算最低需求的模塊集的過程。從根模塊可訪問的每個模塊最終都在解析模塊集中。在數(shù)學(xué)上講,這相當(dāng)于計算依賴關(guān)系圖的傳遞閉包(transitiveclosure)。盡管聽起來很嚇人,但這個過程還是非常直觀的:1)首先從一個根模塊開始,并將其添加到解析集中。2)向解析集添加所需的模塊(module-info.java中的requires或requirestransitive)。3)重復(fù)第2步,將新的模塊添加到解析集中。上述過程不會無限制地進行下去,因為只有發(fā)現(xiàn)了新的模塊才會重復(fù)該過程。此外,依賴關(guān)系圖必須是非循環(huán)的。如果想要為多個根模塊解析模塊,則需要首先將該算法應(yīng)用于每個根模塊,然后再合并結(jié)果集。接下來嘗試一個示例。此時有一個應(yīng)用程序模塊app,它是解析過程中的根模塊,并且只使用了來自模塊化JDK的java.sql:moduleapp{

requiresjava.sql;

}

現(xiàn)在,完成模塊解析過程。當(dāng)考慮到模塊的依賴關(guān)系時,忽略java.base,并假定它始終是解析模塊的一部分??梢愿鶕?jù)圖2-1所示的邊來完成下面的步驟:1)向解析集添加app;并觀察到它需要使用java.sql。2)向解析集添加java.sql;并觀察到它需要java.xml和java.logging。3)向解析集添加java.xml;并觀察到它不再需要其他模塊。4)向解析集添加java.logging,并觀察到它不再需要其他模塊。5)不再需要添加任何新模塊;解析過程結(jié)束。該解析過程的結(jié)果是生成了一個包含app、java.sql、java.xml、java.logging和java.base的集合。當(dāng)運行app時,會按照上述過程解析模塊,模塊系統(tǒng)從模塊路徑中獲取模塊。在解析過程中還會完成一些額外的檢查。例如,具有相同名稱的兩個模塊在啟動時(而不是在運行過程出現(xiàn)類加載失敗時)會產(chǎn)生錯誤。此外,還會檢查導(dǎo)出包的唯一性。模塊路徑上只有一個模塊可以公開一個給定的包。5.5.1節(jié)將討論多個模塊導(dǎo)出相同包的問題。版本前面已經(jīng)討論了模塊的解析過程,卻沒有提及版本問題。這看上去可能很奇怪,因為我們習(xí)慣于指定具有依賴關(guān)系的版本,如MavenPOM。將版本選擇(versionselection)放置在Java模塊系統(tǒng)的范圍之外是一個經(jīng)過深思熟慮的設(shè)計決策。在模塊解析過程中版本不起任何作用,5.7節(jié)將會深入討論這一決策。模塊解析過程以及額外的檢查確保了應(yīng)用程序在一個可靠的環(huán)境中運行,降低了運行時失敗的可能性。在第3章,將會學(xué)習(xí)如何在編譯和運行自己的模塊時構(gòu)建一個模塊路徑。2.8在不使用模塊的情況下使用模塊化JDK到目前為止,已經(jīng)學(xué)習(xí)了模塊系統(tǒng)所引入的許多新概念。此時,你可能想知道模塊系統(tǒng)如何影響現(xiàn)有的代碼,很顯然,這些代碼并沒有實現(xiàn)模塊化。難道真的需要將代碼轉(zhuǎn)換為模塊才能開始使用Java9嗎?幸好不是這樣。Java9可以像以前的Java版本一樣使用,而不必將代碼移動到模塊中。模塊系統(tǒng)完全適用于現(xiàn)有的應(yīng)用程序代碼,類路徑仍然可以使用。但JDK本身由模塊組成。這兩個世界是如何調(diào)解的呢?接下來看一下示例2-5所示的代碼片段。示例2-5:NotInModule.javaimportjava.util.logging.Level;

importjava.util.logging.Logger;

importjava.util.logging.LogRecord;

publicclassNotInModule{

publicstaticvoidmain(String...args){

Loggerlogger=Logger.getGlobal();

LogRecordmessage=newLogRecord(Level.INFO,"Thisstillworks!");

logger.log(message);

}

}

上面所示的僅僅是一個類,而不是任何模塊。該代碼使用了來自JDK的java.logging模塊中的類型,但卻沒有任何模塊描述符來表示這種依賴關(guān)系。當(dāng)編譯沒有模塊描述符的代碼,然后將其放置到類路徑并運行時,代碼可以正常工作。怎么會這樣呢?在一個模塊之外編譯和加載的代碼最終都放在未命名模塊(unnamedmodule)中。相比之下,目前所看到的模塊都是顯式模塊,并在module-info.java中定義了它們的名稱。未命名模塊非常特殊:它可以讀取所有其他模塊,包括此例所讀取的java.logging模塊。通過使用未命名模塊,尚未模塊化的代碼可以繼續(xù)在JDK9上運行。當(dāng)將代碼放在類路徑上時,會自動使用未命名模塊。這也意味著需要構(gòu)建一個正確的類路徑??梢坏┦褂昧宋疵K,前面討論的模塊系統(tǒng)所帶來的保障和好處也就沒有了。當(dāng)在Java9中使用類路徑時,還需要注意兩件事情。首先,由于平臺是模塊化的,因此對內(nèi)部實現(xiàn)類進行了強封裝。在Java8和更早的版本中,可以使用這些不被支持的內(nèi)部API,而不會有任何不良影響。但如果使用Java9,則不能對平臺模塊中的封裝類型進行編譯。為了便于遷移,使用了內(nèi)部API在早期版本的Java上編譯的代碼現(xiàn)在可以繼續(xù)在JDK9類路徑上運行。當(dāng)在Java9類路徑上運行(而不是編譯)一個應(yīng)用程序時,使用了更為寬松的強封裝形式。在JDK8以及更早版本上可訪問的所有內(nèi)部類在JDK9運行時上都是可訪問的。但是當(dāng)通過反射訪問這些封裝類型時,會出現(xiàn)警告信息。在未命名模塊中編譯代碼時需要注意的第二件事是編譯期間java.se將作為根模塊。如示例2-5所示,可以通過java.se訪問任何可訪問模塊中的類型。這意味著java.se.ee(而不是java.se)下的模塊(比如java.corba和java.xml.ws)都無法解析,因此也就無法訪問。此策略最突出的示例之一就是JAXBAPI。以上兩種限制背后的原因以及處理方法將在第7章更詳細地討論。在本章,主要學(xué)習(xí)了如何對JDK進行模塊化。雖然模塊在JDK9中起到了核心作用,但卻是應(yīng)用程序的可選項。如前所示,雖然目前已經(jīng)采取謹慎措施,以確保在JDK9之前的類路徑上運行的應(yīng)用程序可以繼續(xù)運行,但也有一些注意事項。在下一章中,將會更加詳細地討論前面所介紹的模塊概念,并使用它們來構(gòu)建自己的模塊。第3章使用模塊在本章,將邁出使用Java9進行模塊開發(fā)的第一步,即動手編寫自己的第一個模塊,而不僅僅是查看JDK中現(xiàn)有的模塊。為了輕松地走好第一步,首先創(chuàng)建一個最簡單的模塊,可以將其稱為ModularHelloWorld。有了相關(guān)的經(jīng)驗之后,就可以開始創(chuàng)建帶有多個模塊的更復(fù)雜的示例了,此時將會介紹貫穿本書的運行示例EasyText。隨著對模塊系統(tǒng)的進一步了解,該示例的設(shè)計也將逐步完善。3.1第一個模塊在前一章,已經(jīng)給出了模塊描述符的示例。然而,一個模塊通常不只是一個描述符。因此,ModularHelloWorld超越了單個源文件的級別:需要在上下文中來研究該示例。我們將從編譯、打包和運行單個模塊開始,以了解模塊的新工具選項。3.1.1剖析模塊第一個示例的目的是將下面所示的類編譯成一個模塊并運行(見示例3-1)。首先從一個包的單一類開始,編譯成單一模塊。模塊可能僅包含包中的類型,所以需要一個包定義。示例3-1:HelloWorld.java(chapter3/helloworld)packagecom.javamodularity.helloworld;

publicclassHelloWorld{

publicstaticvoidmain(String...args){

System.out.println("HelloModularWorld!");

}

}

文件系統(tǒng)中源文件的布局如下所示:①模塊目錄。②模塊描述符。相比于Java源文件的傳統(tǒng)布局,上述布局存在兩個主要區(qū)別。首先,有一個額外的間接層:在src的下面引入了另一個目錄helloworld,該目錄以所創(chuàng)建的模塊名稱命名。其次,在模塊目錄中找到了源文件(像往常一樣嵌套在包結(jié)構(gòu)中)和一個模塊描述符。模塊描述符位于module-info.java文件中,是Java模塊的關(guān)鍵組成部分。它的存在相當(dāng)于告訴Java編譯器正在使用的是一個模塊,而不是普通的Java源代碼。如本章的后續(xù)內(nèi)容所述,與普通的Java源文件相比,當(dāng)使用模塊時,編譯器的行為是完全不同的。模塊描述符必須存在于模塊目錄的根目錄中。它與其他源文件一起編譯成一個名為module-info.class的二進制類文件。模塊描述符的內(nèi)部是什么呢?ModularHelloWorld示例非常簡單:modulehelloworld{

}

此時,使用新關(guān)鍵字module并緊跟模塊名稱聲明了一個模塊。該名稱必須與包含模塊描述符的目錄名稱相匹配。否則,編譯器將拒絕編譯并報告匹配錯誤。僅在多模塊模式下(這種情況是非常常見的)運行編譯器時,才需要滿足名稱匹配要求。對于3.1.3節(jié)中所討論的單模塊方案,目錄名稱無關(guān)緊要。但在任何情況下,使用模塊名稱作為目錄名稱不失為一個好主意。由于模塊聲明體是空的,因此不會從helloworld模塊中導(dǎo)出任何內(nèi)容到其他模塊。默認情況下,所有包都是強封裝的。即使目前在這個聲明中沒有任何依賴關(guān)系信息,但是請記住,該模塊隱式地依賴java.base平臺模塊。此時,你可能會問,向語言中添加新的關(guān)鍵字是否會破壞使用module作為標(biāo)識符的現(xiàn)有代碼。幸運的是,情況并非如此。仍然可以在其他源文件中使用名為module的標(biāo)識符,因為module關(guān)鍵字是限制關(guān)鍵字(restrictedkeyword),它僅在module-info.java中被視為關(guān)鍵字。對于目前在模塊描述符中所看到的requires關(guān)鍵字和其他新關(guān)鍵字來說,也是一樣的。名稱module-info通常,Java源文件的名稱與其所包含的(公共)類型相對應(yīng)。例如,包含HelloWorld類的文件名必須為HelloWorld.java。但名稱module-info打破了這種對應(yīng)關(guān)系。此外,module-info甚至不是一個合法的Java標(biāo)識符,因為它包含了破折號。這樣做的目的是防止非模塊感知工具盲目地將module-info.java或module-info.class作為普通的Java類加以處理。為特殊源文件保留名稱在Java語言中并不是沒有出現(xiàn)過。在module-info.java之前,就已經(jīng)有了package-info.java。該名稱雖然可能相對比較陌生,但自Java5之后它就出現(xiàn)了。在package-info.java中,可以向包聲明中添加文檔和注釋。與module-info.java一樣,它也是由Java編譯器編譯成一個類文件?,F(xiàn)在,已經(jīng)擁有了一個僅包含模塊聲明的模塊描述符以及一個源文件。接下來可以編譯第一個模塊了!3.1.2命名模塊命名事物雖然很難,但卻很重要。尤其是對于模塊而言更是如此,因為它們將傳遞應(yīng)用程序的高級結(jié)構(gòu)。模塊名稱所在的全局命名空間與Java中其他命名空間是分開的。因此,從理論上講,模塊的名稱可以與類、接口或包的名稱相同。但實際上,這樣做可能會導(dǎo)致混亂。模塊名稱必須是唯一的:應(yīng)用程序只能具有一個給定名稱的模塊。在Java中,通常使用反向DNS符號來確保包名稱全局唯一??梢詫δK應(yīng)用相同的方法。例如,可以將helloworld模塊重命名為com.javamodularity.helloworld,但這樣做會導(dǎo)致模塊名稱過長且有些笨重。應(yīng)用程序中的模塊名稱是否需要全局唯一呢?答案是肯定的,當(dāng)模塊是已發(fā)布的庫并且在許多應(yīng)用程序中使用時,選擇全局唯一的模塊名稱就顯得非常有意義了。在10.2節(jié)中,將會進一步討論這個概念。對于應(yīng)用程序模塊來說,要盡量選擇更短且更令人難忘的名稱。為了增加示例的可讀性,本書選擇使用更簡短的模塊名稱。3.1.3編譯有一個源文件格式的模塊是一回事,但要運行該模塊,首先必須進行編譯。在Java9之前,Java編譯器使用目標(biāo)目錄以及一組源文件進行編譯:javac-doutsrc/com/foo/Class1.javasrc/com/foo/Class2.java

在實踐中,通常是通過構(gòu)建工具(如Maven或Gradle)來完成的,但原理是一樣的。在目標(biāo)目錄中輸出類(此時使用了out),其中包含代表輸入(包)結(jié)構(gòu)的嵌套文件夾。按照同樣的模式,可以編譯ModularHelloWorld示例:javac-dout/helloworld\

src/helloworld/com/javamodularity/helloworld/HelloWorld.java\

src/helloworld/module-info.java

存在兩個顯著的區(qū)別:·輸出到反映了模塊名稱的helloworld目錄?!odule-info.java作為額外源文件進行編譯。在要編譯的文件集中出現(xiàn)module-info.java源文件會觸發(fā)javac的模塊感知模式。下面顯示的輸出是編譯后的結(jié)果,也被稱為分解模塊(explodedmodule)格式:out

└─helloworld

├─com

│└─javamodularity

│└─helloworld

│└─HelloWorld.class

└─module-info.class

最好在模塊之后命名包含分解模塊的目錄,但不是必需的。最終,模塊系統(tǒng)是從描述符中獲取模塊名稱,而不是從目錄名稱中。在3.1.5節(jié)中,將會創(chuàng)建并運行這個分解模塊。編譯多個模塊到目前為止所看到的都是所謂的Java編譯器單模塊模式(single-modulemode)。通常,需要編譯的項目由多個模塊組成,這些模塊還可能會相互引用。又或者項目是單個模塊,但卻使用了其他(已經(jīng)編譯)的模塊。為了處理這些情況,引入了額外的編譯器標(biāo)志:--module-source-path和--module-path。這些標(biāo)志都是-sourcepath和-classpath標(biāo)志(長期以來,這些標(biāo)志一直是javac的一部分)的模塊感應(yīng)對應(yīng)項。在學(xué)習(xí)3.2.2節(jié)中的多模塊示例時,將會解釋其語義。請記住,模塊源目錄的名稱必須與在多模塊模式下module-info.java中聲明的名稱相匹配。構(gòu)建工具直接通過命令行使用Java編譯器、操作其標(biāo)志以及手動列出所有源文件并不是常見的做法,更常見的做法是使用Maven或Gradle等構(gòu)建工具來抽取這些細節(jié)信息。因此,本書不會詳細介紹添加到Java編譯器和運行時的每個新選項,可以在官方文檔中找到相關(guān)詳細信息(http://bitly/tools-comm-ref)。當(dāng)然,構(gòu)建工具也需要適應(yīng)新的模塊化現(xiàn)實。在第11章中,將介紹一些可以與Java9一起使用的最流行的構(gòu)建工具。3.1.4打包到目前為止,已經(jīng)創(chuàng)建了單個模塊并將其編譯為分解模塊格式,在下一節(jié)將會討論如何運行分解模塊。這種格式在開發(fā)環(huán)境下是可行的,但在生產(chǎn)環(huán)境中則需要以更方便的格式分發(fā)模塊。為此,可以將模塊打包成JAR文件并使用,從而產(chǎn)生了模塊化JAR文件。模塊化JAR文件類似于普通的JAR文件,但它還包含了module-info.class。JAR工具已經(jīng)進行了更新,從而可以使用Java9中的模塊。為了打包ModularHelloWorld示例,請運行下面的命令:jar-cfemods/helloworld.jarcom.javamodularity.helloworld.HelloWorld\

-Cout/helloworld.

通過上述命令,在mods目錄(請確保該目錄存在)中創(chuàng)建了一個新存檔文件(-cf)helloworld.jar。此外,還希望這個模塊的入口點(-e)是HelloWorld類;每當(dāng)模塊啟動并且沒有指定另一個要運行的主類時,這是默認入口點。此時提供了完全限定的類名稱作為入口點的參數(shù)。最后,指示jar工具更改(-C)為out/helloworld目錄,并將此目錄中的所有已編譯文件放在JAR文件中?,F(xiàn)在,JAR的內(nèi)容類似于分解模塊,同時還額外添加了一個MANIFEST.MF文件:helloworld.jar

├──META-INF

│└──MANIFEST.MF

├──com

│└──javamodularity

│└──helloworld

│└──HelloWorld.class

└──module-info.class

與編譯期間的模塊目錄名稱不同,模塊化JAR文件的名稱并不重要??梢允褂萌我庀矚g的文件名,因為模塊由綁定的module-info.class中聲明的名稱所標(biāo)識。3.1.5運行模塊現(xiàn)在,回顧一下目前所完成的事情。首先從ModularHelloWorld示例開始,創(chuàng)建了一個帶有單個HelloWorld.java源文件和模塊描述符的helloworld模塊。然后,將模塊編譯成分解模塊格式。最后,將分解模塊打包成一個模塊化JAR文件。這個JAR文件包含了已編譯的類和模塊描述符,并且知道要執(zhí)行的主類。嘗試運行模塊,分解模塊格式和模塊化JAR文件都可以運行??梢允褂孟旅娴拿钸\行分解模塊格式:$java--module-pathout\

--modulehelloworld/com.javamodularity.helloworld.

HelloWorldHelloModularWorld!

還可以使用--module-path的縮寫格式-p。標(biāo)志--module可以縮寫為-m。除了基于類路徑的應(yīng)用程序之外,Java命令還獲得了新的標(biāo)志來處理模塊。請注意,此時將out目錄(包含分解的helloworld模塊)放在模塊路徑上。模塊路徑是原始類路徑的模塊感知對應(yīng)項。接下來使用--module標(biāo)志提供所運行的模塊。此時,模塊名稱后跟斜杠,然后是要運行的類。另一方面,如果運行模塊化JAR,則只需提供模塊名稱即可:$java--module-pathmods--modulehelloworld

HelloModularWorld!

這么做是有道理的,因為模塊化JAR知道要從其元數(shù)據(jù)執(zhí)行的類。在構(gòu)建模塊化JAR時已經(jīng)顯式地將入口點設(shè)置為com.javamodularity.helloworld.HelloWorld。帶有相應(yīng)模塊名稱(和可選主類)的--module或-m標(biāo)志必須始終在最后。任何后續(xù)參數(shù)都將傳遞至從給定模塊啟動的主類。以這兩種方式中的任何一種啟動都會使helloworld成為執(zhí)行的根模塊。JVM從這個根模塊開始,解析從模塊路徑運行根模塊所需的任何其他模塊。如前面的2.7節(jié)所述,解析模塊是一個遞歸過程:如果新解析的模塊需要其他模塊,那么模塊系統(tǒng)會自動考慮到這一點。在簡單的HelloWorld例子中,沒有執(zhí)行太多的解析??梢韵?/p>

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論