拼图项目:一个不完整的难题
馬克·雷因霍爾德(Mark Reinhold)最近提議延遲Java 9,以花更多的時間來完成Jigsaw項目,這是即將發(fā)布的版本的主要功能。 雖然這個決定肯定會使Java的厄運論者重回舞臺,但我個人感到很放心,并認為這是一個很好且必要的決定。 Java 9功能完成的里程碑當前設(shè)置為12月10日 ,禁止在該日期之后引入新功能 。 但是, 從 Jigsaw項目的早期訪問版本來看,Java的模塊系統(tǒng)似乎尚未為該開發(fā)階段做好準備。
在最新的Java發(fā)布周期中,項目Jigsaw的延遲已成為一種習慣 。 一定不能將其誤解為無能,而只能作為一種指示,說明向Java引入模塊有多困難,而Java目前對于真正的模塊化來說是陌生的。 最初,Java的模塊系統(tǒng)是在2008年提出的,以包含在Java 7中。但是直到今天,拼圖的實現(xiàn)始終比預(yù)期的要困難得多。 在幾次停職甚至是暫時的放棄之后,Java的管理員肯定會承受最終成功的壓力。 很高興看到這種壓力并沒有促使Java團隊急于發(fā)布。
在本文中,我嘗試總結(jié)一下Jigsaw項目的狀態(tài),并在Jigsaw郵件列表中進行了公開討論 。 我寫這篇文章是對當前討論的貢獻,希望有更多的人參與正在進行的開發(fā)過程。 我無意淡化Oracle的辛勤工作。 我在明確聲明這一點是為了避免在隱藏sun.misc.Unsafe之后對拼圖進行了相當激動的討論之后進行的誤解。
模塊化反射
究竟是什么使Jigsaw項目如此困難? 如今,可見性修飾符是封裝類范圍的最接近的近似值。 Package-privacy可以用作其包裝類型的不完美保留。 但是對于跨越多個程序包的內(nèi)部API的更復(fù)雜的應(yīng)用程序,可見性修飾符不足,因此需要真正的模塊。 使用項目Jigsaw,可以真正封裝類,這使它們對于某些代碼不可用,即使這些類被聲明為公共類也是如此。 但是,基于所有類在運行時始終可用的假設(shè)下構(gòu)建的Java程序可能需要進行根本性的更改。
對于最終用戶應(yīng)用程序的開發(fā)人員而言,此更改的根本性可能不如Java庫和框架的維護人員。 庫通常在編譯過程中不知道其用戶代碼。 為了克服此限制,庫可以回退到使用反射。 這樣,用于依賴項注入的容器(例如Spring)可以實例化應(yīng)用程序的bean實例,而框架在編譯時并不知道bean類型。 為了實例化此類對象,容器只需將其工作延遲到運行時,直到它掃描應(yīng)用程序的類路徑并發(fā)現(xiàn)現(xiàn)在可見的bean類型。 對于這些類型中的任何一種,框架都將定位一個構(gòu)造函數(shù),該構(gòu)造函數(shù)在解析所有注入的依賴項后會進行反射性調(diào)用。
一長串Java框架使用與反射配對的運行時發(fā)現(xiàn)。 但是在模塊化環(huán)境中,如果不解決模塊邊界問題,就無法再運行以前的運行時分辨率。 使用項目Jigsaw,Java運行時斷言每個模塊僅訪問在訪問模塊的描述符中聲明為依賴項的模塊。 此外,導(dǎo)入的模塊必須將相關(guān)的類導(dǎo)出到其訪問器。 依賴項注入容器的模塊化版本無法將任何用戶模塊聲明為依賴項,因此禁止進行反射訪問。 實例化未導(dǎo)入的類時,這將導(dǎo)致運行時錯誤。
為了克服此限制,Jigsaw項目添加了一個新的API,該API允許在運行時包含其他模塊依賴性。 使用此API并添加所有用戶模塊后,模塊化的依賴項注入容器現(xiàn)在可以繼續(xù)實例化在編譯時不知道的bean類型。
但是這個新的API真的可以解決問題嗎? 從純功能的角度來看,此附加API允許遷移庫以保留其功能,即使將其重新包裝為模塊也是如此。 但是不幸的是,模塊邊界的運行時強制要求在使用大多數(shù)反射代碼之前先進行儀式舞蹈。 在調(diào)用方法之前,調(diào)用者需要始終確保相應(yīng)的模塊已經(jīng)是調(diào)用者的依賴項。 如果框架忘記添加此檢查,則會在編譯期間拋出運行時錯誤而沒有發(fā)現(xiàn)任何機會。
由于許多庫和框架都過度使用了反射,因此可訪問性的這種變化不太可能改善運行時封裝。 即使安全管理器會限制框架添加運行時模塊依賴項,強制執(zhí)行此類邊界也可能會破壞大多數(shù)現(xiàn)有應(yīng)用程序。 更現(xiàn)實的是,大多數(shù)違反模塊邊界的行為并不表示真正的錯誤,而是由代碼遷移不當引起的。 同時,如果大多數(shù)框架搶占了對大多數(shù)用戶模塊的訪問權(quán)限,則運行時限制不可能改善封裝。
當模塊在其自身類型上使用反射時,此要求當然不適用,但是在實踐中這種反射的使用非常少見,可以通過使用多態(tài)來代替。 在我眼中,在使用反射時強制執(zhí)行模塊邊界與它的主要用例相矛盾,并使本來就不容易的反射API更加難以使用。
模塊化資源
除了這個限制,目前還不清楚依賴注入容器將如何發(fā)現(xiàn)它應(yīng)該實例化的類。 在非模塊化應(yīng)用程序中,框架可以例如期望給定名稱的文件存在于類路徑上。 然后,此文件用作描述如何發(fā)現(xiàn)用戶代碼的入口點。 通常通過從類加載器請求命名資源來獲取此文件。 對于項目Jigsaw,當所需的資源也封裝在模塊的邊界內(nèi)時,這將不再可能。 據(jù)我所知,資源封裝的最終狀態(tài)尚未完全確定。 但是,當嘗試當前的早期訪問版本時,將無法訪問外部模塊的資源。
當然,拼圖項目的當前草案中也解決了這個問題。 為了克服模塊邊界,已授予Java 預(yù)先存在的ServiceLoader類超能力。 為了使特定的類可用于其他模塊,模塊描述符提供了一種特殊的語法,該語法允許通過模塊邊界泄漏某些類。 應(yīng)用此語法,框架模塊聲明它提供了某種服務(wù)。 然后,用戶庫聲明框架可以訪問該服務(wù)的實現(xiàn)。 在運行時,框架模塊使用服務(wù)加載器API查找其服務(wù)的任何實現(xiàn)。 這可以用作在運行時發(fā)現(xiàn)其他模塊的方式,并且可以替代資源發(fā)現(xiàn)。
乍看之下,這種解決方案似乎很優(yōu)雅,但我仍然對此提議表示懷疑。 服務(wù)加載器API的使用非常簡單,但同時,其功能也非常有限。 此外,很少有人對其自己的代碼進行修改,這可以視為其范圍有限的指標。 不幸的是,只有時間才能證明此API是否能夠充分容納所有用例。 同時,可以將單個Java類與Java運行時緊密地聯(lián)系在一起,從而幾乎不可能棄用和替換服務(wù)加載程序API。 在Java的歷史背景下,已經(jīng)講述了許多看起來不錯但又變味的想法,我發(fā)現(xiàn)創(chuàng)建這樣一個神奇的中心很容易成為現(xiàn)實,它很容易成為實現(xiàn)瓶頸。
最后,還不清楚如何在模塊化應(yīng)用程序中公開資源。 盡管Jigsaw不會破壞任何二進制兼容性,但從以前總是返回值的ClassLoader::getResource調(diào)用返回null可能只是將應(yīng)用程序埋在成堆的null指針異常下。 例如,代碼操縱工具需要一種方法來定位類文件,這些類文件現(xiàn)在已經(jīng)封裝起來,這至少會阻礙它們的采用過程。
可選依賴項
服務(wù)加載器API無法容納的另一個用例是可選依賴項的聲明。 在許多情況下,可選的依賴項不被認為是一種好習慣,但實際上,如果可以將依賴項組合成大量排列,它們提供了一種便捷的方法。
例如,如果特定的依賴項可用,則庫可能能夠提供更好的性能。 否則,它將退回到另一個不太理想的選擇。 為了使用可選的依賴關(guān)系,需要庫根據(jù)其特定的API進行編譯。 但是,如果此API在運行時不可用,則庫需要確保永不執(zhí)行可選代碼,并退回到可用的默認值。 這樣的可選依賴關(guān)系無法在模塊化環(huán)境中表達,在模塊化環(huán)境中,即使從未使用過依賴關(guān)系,在應(yīng)用程序啟動時都會驗證任何聲明的模塊依賴關(guān)系。
可選依賴項的一個特殊用例是可選注釋包。 今天,Java運行時將注釋視為可選的元數(shù)據(jù)。 這意味著,如果類加載器無法定位注釋的類型,則Java運行時將僅忽略所關(guān)注的注釋,而不是拋出NoClassDefFoundError 。 例如, FindBugs應(yīng)用程序提供了一個注釋包,用于在用戶發(fā)現(xiàn)相關(guān)代碼為假陽性后抑制潛在的錯誤。 在應(yīng)用程序的常規(guī)運行時期間,不需要特定于FindBugs的注釋,因此它們不包含在應(yīng)用程序包中。 但是,在運行FindBugs時,該實用程序會顯式添加注釋包,以使注釋變?yōu)榭梢姟?在拼圖項目中,這不再可能。 僅當模塊聲明對注釋包的依賴性時,注釋類型才可用。 如果以后在運行時缺少此依賴項,則盡管注釋不相關(guān),也會引發(fā)錯誤。
非模塊化
當然,不將框架捆綁為Java 9中的模塊是避免所有討論的限制的最簡單方法。 Java運行時將任何未模塊化的jar文件視為類加載器的所謂未命名模塊的一部分 。 這個未命名的模塊定義了對正在運行的應(yīng)用程序中存在的所有其他模塊的隱式依賴性,并將其所有程序包導(dǎo)出到任何其他模塊。 當混合模塊化和非模塊化代碼時,這可以作為備用。 由于未命名模塊的隱式導(dǎo)入和導(dǎo)出,所有未遷移的代碼應(yīng)繼續(xù)像以前一樣運行。
盡管這種選擇退出可能是沉重反射框架的最佳解決方案,但是緩慢采用項目Jigsaw的確也違反了模塊系統(tǒng)的目的。 由于時間的缺乏是大多數(shù)開源項目的主要限制,因此很可能會出現(xiàn)這種結(jié)果。 此外,許多開源開發(fā)人員都必須將其庫編譯為舊版Java。 由于模塊化和非模塊化代碼的運行時行為不同,因此框架需要維護兩個分支,以便能夠使用Java 9 API遍歷模塊化捆綁包中的模塊邊界。 許多開源開發(fā)人員不太可能花時間來使用這種混合解決方案。
代碼檢測
在Java中,反射方法訪問不是庫與未知用戶代碼進行交互的唯一方法。 使用工具API ,可以重新定義類以包含其他方法調(diào)用。 例如,這通常用于實現(xiàn)方法級別的安全性或收集代碼指標。
在檢測代碼時,通常會在類加載器加載Java類的類文件之前立即對其進行更改。 由于通常在緊接類加載之前應(yīng)用類轉(zhuǎn)換,因此當前無法預(yù)先更改模塊圖,因為未加載的類的模塊是未知的。 如果檢測代碼在首次使用之前無法訪問已加載的類,則可能會導(dǎo)致無法解決的沖突無法解決。
摘要
軟件估算很困難,我們所有人都傾向于低估應(yīng)用程序的復(fù)雜性。 Jigsaw項目對Java應(yīng)用程序的運行時行為進行了根本性的更改,將發(fā)布推遲到徹底評估所有可能性是非常合理的。 當前,有太多未解決的問題,這是延遲發(fā)布日期的不錯選擇。
我希望模塊邊界根本不由運行時強制執(zhí)行,而是保留為編譯器構(gòu)造。 盡管存在一些缺陷,Java平臺已經(jīng)實現(xiàn)了泛型類型的編譯時擦除,并且該解決方案運行良好。 如果沒有運行時強制實施,則在JVM上采用動態(tài)語言的模塊也將是可選的,因為與Java中相同的模塊化形式可能沒有意義。 最后,我覺得當前嚴格的運行時封裝形式試圖解決一個不存在的問題。 在使用Java多年之后,我很少遇到無意間使用內(nèi)部API引起嚴重問題的情況。 相比之下,我記得在很多情況下濫用本應(yīng)為私有的API可以解決我無法解決的問題。 同時,Jigsaw仍無法解決Java中缺少模塊的其他癥狀(通常稱為jar hell) ,Jigsaw不能區(qū)分模塊的不同版本。
最后,我認為向后兼容性的適用范圍超出了二進制級別。 實際上,二進制不兼容通常比行為更改更容易處理。 在這種情況下,Java多年來成就斐然。 因此,方法合同應(yīng)與二進制兼容性一樣受尊重。 盡管項目Jigsaw從技術(shù)上講不會通過提供未命名的模塊來破壞方法契約,但是模塊化基于其綁定對代碼行為進行了微妙的更改。 我認為,這將使經(jīng)驗豐富的Java開發(fā)人員和新手都感到困惑,并導(dǎo)致重新出現(xiàn)運行時錯誤。
這就是為什么我發(fā)現(xiàn)強制運行時模塊邊界的價格與其提供的好處相比過高的原因。 OSGi是一個具有版本控制功能的運行時模塊系統(tǒng),適用于確實需要模塊化的用戶。 作為一個很大的好處,OSGi在虛擬機之上實現(xiàn),因此不會影響VM行為。 另外,我認為Jigsaw可以為庫提供一種規(guī)范的方式,使其在有道理的情況下選擇退出運行時約束,例如對于大量反射的庫。
翻譯自: https://www.javacodegeeks.com/2015/12/project-jigsaw-incomplete-puzzle.html
總結(jié)
以上是生活随笔為你收集整理的拼图项目:一个不完整的难题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: (ftp linux 查看)
- 下一篇: lcs文本相似度_具有LCS方法的通用文