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

下載本文檔

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

文檔簡介

Java9模塊化開發(fā)核心原則與實踐(第二部分)目錄\h第二部分遷移\h第7章沒有模塊的遷移\h7.1類路徑已經(jīng)“死”了?\h7.2庫、強封裝和JDK9類路徑\h7.3編譯和封裝的API\h7.4刪除的類型\h7.5使用JAXB和其他JavaEEAPI\h7.6jdk.unsupported模塊\h7.7其他更改\h第8章遷移到模塊\h8.1遷移策略\h8.2一個簡單示例\h8.3混合類路徑和模塊路徑\h8.4自動模塊\h8.5開放式包\h8.6開放式模塊\h8.7破壞封裝的VM參數(shù)\h8.8自動模塊和類路徑\h8.9使用jdeps\h8.10動態(tài)加載代碼\h8.11拆分包\h第9章遷移案例研究:Spring和Hibernate\h9.1熟悉應用程序\h9.2使用Java9在類路徑上運行\(zhòng)h9.3設置模塊\h9.4使用自動模塊\h9.5Java平臺依賴項和自動模塊\h9.6開放用于反射的包\h9.7解決非法訪問問題\h9.8重構(gòu)到多個模塊\h第10章庫遷移\h10.1模塊化之前\h10.2選擇庫模塊名稱\h10.3創(chuàng)建模塊描述符\h10.4使用模塊描述符更新庫\h10.5針對較舊的Java版本\h10.6庫模塊依賴關系\h10.7針對多個Java版本\h第三部分模塊化開發(fā)工具\h第11章構(gòu)建工具和IDE\h11.1ApacheMaven\h11.2Gradle\h11.3IDE\h第12章測試模塊\h12.1黑盒測試\h12.2使用JUnit進行黑盒測試\h12.3白盒測試\h12.4測試工具\h第13章使用自定義運行時映像進行縮減\h13.1靜態(tài)鏈接和動態(tài)鏈接\h13.2使用jlink\h13.3查找正確的服務提供者模塊\h13.4鏈接期間的模塊解析\h13.5基于類路徑應用程序的jlink\h13.6壓縮大小\h13.7提升性能\h13.8跨目標運行時映像\h第14章模塊化的未來\h14.1OSGi\h14.2JavaEE\h14.3微服務\h14.4下一步第二部分遷移第7章沒有模塊的遷移

向后兼容性一直是Java的主要目標。通常,從開發(fā)人員的角度來看遷移到新的Java版本通常是微不足道的。模塊化系統(tǒng)和模塊化JDK可以說是自Java平臺出現(xiàn)以來對整個平臺最大的改變。但即便如此,向后兼容性也是重中之重。將現(xiàn)有的應用程序遷移到Java9最好分兩步進行。本章重點介紹如何遷移現(xiàn)有代碼以便在Java9上構(gòu)建和運行,而無須將代碼遷移到模塊。下一章將深入討論如何將代碼遷移到模塊,并提供了實現(xiàn)這一目標的策略。當不需要使用Java9的旗艦功能——模塊系統(tǒng)時,為什么還要遷移到Java9呢?升級到Java9后就可以訪問屬于Java9的其他功能了,想一想新的API、工具和性能改進。究竟是一直使用模塊還是從一開始就不使用模塊要視情況而定。應用程序是否可以得到更多擴展和新功能?如果可以,那么模塊化所帶來的好處可以證明采取第二步所付出的代價是值得的。正如本章所述,當應用程序處于維護模式并且只需在Java9上運行時,那么只需要完成執(zhí)行第一步就可以了。對于庫的維護者來說,問題不在于Java9的支持是否必要,而在于何時提供支持。相比于遷移應用程序,將庫遷移到Java9和模塊會引發(fā)完全不同的問題,在第10章中將會解決這些問題。但首先,如果沒有為應用程序采用模塊,那么將應用程序遷移到Java9上需要做些什么呢?應該清楚的是,為Java8或更早版本所開發(fā)的應用程序必須遵循最佳實踐(僅使用公共JDKAPI)才能正常工作。雖然JDK9仍然是向后兼容的,但是已經(jīng)做了許多內(nèi)部改變。可能遇到的遷移問題通常是由JDK的錯誤使用造成的,而這些錯誤使用可能是應用程序代碼本身的問題,或者更可能是庫的問題。當談到遷移問題時,庫可能是遷移失敗的主要根源。許多框架和庫對JDK的(非公共的,因此也是不支持的)實現(xiàn)細節(jié)進行了假設。從技術上講,不能責怪JDK破壞了代碼。事實上,事情更加微妙。7.2節(jié)介紹了在不破壞現(xiàn)有庫的情況下實現(xiàn)強封裝的折中辦法。在一個理想的世界中,在Java9發(fā)布之前庫和框架就將它們的實現(xiàn)更新為與Java9兼容。但不幸的是,這不是我們生活的世界。作為庫和框架的用戶,應該知道如何解決潛在的問題。本章的其余部分將重點介紹即使在非理想的世界中,應該采取哪些策略使應用程序在Java9上運行。希望隨著時間的流逝,這一章會變得過時。7.1類路徑已經(jīng)“死”了?前面的章節(jié)介紹了模塊路徑。在許多方面,可以將模塊路徑視為類路徑的繼任者。這是否意味著類路徑在Java9中不存在了?或者說它消失了嗎?絕對不是!歷史將會告訴我們類路徑是否應該從Java中移除。與此同時,類路徑在Java9中仍然可用,并且工作方式與以前的版本大致相同。在下一章將會看到,類路徑甚至可以與新的模塊路徑結(jié)合使用。如果忽略模塊路徑,而使用類路徑來構(gòu)建和運行應用程序,那么我們根本就沒有在應用程序中使用新的模塊功能。此時需要對現(xiàn)有代碼進行最少的(如果有的話)更改。簡單地說,當應用程序及其依賴項僅使用來自JDK的官方認可的API時,它應該可以在JDK9上編譯并運行而不會出現(xiàn)任何問題。如果說更改是必須的,那是因為JDK本身已經(jīng)實現(xiàn)了模塊化。無論應用程序是否使用了模塊,從Java9開始,它運行的JDK始終由模塊組成。雖然此時從應用程序的角度來看,可以忽略模塊系統(tǒng),但對JDK結(jié)構(gòu)的更改仍然存在。在大多數(shù)情況下,模塊化JDK不會給基于類路徑的應用程序帶來任何問題,但肯定會有一些警告。這些警告在大多數(shù)情況下與庫有關。本章的其余部分將介紹可能存在的問題,更重要的是這些問題的解決方法。7.2庫、強封裝和JDK9類路徑將基于類路徑的應用程序遷移到Java9時可能遇到的一個問題是由平臺模塊中代碼的強大封裝所引起的。許多庫使用了平臺中用Java9封裝的類,或者使用深度反射來窺探平臺類的非公共部分。深度反射使用反射API來訪問類的非公共元素。在6.1.1節(jié)中曾經(jīng)講過,從模塊中導出包不會使其非公共元素可用于反射。但不幸的是,許多庫調(diào)用了通過反射找到的私有元素上的setAccessible??梢钥吹?,當使用模塊時,默認情況下JDK9不允許訪問封裝的包以及深度反射其他模塊(包括平臺模塊)中的代碼。這樣做的一個很好的理由是:平臺內(nèi)部類的濫用已經(jīng)成為許多安全問題的源頭,并且阻礙了API的發(fā)展。但是,在本章中仍然需要在模塊化JDK之上處理基于類路徑的應用程序。在類路徑上,平臺內(nèi)部類的強封裝并不是嚴格執(zhí)行的,盡管它仍然起作用。對JDK類型使用深度反射有時是非常讓人費解的。為什么要使JDK類的私有部分可訪問呢?事實證明,一些常用的庫都是這么做的。javassist運行時代碼生成庫就是一個例子,其他許多框架都使用它。為了便于將基于類路徑的應用程序遷移到Java9,在對平臺模塊中的類應用深度反射時,或者使用反射來訪問非導出包中的類型時,JVM默認顯示警告。例如,當運行使用javassist庫的代碼時,會看到以下警告:WARNING:Anillegalreflectiveaccessoperationhasoccurred

WARNING:Illegalreflectiveaccessbyxy.SecurityActions

(...javassist-3.20.0-GA.jar)tomethod

java.lang.ClassLoader.defineClass(...)

WARNING:Pleaseconsiderreportingthistothemaintainersof

xy.SecurityActions

WARNING:Use--illegal-access=warntoenablewarningsoffurtherillegal

reflectiveaccessoperations

WARNING:Allillegalaccessoperationswillbedeniedinafuturerelease

接下來仔細思考一下。那些在JDK8和更早的版本上運行沒有任何問題的代碼現(xiàn)在會在控制臺上顯示一個醒目的警告——即使是在生產(chǎn)環(huán)境中也是如此。這表明嚴重破壞了強封裝。除了這個警告之外,應用程序仍然照常運行。如警告消息所示,在下一個Java版本中行為將發(fā)生變化。將來,即使是類路徑上的代碼,JDK也會強制執(zhí)行平臺模塊的強封裝。在未來的Java版本中,相同的應用程序?qū)⒉荒茉谀J設置下運行。因此,好好研究一下警告信息并解決潛在的問題是非常重要的。如果警告是由庫引起的,那么通常意味著向維護者報告了相關問題。在默認情況下,只會在第一次非法訪問嘗試時產(chǎn)生一個警告,而后續(xù)的嘗試將不會產(chǎn)生額外的錯誤或警告。如果想要進一步調(diào)查問題的原因,可以使用--illegal-access命令行標志的不同設置來調(diào)整行為:--illegal-access=permit

默認行為。允許對封裝類型進行非法訪問。當?shù)谝淮螄L試通過反射進行非法訪問時會生成一個警告。--illegal-access=warn

與permit一樣,但每次非法訪問嘗試時都會產(chǎn)生錯誤。--illegal-access=debug

同時顯示非法訪問嘗試的堆棧跟蹤。--illegal-access=deny

不允許非法的訪問嘗試。這將是未來的默認行為。請注意,沒有允許取消打印的警告的設置。這是由設計所決定的。在本章中,將學習如何解決潛在的問題,以消除非法訪問警告。由于--illegal-access=deny將是未來的默認設置,因此下一步目標是使用此設置運行應用程序。如果使用--illegal-access=deny運行使用javassist的代碼,那么應用程序?qū)o法運行,并且會看到以下錯誤:java.lang.reflect.InaccessibleObjectException:Unabletomakeprotectedfinal

java.lang.Classjava.lang.ClassLoader.defineClass(java.lang.String,byte[],

int,int,java.security.ProtectionDomain)

throwsjava.lang.ClassFormatErroraccessible:modulejava.basedoesnot

"opensjava.lang"tounnamedmodule@0x7b3300e5

這個錯誤解釋了javassist試圖調(diào)用java.lang.Class上的defineClass方法??梢允褂?-add-opens標志授予對模塊中特定包的類路徑深度反射訪問?!吧疃确瓷洹币还?jié)中已經(jīng)詳細地討論了開放式模塊和開放式包?,F(xiàn)在復習一下相關內(nèi)容,為了進行深度反射,需要開放包。當導出包時也是如此,就像此時所使用的java.lang一樣。通常在模塊描述符中開放一個包,這與導出包的方式類似。可以通過命令行對那些無法控制的模塊(例如平臺模塊)執(zhí)行相同的操作:java--add-opensjava.base/java.lang=ALL-UNNAMED

在本示例中,java.base/java.lang是授權(quán)訪問的模塊/包。最后一個參數(shù)是獲取訪問權(quán)限的模塊。因為代碼仍然在類路徑中,所以使用了ALL-UNNAMED,它表示類路徑?,F(xiàn)在,這個包是開放的,所以深度反射不再是非法的,從而消除了警告(或者避免使用--illegal-access=deny運行時出錯)。同樣,當類路徑上的代碼嘗試訪問非導出包中的類型時,可以使用--add-exports來強制導出包。在下一節(jié)中將會看到這種情況的一個例子。請記住,這僅僅是一種解決方法??梢栽儐栆粋€庫的維護者,即使是使用正確修復的已更新庫版本,也會導致非法訪問問題。默認設置--illegalaccess=permit允許對JDK9之前已經(jīng)存在但現(xiàn)在被封裝的包進行非法訪問。即使代碼位于類路徑中,JDK9中的任何新的封裝包都會被強封裝。安全影響--add-opens和--add-exports如何影響安全性?在未來的Java版本中,默認情況下不允許對平臺模塊進行深度反射的原因之一是為了防止惡意代碼到達危險的JDK內(nèi)部。是否可以使用一個標志來簡單地禁用這些檢查,同時繼續(xù)保持這種安全優(yōu)勢呢?一方面,答案是肯定的,但是當選擇這么做時,無形間就打開了一個更大的攻擊面。但考慮到這點:僅僅通過執(zhí)行Java代碼,無法在運行時獲得由--add-opens或--add-exports提供的權(quán)限。攻擊者需要訪問應用程序的啟動腳本(命令行)來添加這些標志。當建立了這種訪問級別時,攻擊者已經(jīng)可以進行任意修改了,而不僅僅是添加JVM選項了。7.3編譯和封裝的APIJDK包含許多私有的內(nèi)部API,這些API應該僅能被JDK所使用。從早期開始,這一規(guī)定就已被清楚地記載下來了。比如sun.*和ernal.*包。作為應用程序開發(fā)人員,可能并不會直接使用這些類型。大多數(shù)的內(nèi)部類僅用于一些極端情況,典型的應用程序通常不需要。在編寫本書的時候,甚至很難從應用程序開發(fā)的角度提供一個恰當?shù)氖纠?。當然,一些應用程序以及(較舊的)庫仍然使用這些內(nèi)部類。在以前,JDK內(nèi)部沒有進行強封裝,因為當時沒有這樣的機制。當使用內(nèi)部類時,Java9之前的編譯器會發(fā)出警告,但那些警告很容易被忽略。前面已經(jīng)看到,隨著時間的推移,因為--illegal-access=permit默認設置,使用了封裝JDK類型且在舊版本Java上編譯的代碼仍然可以在Java9上運行。然而,相同的代碼在Java9上卻無法通過編譯!假設使用JDK8編譯器編譯示例7-1所示的代碼,該代碼使用了sun.security.x509包中的類型。示例7-1:EncapsulatedTypes.java(chapter7/encapsulation)packageencapsulated;

importsun.security.x509.X500Name;

publicclassEncapsulatedTypes{

publicstaticvoidmain(String...args)throwsException{

System.out.println(newX500Name("","test",

"test","US"));

}

}

使用JDK9編譯上面的代碼,會產(chǎn)生如下所示編譯器錯誤:./src/encapsulated/EncapsulatedTypes.java:3:error:packagesun.security.x509

isnotvisible

importsun.security.x509.X500Name;

^

(packagesun.security.x509isdeclaredinmodulejava.base,whichdoesnot

exportittotheunnamedmodule)

默認情況下,盡管代碼使用封裝包,但代碼仍然可以在Java9上成功運行。你可能想知道為什么在訪問封裝類型時,javac和java之間存在區(qū)別。當無法編譯相同的代碼時,能夠運行訪問封裝類型的代碼又有什么意義呢?這樣的代碼之所以仍然能夠運行,其原因是為現(xiàn)有的庫提供向后兼容性。而禁止相同封裝類型編譯的原因是為了防止將來可能出現(xiàn)的兼容性噩夢。對于自己可以控制的代碼,當涉及封裝類型時,應立即采取行動,并用非封裝的替代類型替換它們。當所使用的庫(用較舊的Java版本進行編譯)使用了封裝類型或?qū)DK內(nèi)部進行深度反射時,情況就更加復雜了。此時無法自己修復這個問題,這樣一來,會阻止你向Java9的遷移。但由于較寬松的運行時,因此庫仍然可以暫時使用。允許在運行時使用封裝的JDK類型只是一種臨時情況。在未來的Java版本中,這種情況將被禁止。通過設置上一節(jié)中所介紹的--illegal-access=deny標志可以防止這類情況的出現(xiàn)。使用java--illegal-access=deny運行相同的代碼會生成一個錯誤:Exceptioninthread"main"java.lang.IllegalAccessError:

classencapsulated.EncapsulatedTypes(inunnamedmodule@0x2e5c649)cannot

accessclasssun.security.x509.X500Name(inmodulejava.base)becausemodule

java.basedoesnotexportsun.security.x509tounnamedmodule@0x2e5c649

atencapsulated.EncapsulatedTypes.main(EncapsulatedTypes.java:7)

請注意,如果使用deny之外的任何其他值配置--illegal-access,則不會顯示任何警告。只有反射性的非法訪問會觸發(fā)前面所看到的警告,而靜態(tài)引用封裝類型則不會。該限制非常實用:如果將VM更改為對封裝類型的靜態(tài)引用也會產(chǎn)生警告,那么這種更改就顯得過于侵入性了。正確的做法是將問題報告給庫的維護者。但是,如果是自己的代碼,同時需要使用JDK9重新編譯,但又不能立即更改代碼,應該怎么做呢?更改代碼總是有風險的,所以必須找到更改的合適時機。也可以使用命令行標志在編譯時打破封裝。在上一節(jié)中,看到了如何使用--add-opens從命令行公開一個包。java和javac都支持--add-exports,顧名思義,可以使用它從模塊導出一個封裝的包。語法是--add-exports<module>/<package>=<targetmodule>。因為代碼仍然在類路徑上運行,所以可以使用ALL-UNNAMED作為目標模塊。請注意,導出封裝的包并不意味著可以對其類型進行深度反射。要進行深度反射,必須開放包。目前,導出包就足夠了。在示例7-1中,直接引用了封裝類型,而沒有涉及任何反射。而對于sun.security.x509.X500Name示例,可以使用下面的命令編譯并運行:javac--add-exportsjava.base/sun.security.x509=ALL-UNNAMED\

encapsulated/EncapsulatedTypes.java

java--add-exportsjava.base/sun.security.x509=ALL-UNNAMED

\encapsulated.EncapsulatedTypes

不僅對JDK內(nèi)部,--add-exports和--add-opens標志可用于任何模塊和包。在編譯過程中,內(nèi)部API的使用仍然會發(fā)出警告。理想情況下,--add-exports標志是臨時遷移步驟??梢砸恢笔褂盟?,直到將代碼調(diào)整為公共API,或(如果庫違反規(guī)則)直到使用替代API的第三方庫的新版本發(fā)布。太多命令行標志一些操作系統(tǒng)限制了可執(zhí)行命令行的長度。當在遷移期間需要添加許多標志時,可能會觸犯這些限制。此時的替代做法是使用一個文件將所有的命令行參數(shù)提供給java/javac:$java@arguments.txt

參數(shù)文件必須包含所有必要的命令行標志。文件中的每一行都包含一個選項。例如,arguments.txt可以包含以下內(nèi)容:-cpapplication.jar:javassist.jar

--add-opensjava.base/java.lang=ALL-UNNAMED

--add-exportsjava.base/sun.security.x509=ALL-UNNAMED

-jarapplication.jar

即使沒有命令行限制,相比于腳本中非常長的命令行,參數(shù)文件可能更清晰。7.4刪除的類型代碼也可以使用目前已經(jīng)完全刪除的內(nèi)部類型,雖然這與模塊系統(tǒng)沒有直接關系,但仍值得一提。其中一個在Java9中刪除的內(nèi)部類是sun.misc.BASE64Encoder,在Java8引入java.util.Base64類之前,該類是非常流行的。示例7-2顯示了使用BASE64Decoder的代碼:示例7-2:RemovedTypes.java(chapter7/removedtypes)packageremoved;

importsun.misc.BASE64Decoder;

//CompilewithJava8,runonJava9:NoClassDefFoundError.

publicclassRemovedTypes{

publicstaticvoidmain(String...args)throwsException{

newBASE64Decoder();

}

}

該代碼在Java9上無法編譯或運行。當嘗試編譯時,會看到以下所示錯誤:removed/RemovedTypes.java:3:error:cannotfindsymbol

importsun.misc.BASE64Decoder;

^

symbol:classBASE64Decoder

location:packagesun.misc

removed/RemovedTypes.java:8:error:cannotfindsymbol

newBASE64Decoder();

^

symbol:classBASE64Decoder

location:classRemovedTypes

2errors

如果使用舊版本的Java編譯上面的代碼,但試圖在Java9上運行,也會失?。篍xceptioninthread"main"java.lang.NoClassDefFoundError:sun/misc/BASE64Decoder

atremoved.RemovedTypes.main(RemovedTypes.java:8)

Causedby:java.lang.ClassNotFoundException:sun.misc.BASE64Decoder

...

對于封裝類型,可以通過使用命令行標志進行強制訪問來解決此問題。但此時針對Base64Decoder示例無法這么做,因為該類不存在。理解這種差異是很重要的。使用jdeps查找已刪除或封裝的類型及其替代方法jdeps是JDK附帶的一個工具。jdeps可以做的一件事是找到使用被刪除或封裝的JDK類型的地方,并建議替換。jdeps總是在類文件上工作,而不是在源代碼上。如果使用Java8編譯示例7-2,則可以在生成的類上運行jdeps:jdeps-jdkinternalsremoved/RemovedTypes.class

RemovedTypes.class->JDKremovedinternalAPI

removed.RemovedTypes->sun.misc.BASE64Decoder

JDKinternalAPI(JDKremovedinternalAPI)

Warning:JDKinternalAPIsareunsupportedandprivatetoJDKimplementation

thataresubjecttoberemovedorchangedincompatiblyandcould

breakyourapplication.

PleasemodifyyourcodetoeliminatedependenceonanyJDKinternalAPIs.

ForthemostrecentupdateonJDKinternalAPIreplacements,pleasecheck:

/display/JDK8/Java+Dependency+Analysis+Tool

JDKInternalAPISuggestedReplacement

-------------------------------------

sun.misc.BASE64DecoderUsejava.util.Base64@since1.8

類似地,示例7-1中諸如X500Name之類的封裝類型也被jdeps報告并帶有建議的替換方法。有關如何使用jdeps的更多詳細信息,請參閱8.9節(jié)。從Java8開始,JDK就包含了java.util.Base64,這是一個更好的選擇。在這種情況下,解決方案就很簡單:必須遷移到公共API以便在JDK9上運行。一般來說,遷移到Java9將會暴露本章討論的許多技術債務(technicaldebt)。7.5使用JAXB和其他JavaEEAPI在過去,JDK附帶的某些JavaEE技術(如JAXB)是與JavaSEAPI一起提供的。這些技術在Java9中仍然存在,但是需要特別注意。它們主要包含在以下模塊列表中:·java.activation·java.corba·java.transaction·java.xml.bind·java.xml.ws·java.xml.ws.annotation在Java9中,這些模塊已被棄用。@Deprecated注釋在Java9中有一個新的參數(shù)forRemoval。如果將其設置為true,則意味著API元素將在未來版本中被刪除。對于屬于JDK的API元素來說,這意味著在下一個主要版本中可能會被刪除。有關棄用的更多細節(jié)可以在JEP277(\h/jeps/277)中找到。從JDK中刪除JavaEE技術有很好的理由。JDK中JavaSE和JavaEE之間的重疊一直是令人困惑的。JavaEE應用程序服務器通常提供了API的自定義實現(xiàn)。簡單地講,是通過將替代實現(xiàn)放在類路徑上,覆蓋默認的JDK版本來完成的。而在Java9中,這就成為一個問題。模塊系統(tǒng)不允許多個模塊提供相同的包。如果在類路徑中找到重復的包(因此在未命名的模塊中),則會將其忽略。在任何情況下,若JavaSE和應用程序服務器都提供java.xml.bind,則不會實現(xiàn)預期的行為。這是一個嚴重的實際問題,會破壞許多現(xiàn)有的應用服務器和相關工具。為了避免出現(xiàn)該問題,在基于類路徑的場景中,默認情況下不會解析這些模塊。接下來看一下圖7-1所示的平臺模塊圖。圖7-1:顯示只能通過java.se.ee(而不是java.se)訪問的模塊的JDK模塊圖的子集最頂層是java.se和java.se.ee模塊。兩者都是聚合器模塊,這些模塊不包含代碼,而是將一組更細粒度的模塊組合在一起。聚合器模塊已經(jīng)在5.4節(jié)中詳細討論過。大多數(shù)平臺模塊駐留在java.se中,圖中并沒有顯示(但可以從圖2-1中看到整個模塊圖)。java.se.ee模塊聚合了正在討論的模塊,它們不是java.se聚合器模塊的一部分,其中包括包含了JAXB類型的java.xml.bind模塊。默認情況下,在編譯和運行未命名模塊中的類時,javac和java都使用java.se作為根,代碼可以訪問由java.se的可傳遞依賴項導出的任何包。因此,在java.se.ee中卻不在java.se下的模塊不會被解析,所以它們也不會被未命名的模塊讀取。即使從模塊java.xml.bind中導出包javax.xml.bind也沒關系,因為在編譯和運行時不會對其進行解析。如果java.se.ee中的模塊是必需的,那么就需要將它們顯式地添加到已解析的平臺模塊集合中??梢允褂胘avac和java的--add-modules標志將這些模塊添加為根模塊。接下來看一下基于JAXB的示例7-3。該示例將Book序列化為XML。示例7-3:JaxbExample.java(chapter7/jaxb)packageexample;

importjavax.xml.bind.JAXBContext;

importjavax.xml.bind.JAXBException;

importjavax.xml.bind.Marshaller;

publicclassJaxbExample{

publicstaticvoidmain(String...args)throwsException{

Bookbook=newBook();

book.setTitle("Java9Modularity");

JAXBContextjaxbContext=JAXBContext.newInstance(Book.class);

MarshallerjaxbMarshaller=jaxbContext.createMarshaller();

jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT,true);

jaxbMarshaller.marshal(book,System.out);

}

}

在Java8上,該示例可以編譯和運行而不會出現(xiàn)任何問題。但在Java9上,則會在編譯時出現(xiàn)幾個錯誤:example/JaxbExample.java:3:error:packagejavax.xml.bindisnotvisible

importjavax.xml.bind.JAXBContext;

^

(packagejavax.xml.bindisdeclaredinmodulejava.xml.bind,whichisnot

inthemodulegraph)

example/JaxbExample.java:4:error:packagejavax.xml.bindisnotvisible

importjavax.xml.bind.JAXBException;

^

(packagejavax.xml.bindisdeclaredinmodulejava.xml.bind,whichisnot

inthemodulegraph)

example/JaxbExample.java:5:error:packagejavax.xml.bindisnotvisible

importjavax.xml.bind.Marshaller;

^

(packagejavax.xml.bindisdeclaredinmodulejava.xml.bind,whichisnot

inthemodulegraph)

3errors

當使用Java8進行編譯并使用Java9運行代碼時,就會在運行時生成報告相同問題的異常。前面已經(jīng)介紹了如何解決這個問題:將--add-modulesjava.xml.bind添加到javac和java調(diào)用中。除了添加包含JAXB的平臺模塊之外,還可以添加一個為類路徑提供JAXB的JAR。幾個流行的(開源)庫提供了JAXB實現(xiàn)。由于JDK中的JavaEE模塊被標記為“刪除”,因此這是一個更具前瞻性的解決方案。請注意,如果使用模塊路徑,則不會遇到該問題。如果代碼存在于一個模塊中,那么它必須在java.base以外的任何模塊上顯式地定義一個需求。對于示例代碼來說,包括對java.xml.bind的依賴?;诖嗽?,模塊系統(tǒng)不需要命令行標志即可解析這些模塊??偨Y(jié)一下,當從JDK使用JavaEE代碼時要格外小心。當接收到包不可見的錯誤時,可以通過使用--add-modules添加相關模塊。但是請注意,所添加的模塊將在下一個主要的Java版本中被刪除??梢詫⒆约旱倪@些技術版本添加到類路徑中,以避免將來出現(xiàn)問題。7.6jdk.unsupported模塊JDK的一些內(nèi)部類已經(jīng)被證明更難以進行封裝。一般來說,使用sun.misc.Unsafe等類的機會是比較少的。這些一直是不受支持的類,意味著只能在JDK內(nèi)部使用。由于性能原因,這些類中的一些被許多庫廣泛使用。顯而易見這種做法是不對的,但在某些情況下這是唯一的選擇。一個著名的示例是sun.misc.Unsafe類,它可以繞過Java的內(nèi)存模型和其他安全網(wǎng)執(zhí)行一些低級操作,而JDK之外的庫無法實現(xiàn)相同的功能。如果簡單地封裝這些類,那么依賴于它們的庫將不再使用JDK9,至少沒有警告。從理論上講,這不是一個向后兼容的問題,畢竟這些庫濫用了不支持的實現(xiàn)類。對于這些使用頻率比較高的內(nèi)部API來說,由于其在現(xiàn)實世界中的作用太大而無法忽視,特別是當它們所提供的功能沒有替代方案時更是如此。考慮到這一點,達成了妥協(xié)。JDK團隊研究了哪些JDK平臺內(nèi)部類是庫使用最多的,哪些只能在JDK內(nèi)部實現(xiàn)。這些類沒有封裝在Java9中。下面所示的是可以訪問的特定類和方法的列表:·sun.misc.{Signal,SignalHandler}·sun.misc.Unsafe·sun.reflect.Reflection::getCallerClass(int)·sun.reflect.ReflectionFactory::newConstructorForSerialization請記住,如果這些名字對你來說沒有任何意義,那是一件好事。諸如Netty、Mockito和Akka之類的流行庫都使用了這些類。不破壞這些庫也是一件好事。由于這些方法和類主要不是為在JDK之外使用而設計的,因此可以將它們移動到名為jdk.unsupported的平臺模塊中。這表明在未來的Java版本中,該模塊中的類將被其他API所替換。jdk.unsupported模塊導出和/或公開了包含所討論類的內(nèi)部包?,F(xiàn)有的用途包括深度反射。與7.2節(jié)中所討論的場景不同,通過反射使用這些類不會導致警告。這是因為jdk.unsupported在其模塊描述符中公開了必需的包,所以從這個角度來看不存在非法訪問。雖然可以在不破壞封裝的情況下使用這些類型,但它們?nèi)匀徊皇苤С?,依舊不鼓勵使用這些類型。預計在將來會提供支持的替代類型。例如,Unsafe中一些功能被JEP193(\h/jeps/193)中所提出的變量句柄(variablehandle)所取代。但在此之前,仍然維持現(xiàn)狀。當代碼仍然存在于類路徑上時,不需要進行任何更改。庫可以像以前一樣通過類路徑使用這些類,并且在沒有任何警告或錯誤的情況下運行。編譯器針對jdk.unsupported中的類進行編譯時會生成警告,而不是像編譯封裝類型一樣產(chǎn)生錯誤:warning:UnsafeisinternalproprietaryAPIandmaybe

removedinafuturerelease

如果想從模塊中使用這些類型,則必須依靠jdk.unsupported。在模塊描述符中有這樣一個requires語句就相當于是一個警告標志。在未來的Java版本中,其可能需要進行更改以適應公開支持的API,而不是不支持的API。7.7其他更改JDK9中的許多其他更改可能會破壞代碼。比如,這些更改會影響工具作者以及使用JDK擴展機制的應用程序。其中一些變化如下:·JDK布局由于平臺模塊化,包含所有平臺類的大rt.jar不再存在。正如JEP220(\h/jeps/220)中所記載的那樣,JDK本身的布局也發(fā)生了相當大的變化。依靠JDK布局的工具或代碼必須適應這個新的現(xiàn)實。·版本字符串所有Java平臺版本都以1.x為前綴開頭的日子已經(jīng)一去不復返了。Java9對應的是版本9.0.0。版本字符串的語法和語義已經(jīng)發(fā)生了很大的變化。如果應用程序?qū)ava版本進行任何類型的解析,請閱讀JEP223(\h/jeps/223)以了解所有的細節(jié)?!U展機制諸如授權(quán)標準覆蓋機制(EndorsedStandardOverrideMechanism)以及通過java.ext.dirs屬性的擴展機制等功能已被刪除。它們被可升級的模塊所取代。更多的信息可以在JEP220(\h/jeps/220)中找到。這些都是JDK的高度專業(yè)化功能。如果應用程序確實需要它們,那么它就不能使用JDK9。因為這些更改與Java模塊系統(tǒng)沒有真正的關系,所以在此不做詳細討論。鏈接相關的JEP(JDKEnhancementProposal)包含有關如何在這些情況下繼續(xù)進行操作的指導。恭喜!現(xiàn)在你已經(jīng)知道如何在JDK9上運行現(xiàn)有的應用程序了。雖然有些事情可能出錯,但在大多數(shù)情況下,還是比較順利的。請記住,用--illegal-access=deny來運行你的應用程序,以便為將來做好準備。在解決了從類路徑中運行現(xiàn)有的應用程序所存在的問題之后,接下來看一下如何使現(xiàn)有的應用程序更加模塊化。第8章遷移到模塊在了解了前幾章所介紹的模塊優(yōu)點之后,也許你非常希望能夠開始使用Java模塊系統(tǒng)。只要理解了基本概念,編寫基于模塊的新代碼還是比較簡單的。在現(xiàn)實世界中,存在許多需要遷移到模塊中的現(xiàn)有代碼。上一章介紹了如何將現(xiàn)有的代碼遷移到Java9,而無須將代碼轉(zhuǎn)換為模塊。這只是任何遷移方案中的第一步。掌握了相關內(nèi)容之后,接下來可以把注意力放在如何遷移到Java模塊系統(tǒng)上了。在此,并不建議遷移每個現(xiàn)有的應用程序以使用Java模塊系統(tǒng)。如果一個應用程序不再被積極開發(fā),那么這么做是不值得的。此外,小型應用程序可能不會真正從模塊結(jié)構(gòu)中受益。在合理的情況下進行遷移,以提高可維護性、可變性和可重用性——而不僅僅是為了遷移而遷移。移植所需的工作量在很大程度上取決于代碼庫的結(jié)構(gòu)如何,但即使對于結(jié)構(gòu)良好的代碼庫,遷移到模塊化運行時也是一項艱巨的任務。大多數(shù)應用程序都使用了第三方庫,這是遷移時需要考慮的一個重要因素。這些庫不一定是模塊化的,當然你可能也不想承擔這個責任。幸運的是,Java模塊系統(tǒng)將向后兼容性和可移植性作為主要的關注點。在Java模塊系統(tǒng)中引入了幾個結(jié)構(gòu),從而可以對現(xiàn)有代碼進行逐步遷移。在本章中,將了解這些構(gòu)造,從而將自己的代碼遷移到模塊。從庫維護者的角度來看,遷移當然是需要的,但是需要一個略微不同的過程(第10章將著重介紹該內(nèi)容)。8.1遷移策略一個典型的應用程序包含應用程序代碼以及庫代碼,而應用程序代碼可能會使用來自第三方庫的代碼。在理想情況下,所有使用的庫都已經(jīng)是模塊,此時只需專注于模塊化自己的代碼即可。在Java9發(fā)布后的頭幾年,現(xiàn)實情況可能不是這樣的,一些庫還不能作為模塊來使用,也許永遠不能,因為它們不再被維護。如果一直等待整個生態(tài)系統(tǒng)向模塊方向發(fā)展,那么可能要等很長時間。此外,還需要更新到這些庫的新版本,從而可能引起一系列問題。也可以手動修補庫,添加一個模塊描述符并將它們轉(zhuǎn)換為一個模塊。顯然,這需要完成大量工作,并且需要對庫進行拆分(fork),這使得未來的更新變得更加痛苦。如果能夠?qū)W⒂谶w移自己的代碼,而暫時保留庫現(xiàn)有的工作方式,那就最好了。8.2一個簡單示例本章將介紹幾個遷移示例,以了解實踐中可能遇到的各種情況。首先看一個簡單的應用程序,其使用Jackson庫將Java對象轉(zhuǎn)換為JSON。對于這個應用程序,需要Jackson項目中的三個JAR文件:·com.fasterxml.jackson.core·com.fasterxml.jackson.databind·com.fasterxml.jackson.annotations本例所使用的版本(2.8.8)中的JacksonJAR文件并不是模塊。它們是普通JAR文件,沒有模塊描述符。應用程序由兩個類組成,其中示例8-1列出了主類。這里沒有列出的Book類是一個表示書的簡單類,包含getter和setter。Main類包含一個使用com.fasterxml.jackson.databind的ObjectMapper將一個Book實例轉(zhuǎn)換為JSON的main方法。示例8-1:Main.java(chapter8/jackson-classpath)packagedemo;

importcom.fasterxml.jackson.databind.ObjectMapper;

publicclassMain{

publicstaticvoidmain(String...args)throwsException{

BookmodularityBook=

newBook("Java9Modularity","Modularizeallthethings!");

ObjectMappermapper=newObjectMapper();

Stringjson=mapper.writeValueAsString(modularityBook);

System.out.println(json);

}

}

示例中的com.fasterxml.jackson.databind.ObjectMapper類是jackson-databind-2.8.8.jar的一部分。這個JAR文件依賴于jackson-core-2.8.8.jar和jackson-annotations-2.8.8.jar。但是,這種依賴關系是隱式的,因為JAR文件不是模塊。示例項目開始時具有以下文件結(jié)構(gòu):├──lib

│├──jackson-annotations-2.8.8.jar

│├──jackson-core-2.8.8.jar

│└──jackson-databind-2.8.8.jar

└──src

└──demo

├──Book.java

└──Main.java

正如在上一章中看到的那樣,類路徑在Java9中仍然可用。在開始遷移到模塊之前,首先在類路徑上構(gòu)建和運行??梢允褂檬纠?-2中的命令來構(gòu)建和運行應用程序。示例8-2:run.sh(chapter8/jackson-classpath)CP=lib/jackson-annotations-2.8.8.jar:

CP+=lib/jackson-core-2.8.8.jar:

CP+=lib/jackson-databind-2.8.8.jar

javac-cp$CP-dout-sourcepathsrc$(findsrc-name'*.java')

java-cp$CP:outdemo.Main

該應用程序可以使用Java9編譯和運行,而無須進行任何更改。雖然Jackson庫并不在我們的直接控制之下,但我們卻可以控制Main和Book的代碼,所以這些代碼是遷移時所需要重點關注的。這是一個非常常見的遷移情況,將自己的代碼轉(zhuǎn)移到模塊上,而不用擔心庫。Java模塊系統(tǒng)提供了一些技巧來實現(xiàn)逐步遷移。8.3混合類路徑和模塊路徑為了使逐步遷移成為可能,可以混合使用類路徑和模塊路徑。但這不是一種理想的情況,因為只能部分受益于Java模塊系統(tǒng)的優(yōu)點。但是,“小步”遷移是非常有幫助的。因為Jackson庫不是我們自己的源代碼,所以理想情況下根本不會更改它們。相反,首先通過遷移自己的代碼開始自上而下的遷移。接下來將代碼放到一個名為books的模塊中。雖然這樣做是遠遠不夠的,但卻可以通過為模塊創(chuàng)建一個簡單的module-info.java開始遷移之旅:modulebooks{

}

請注意,該模塊還沒有任何requires語句。這非??梢?,因為顯而易見其需要依賴于jackson-databind-2.8.8.jar文件中的類。因為已經(jīng)有了一個真正的模塊,所以可以使用--module-source-path標志來編譯代碼。Jackson庫不是模塊,所以暫時留在類路徑上:CP=lib/jackson-annotations-2.8.8.jar:

CP+=lib/jackson-core-2.8.8.jar:

CP+=lib/jackson-databind-2.8.8.jar

javac-cp$CP-dout--module-source-pathsrc-mbook

ssrc/books/demo/Main.java:3:error:

packagecom.fasterxml.jackson.databinddoesnotexist

importcom.fasterxml.jackson.databind.ObjectMapper;

^

src/books/demo/Main.java:11:error:cannotfindsymbol

ObjectMappermapper=newObjectMapper();

^

symbol:classObjectMapper

location:classMain

src/books/demo/Main.java:11:error:cannotfindsymbol

ObjectMappermapper=newObjectMapper();

^

symbol:classObjectMapper

location:classMain

3errors

此時,編譯器顯然不開心!盡管jackson-databind-2.8.8.jar仍然在類路徑中,但編譯器卻告知無法在模塊中使用。模塊不能讀取類路徑,所以模塊不能訪問類路徑上的類型,如圖8-1所示。圖8-1:模塊不能讀取類路徑即使在遷移過程中需要做一些工作,而無法從類路徑中讀取卻是一件好事,因為需要明確依賴關系。如果模塊可以讀取類路徑,除了便于獲取顯式依賴關系之外,其他的優(yōu)勢就不復存在了。即便如此,應用程序現(xiàn)在還無法編譯,所以先試著解決這個問題。如果不能依賴類路徑,那么唯一的方法就是將模塊依賴的代碼作為模塊來使用。這就要求將jackson-databind-2.8.8.jar變成一個模塊。8.4自動模塊Jackson庫的源代碼是開源的,所以可以修補代碼,將其變?yōu)橐粋€模塊。在使用長列表(可傳遞)依賴關系的大型應用程序中,并不需要修補所有的依賴關系。此外,我們可能還沒有掌握正確模塊化庫所需的知識。Java模塊系統(tǒng)提供了一個有用的功能來處理非模塊的代碼:自動模塊。只需將現(xiàn)有的JAR文件從類路徑移動到模塊路徑,而不改變其內(nèi)容,就可以創(chuàng)建一個自動模塊。這樣一來,JAR就轉(zhuǎn)換為一個模塊,同時模塊系統(tǒng)動態(tài)生成模塊描述符。相比之下,顯式模塊始終有一個用戶自定義的模塊描述符。到目前為止,所看到的所有模塊(包括平臺模塊)都是顯式模塊。自動模塊的行為不同于顯式模塊。自動模塊具有以下特征:·不包含module-info.class?!に幸粋€在META-INF/MANIFEST.MF中指定或者來自其文件名的模塊名稱?!ねㄟ^requirestransitive請求所有其他已解析模塊。·導出所有包?!ぷx取路徑(或者更準確地講,讀取前面所討論的未命名模塊)?!に荒芘c其他模塊拆分包。上述特征使得自動模塊可以立即用于其他模塊。請注意,自動模塊并不是一個設計良好的模塊。雖然請求所有的模塊并導出所有的包聽起來不像是正確的模塊化,但至少是可用的。請求所有其他已解析模塊是什么意思呢?自動模塊需要已解析模塊圖中的每個模塊。請記住,自動模塊中仍然沒有明確的信息來告訴模塊系統(tǒng)真正需要哪些模塊,這意味著JVM在啟動時不會警告自動模塊的依賴項丟失。作為開發(fā)人員,負責確保模塊路徑(或類路徑)包含所有必需的依賴項。這與使用類路徑?jīng)]有太大區(qū)別。模塊圖中的所有模塊都需要通過自動模塊傳遞。這實際上意味著,如果請求一個自動模塊,那么就可以“免費”獲得所有其他模塊的隱式可讀性。這是一種權(quán)衡,將在稍后詳細討論。請將jackson-databind-2.8.8.jar文件移動到模塊路徑,從而將其轉(zhuǎn)換成一個自動模塊。首先把JAR文件移動到一個新的目錄,在本示例中將其命名為mods:├──lib

│├──jackson-annotations-2.8.8.jar

│└──jackson-core-2.8.8.jar

├──mods

│└──jackson-databind-2.8.8.jar

└──src

└──books

├──demo

├──Book.java

└──Main.java

└──module-info.java

接下來必須修改books模塊中的module-info.java,以便請求jackson.databind:modulebooks{

requiresjackson.databind;

}

books模塊請求jackson.databind,就好像它是一個正常的模塊。但模塊名稱來自哪里呢?自動模塊的名稱可以在META-INF/MANIFEST.MF文件的新引入的Automatic-Module-Name字段中指定。這樣一來,即使在將庫完全遷移到模塊之前,庫維護人員也可以選擇模塊名稱。有關以這種方式命名模塊的更多詳細信息,請參閱10.2節(jié)。如果沒有指定名稱,則模塊名稱是從JAR的文件名派生的。命名算法大致如下:·使用點(.)替換破折號(-)?!ず雎园姹咎?。在Jackson示例中,模塊名稱是基于文件名稱的?,F(xiàn)在,可以通過運行下面的命令成功地編譯程序:CP=lib/jackson-annotations-2.8.8.jar:

CP+=lib/jackson-core-2.8.8.jar

javac-cp$CP--module-pathmods-dout--module-source-pathsrc-mbooks

將jackson-databind-2.8.8.jar文件從類路徑中移除,并配置了一個模塊路徑,指向mods目錄。圖8-2提供了所有代碼的概述。圖8-2:模塊路徑上的非模塊化JAR變成了自動模塊。而類路徑變成了未命名模塊為了運行程序,還需要更新Java調(diào)用:java-cp$CP--module-pathmods:out-mbooks/demo.Main對Java命令完成如下修改:·將out目錄移至模塊路徑中。·將jackson-databind-2.8.8.jar文件從類路徑(lib)移至模塊路徑(mods)中?!ねㄟ^使用-m標志來指定模塊,從而啟動應用程序??梢詫⑺蠮AR移動到模塊路徑,而不僅僅是移動(jackson-databind)。雖然這樣做使得移動過程變得容易一些,但是很難看到究竟發(fā)生了什么。當遷移自己的應用程序時,可以隨意將所有JAR移動到模塊路徑。現(xiàn)在已經(jīng)很接近第一個遷移的應用程序了,但不幸的是在啟動應用程序時仍然遇到了異常:Exceptioninthread"main"java.lang.reflect.InaccessibleObjectException:

Unabletomakepublicjava.lang.Stringdemo.Book.getTitle()accessible:

modulebooksdoesnot"exportsdemo"tomodulejackson.databind

...

雖然這是JacksonDatabind所特有的問題,但并不罕見。此時使用JacksonDatabind來編組Book類(該類是books模塊的一部分)。JacksonDatabind使用反射來查看類的字段以便進行序列化。因此,JacksonDatabind需要訪問Book類;否則,就無法使用反射來查看它的字段。為此,包含該類的包必須通過其包含模塊(本例中的books模塊)被導出或開放。導出包限制了JacksonDatabind只能反射公共元素,而開放包則還允許進行深度反射。在本示例中,反射公共元素就足夠了。此時陷入了一個困境。沒有必要僅因為Jackson需要Book類而將包含Book的包導出到其他模塊。如果這樣做,就意味著放棄封裝,而封裝性是將現(xiàn)有應用程序轉(zhuǎn)換為模塊的主要原因之一!解決這個問題有多種方法,每種方法都有自己的權(quán)衡考慮。第一種方法是使用限制導出。通過使用限制導出,可以將包僅導出到jackson.databind,同時又不會失去其他模塊的封裝:modulebooks{

requiresjackson.databind;

exportsdemotojackson.databind;

}

重新編譯后,現(xiàn)在可以成功運行應用程序了!除了使用導出方法以外,還有另外一種方法可以更好地適應需求,該方法將在下一節(jié)中探討。使用自動模塊時出現(xiàn)的警告盡管自動模塊對遷移至關重要,但應謹慎使用。每當在自動模塊上寫入一個requires時,請記住,稍后再回來。如果庫作為一個顯式模塊發(fā)布,則應該使用該模塊。編譯器中增加了兩個警告,以幫助解決這個問題。請注意,Java編譯器支持這些警告只是一個建議,所以不同的編譯器實現(xiàn)可能會有不同的結(jié)果。第一個警告是選擇退出(默認情況下啟用),并針對自動模塊上的每個requirestransitive發(fā)出警告。該警告可以通過-Xlint:-requires-transitive-automatic標志禁用。請注意冒號后的短劃級(-)。第二個警告是選擇進入(默認情況下禁用),并針對自動模塊上的每個requires發(fā)出警告。該警告可以通過-Xlint:requires-automatic(冒號后沒有短劃線)標志來啟用。第一個警告之所以是默認啟用的,原因在于這是一種更危險的情況。通過隱式可讀性,會將一個(可能不穩(wěn)定的)自動模塊公開給模塊的使用者。當顯式模塊可用時,可以用顯式模塊替代自動模塊,如果顯式模塊尚不可用,可以要求庫維護者提供。另外請記住,這樣的模塊可能提供了受更多限制的API,因為庫維護者并不希望默認情況下導出所有包。當從自動模塊切換到顯式模塊時會產(chǎn)生額外的工作,庫維護者需要創(chuàng)建一個模塊描述符。8.5開放式包在反射的上下文中使用exports時有一些需要注意的地方。首先,需要將編譯時可讀性賦予一個包,這一點看上去非常奇怪,因為我們只期望運行時(反射)使用。雖然框架經(jīng)常使用反射來處理應用程序代碼,但是它們不需要編譯時可讀性。另外,不可能總是事先知道哪個模塊需要可讀性,所以限制導出是不可能的。使用JavaPersistenceAPI(JPA)就是這種情況的一個例子。當使用JPA時,通常編程為標準化的API。而在運行時,會使用該API的實現(xiàn),比如Hibernate或者EclipseLink。API和實現(xiàn)在不同的模塊中。最終,實現(xiàn)需要訪問類。如果在模塊中將exportscom.mypackage放到hibernate.core或類似的包中,就會連接到實現(xiàn)。如果想要更改JPA實現(xiàn),就需要更改代碼的模塊描述符,而這恰恰是泄露實現(xiàn)細節(jié)的跡象。如6.1.2節(jié)中所討論的那樣,當涉及反射時會出現(xiàn)另一個問題:導出包只導出了包中的公共類型,受保護的或者包專用的類以及導出類中的非公共方法和字段都是不可訪問的。即使導出了包,深度反射(使用setAccessible方法)也不起作用。如果想要進行深度反射(許多框架需要該功能),一個包必須是開放的。返回到Jackson示例,此時可以使用opens關鍵字,而不是對jsckson.databind進行限制導出:modulebooks{

requiresjackson.databind;

opensdemo;

}

一個開放式包允許任何模塊對其類型進行運行時訪問(包括深度反射),但編譯時訪問卻是禁止的。這避免了其他人在編譯時意外地使用了實現(xiàn)代碼,而框架可以在運行時毫無問題地施展它們的“魔力”。當僅需要運行時訪問時,在大多數(shù)情況下opens是一個不錯的選擇。請記住,一個開放式包并沒有真正地封裝,其他模塊始終可以使用反射來訪問這個包。但至少在開發(fā)過程中受到了保護,避免了意外使用,并且清楚地表明這個軟件包不能被其他模塊直接使用。與exports關鍵字一樣,opens關鍵字也可以被限制,從而向一組有限的模塊開放包:modulebooks{

requiresjackson.databind;

opensdemotojackson.databind;

}

現(xiàn)在已經(jīng)看到了解決運行時可訪問性問題的兩種方法,但仍然存在一個問題:為什么只有在運行應用程序時才發(fā)現(xiàn)此問題,而不是在編譯時呢?為了更好地理解這種情況,需要重新審視可讀性規(guī)則。對于一個能夠讀取來自另一個模塊中另一個類的類來說,需要滿足以下條件:·該類必須是公共的(忽略深度反射的情況)?!ち硪粋€模塊中的包必須被導出,或者在進行深度反射時是開放的?!はM模塊與其他模塊之間必須具有可讀性關系(requires)。通常,可以在編譯時對上述條件進行檢查。然而,JacksonDatabind對所編寫的代碼并不存在編譯時依賴。它之所以知道Book類,因為它將作為參數(shù)傳入ObjectMapper。這意味著編譯器并不能幫助我們。在進行反射時,運行時會自動設置一個可讀性關系(requires),所以該步驟要額外小心。接下來,它會發(fā)現(xiàn)該類在運行時沒有導出或開放(因此也不可訪問),并且不會由運行時自動“修復”。既然運行時足夠聰明,可以自動添加可讀性關系,那么為什么不能開放包呢?這涉及意圖和模塊所有權(quán)的問題。當代碼使用反射訪問另一個模塊中的代碼時,從該模塊的角度來看,其目的顯然是讀取其他模塊,所以無須過多地明確這一點。但對于exports/opens來說卻不是這樣的。模塊所有者應決定導出或開放哪些包。只有模塊本身應該定義這個意圖,所以該意圖不能被其他模塊的行為自動推斷出來。許多框架以類似的方式使用反射,所以在遷移之前進行測試通常是非常重要的。在7.2節(jié)中已經(jīng)講過,在默認情況下,Java9使用--illegal-access=permit運行。那么為什么仍然為了進行反射而顯式地開放包呢?請記住,--illegal-access標志只影響類路徑上的代碼。在本示例中,jackson.databind本身是一個模塊,反映了我們所編寫模塊(不是平臺模塊)中的代碼。而在涉及的類路徑中沒有代碼。8.6開放式模塊在前面的章節(jié)中,學習了如何使用開放式包提供對包的運行時訪問,這可以滿足許多框架和庫的反射需求。如果正在進行大規(guī)模的遷移,那么在一個尚未完全模塊化的代碼庫中,哪些包需要開放可能并不是那么明顯。理想情況下或許可以確切地知道所使用的框架和庫是如何訪問代碼的,但我們可能對正在使用的代碼庫并不熟悉。這樣一來,就會導致一個單調(diào)乏味的試錯過程,試圖找出哪些包需要開放。針對這些情況,可以使用開放式模塊,這是一個不太精確但功能更強大的工具:openmodulebooks{

requiresjackson.databind;

}

開放式模塊提供了對其所有包的運行時訪問,但并不會授予對包的編譯時訪問權(quán)限,而這正是遷移代碼想要的。如果需要在編譯時使用包,則必須將其導出。首先創(chuàng)建一個開放式模塊以避免與反射有關的問題,這樣做有助于首先關注需求(requires)和編譯時使用(exports)。一旦應用程序再次運行,可以通過從模塊中刪除open關鍵字來更好地調(diào)整對包的運行時訪問權(quán)限,同時更具體地說明哪些包應該開放。8.7破壞封裝的VM參數(shù)在某些情況下,向模塊添加exports或者opens并不是一個選項,可能是無法訪問代碼,或者只有在測試期間才需要訪問。在這些情況下,可以使用VM參數(shù)來設置更多的導出。對于平臺模塊,已經(jīng)在7.3節(jié)中看到了這一點,也可以對其他模塊(包括自己的模塊)執(zhí)行相同的操作??梢允褂妹钚袠酥緛韺崿F(xiàn)相同的功能,而不是向books模塊描述符添加exports或opens子句:--add-exportsbooks/demo=jackson.databind

運行應用程序的完整命令如下所示:java-cplib/jackson-annotations-2.8.8.jar:lib/jackson-core-2.8.8.jar\

--module-pathout:mods

--add-exportsbooks/demo=jackson.databind\

-mbooks/demo.Main

上述命令在啟動JVM時設置了一個限制導出??梢允褂靡粋€類似的標志來打開包:--add-opens。雖然這些標志在特殊情況下是有用的,但它們應被視為最后的手段。如前一章所示,可以同樣的機制訪問內(nèi)部的非導出包。雖然在代碼正確遷移之前這可能是一個臨時的解決方法,但應該非常謹慎地使用。不應該輕易地破壞封裝。8.8自動模塊和類路徑在前一章中已經(jīng)看到過未命名模塊,類路徑上的所有代碼都是未命名模塊的一部分。在Jackson示例中已經(jīng)講過,正在編譯的模塊中的代碼不能訪問類路徑上的代碼。那么當jackson.databind自動模塊所依賴的JacksonCore和JacksonAnnotationsJAR仍然在類路徑上時該模塊是如何正常工作的?該模塊能正常工作是因為這些庫位于未命名模塊中。未命名模塊導出類路徑中的所有代碼并讀取所有其他模塊。然而這存在一個很大的限制:未命名模塊本身只能通過自動模塊讀??!圖8-3顯示了當讀取未命名模塊時自動模塊和顯式模塊之間的區(qū)別。顯式模塊只能讀取其他顯式模塊和自動模塊。而自動模塊可讀取所有模塊,包括未命名模塊。圖8-3:只有自動模塊可以讀取類路徑未命名模塊的可讀性只是一種在混合類路徑/模塊路徑遷移方案中有助于自動模塊的機制。如果想要在代碼中直接使用來自JacksonCore的類型(而不是來自自動模塊),那么必須將JacksonCore移動到模塊路徑中。如示例8-3所示。示例8-3:Main.java(chapter8/readability_rules)①從JacksonCore導出Versioned類型。②使用Versioned類型打印庫版本。JacksonDatabind的ObjectMapper類型實現(xiàn)了JacksonCore的Versioned接口。請注意,自在模塊中顯式地使用該類型之前都沒有什么問題。當需要在一個模塊中使用外部類型時,都應該立即考慮requires。接下來通過編譯該代碼來證明這一點,此時將產(chǎn)生一個錯誤:src/books/demo/Main.java:4:error:

packagecom.f

溫馨提示

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

評論

0/150

提交評論