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

下載本文檔

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

文檔簡介

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

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

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

默認(rèn)行為。允許對(duì)封裝類型進(jìn)行非法訪問。當(dāng)?shù)谝淮螄L試通過反射進(jìn)行非法訪問時(shí)會(huì)生成一個(gè)警告。--illegal-access=warn

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

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

不允許非法的訪問嘗試。這將是未來的默認(rèn)行為。請(qǐng)注意,沒有允許取消打印的警告的設(shè)置。這是由設(shè)計(jì)所決定的。在本章中,將學(xué)習(xí)如何解決潛在的問題,以消除非法訪問警告。由于--illegal-access=deny將是未來的默認(rèn)設(shè)置,因此下一步目標(biāo)是使用此設(shè)置運(yùn)行應(yīng)用程序。如果使用--illegal-access=deny運(yùn)行使用javassist的代碼,那么應(yīng)用程序?qū)o法運(yùn)行,并且會(huì)看到以下錯(cuò)誤: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

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

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

isnotvisible

importsun.security.x509.X500Name;

^

(packagesun.security.x509isdeclaredinmodulejava.base,whichdoesnot

exportittotheunnamedmodule)

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

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

encapsulated/EncapsulatedTypes.java

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

\encapsulated.EncapsulatedTypes

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

參數(shù)文件必須包含所有必要的命令行標(biāo)志。文件中的每一行都包含一個(gè)選項(xiàng)。例如,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)沒有直接關(guān)系,但仍值得一提。其中一個(gè)在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上無法編譯或運(yùn)行。當(dāng)嘗試編譯時(shí),會(huì)看到以下所示錯(cuò)誤: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上運(yùn)行,也會(huì)失敗:Exceptioninthread"main"java.lang.NoClassDefFoundError:sun/misc/BASE64Decoder

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

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

...

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

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

removedinafuturerelease

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

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

}

請(qǐng)注意,該模塊還沒有任何requires語句。這非常可疑,因?yàn)轱@而易見其需要依賴于jackson-databind-2.8.8.jar文件中的類。因?yàn)橐呀?jīng)有了一個(gè)真正的模塊,所以可以使用--module-source-path標(biāo)志來編譯代碼。Jackson庫不是模塊,所以暫時(shí)留在類路徑上: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

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

requiresjackson.databind;

}

books模塊請(qǐng)求jackson.databind,就好像它是一個(gè)正常的模塊。但模塊名稱來自哪里呢?自動(dòng)模塊的名稱可以在META-INF/MANIFEST.MF文件的新引入的Automatic-Module-Name字段中指定。這樣一來,即使在將庫完全遷移到模塊之前,庫維護(hù)人員也可以選擇模塊名稱。有關(guān)以這種方式命名模塊的更多詳細(xì)信息,請(qǐng)參閱10.2節(jié)。如果沒有指定名稱,則模塊名稱是從JAR的文件名派生的。命名算法大致如下:·使用點(diǎn)(.)替換破折號(hào)(-)?!ず雎园姹咎?hào)。在Jackson示例中,模塊名稱是基于文件名稱的?,F(xiàn)在,可以通過運(yù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文件從類路徑中移除,并配置了一個(gè)模塊路徑,指向mods目錄。圖8-2提供了所有代碼的概述。圖8-2:模塊路徑上的非模塊化JAR變成了自動(dòng)模塊。而類路徑變成了未命名模塊為了運(yùn)行程序,還需要更新Java調(diào)用:java-cp$CP--module-pathmods:out-mbooks/demo.Main對(duì)Java命令完成如下修改:·將out目錄移至模塊路徑中?!ackson-databind-2.8.8.jar文件從類路徑(lib)移至模塊路徑(mods)中?!ねㄟ^使用-m標(biāo)志來指定模塊,從而啟動(dòng)應(yīng)用程序??梢詫⑺蠮AR移動(dòng)到模塊路徑,而不僅僅是移動(dòng)(jackson-databind)。雖然這樣做使得移動(dòng)過程變得容易一些,但是很難看到究竟發(fā)生了什么。當(dāng)遷移自己的應(yīng)用程序時(shí),可以隨意將所有JAR移動(dòng)到模塊路徑?,F(xiàn)在已經(jīng)很接近第一個(gè)遷移的應(yīng)用程序了,但不幸的是在啟動(dòng)應(yīng)用程序時(shí)仍然遇到了異常:Exceptioninthread"main"java.lang.reflect.InaccessibleObjectException:

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

modulebooksdoesnot"exportsdemo"tomodulejackson.databind

...

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

requiresjackson.databind;

exportsdemotojackson.databind;

}

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

requiresjackson.databind;

opensdemo;

}

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

requiresjackson.databind;

opensdemotojackson.databind;

}

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

requiresjackson.databind;

}

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

運(yùn)行應(yīng)用程序的完整命令如下所示: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

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

packagecom.f

溫馨提示

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

最新文檔

評(píng)論

0/150

提交評(píng)論