自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

淺析Java類隔離規(guī)避依賴沖突的實現(xiàn)原理

開發(fā) 前端
Java類隔離容器的思路是在Java語言既有特性的基礎(chǔ)上,利用類加載劫持、類加載器編排實現(xiàn)了一套多版本類并存的機制,確實可以減少某些場景下的類版本沖突的問題。但是它解決了一些問題,但是同樣的也帶來了新的問題。

一、導(dǎo)語

隨著業(yè)務(wù)規(guī)模增長、業(yè)務(wù)邏輯演進(jìn),項目工程的依賴樹(二方依賴、三方依賴)變得愈發(fā)復(fù)雜。隨之而來的便是【依賴沖突】問題。

當(dāng)幾個軟件包對相同的共享包或庫有依賴性,但它們依賴于不同的、不兼容的共享包版本時,就會出現(xiàn)依賴性問題。如果共享包或庫只能安裝一個版本,用戶可能需要通過獲得較新或較舊版本的依賴包來解決這個問題。反過來,這可能會破壞其他的依賴關(guān)系。

【依賴沖突】問題是軟件工程廣泛存在的問題,換句話說,各語言生態(tài)如Python、Golang、Nodejs、Java等都存在類似問題。但是由于Java語言的特殊機制,【依賴沖突】問題在Java中似乎有完美的解決方案,那就是【類隔離容器】。

從2000年的開源規(guī)范OSGI,到阿里巴巴自研Pandora容器,再到螞蟻金服開源sofa-ark,業(yè)界在【類隔離容器】這個領(lǐng)域的實踐方興未艾。那到底什么是類隔離容器?怎么實現(xiàn)類隔離容器?為什么它聽起來很完美但是卻沒有成為主流實踐?

本文代碼均為示意的偽代碼。

二、類隔離容器

當(dāng)項目依賴樹變得復(fù)雜時,不可避免的會出現(xiàn)不同的組件依賴同一個組件的不同版本的問題。如下圖,3個組件分別依賴了 maven-settings 組件的2個版本:3.0、3.3.9;plexus-interpolation組件同理。

圖片圖片

圖片圖片

當(dāng)項目中只有一個依賴空間時,項目需求的多個版本的組件最終只會有一個版本進(jìn)入項目依賴空間,極易因為上層組件對版本需求的眾口難調(diào)而出現(xiàn)ClassNotFoundException、NoSuchMethodException等版本兼容性問題。

為解決這個問題,業(yè)界開始考慮通過Java類加載隔離來在項目運行時創(chuàng)建多個隔離的依賴空間。每個依賴空間中可以各自使用相同組件的不同版本,這種隔離的依賴空間即為:類隔離容器。

如下圖,項目中存在3個類隔離容器,maven-settings組件在兩個容器中分別存在3個版本。

圖片圖片

這里的maven-*只是Jar包名稱,和mvn工具無關(guān),只是筆者手上恰好有這個案例。

類隔離容器劫持、干預(yù)了Java類加載流程,讓同一個組件的多個版本可以在同一個項目中并存。

三、類加載API

Java是一種強類型的動態(tài)語言,其代碼符號(類名、方法名、字段名)都在運行時動態(tài)鏈接,通過【類加載器】來實現(xiàn)運行時的類搜索和代碼裝載。這種動態(tài)特性賦予了框架開發(fā)者極大的便利性,支撐了大量企業(yè)級開發(fā)框架的實現(xiàn),提高了上層業(yè)務(wù)代碼的迭代效率。這也是Java語言二十幾年如一日占據(jù)編程語言排行榜前列的一個重要原因。

圖片圖片

TIOBE編程社區(qū)指數(shù)-2024(https://www.tiobe.com/tiobe-index/)

為支撐上述類加載能力,同時賦予開發(fā)者自定義類加載流程的能力,Java Runtime定義了ClassLoader這一API。抽象的API如下:

圖片圖片

ClassLoader的實現(xiàn)者負(fù)責(zé)根據(jù)【位置無關(guān)】的類標(biāo)識,定位、裝載類。所謂【位置無關(guān)】說的是,JVM不關(guān)心這個類文件的物理位置是在網(wǎng)絡(luò)上、磁盤里、內(nèi)存里。

由于Classs類型的返回值無法由開發(fā)者自行構(gòu)造,涉及JVM內(nèi)部的狀態(tài)聯(lián)動,因此JVM會暴露一個構(gòu)造Class對象的工具API。抽象的API如下:

圖片圖片

該parseAndLinkClass方法由JVM實現(xiàn),JVM內(nèi)部會進(jìn)行我們八股文都背過的類驗證、類解析、類初始化等標(biāo)準(zhǔn)動作。

因此,開發(fā)者自定義類加載流程的樣板代碼如下:

圖片圖片

Java提供了類似上述樣板代碼的具體實現(xiàn),即:java.lang.ClassLoader,其實就是大家都熟悉的【模板方法設(shè)計模式】

上述通俗的、抽象的API能力,映射到Java的具體實現(xiàn)分別為:

裝載類

圖片圖片

java.lang.ClassLoader#loadClass(java.lang.String)

定義類

圖片

java.lang.ClassLoader#defineClass0

JNI方法實現(xiàn)

圖片圖片

jdk/src/share/native/java/lang/ClassLoader.c

四、類的相等性:

ClassCastException

盡管在源代碼層面,我們用【類的全限定名】作為編碼時定位類的標(biāo)識,但是在JVM內(nèi)部,類的標(biāo)識是一個聯(lián)合索引。

JVM內(nèi)部使用 <ClassLoader,className> 二元組來索引、標(biāo)識一個類。通俗來說就是,兩個不同的 ClassLoader使用相同的類名和字節(jié)碼 defineClass得到的是兩個不同的Class對象。

通俗的偽代碼來表達(dá)的話,上述ClassLoadUtil#parseAndLinkClass方法的實現(xiàn)如下:

圖片圖片

defineClass時,創(chuàng)建的Class對象上會關(guān)聯(lián)Loader。

具體到Java中的java.lang.Class類,我們可以看到如下字段:

圖片圖片

java.lang.Class#classLoader

上述類加載特性,在復(fù)雜的類加載邏輯下如果沒有處理好的話極易產(chǎn)生類型轉(zhuǎn)換異常:ClassCastException。如下示例:

圖片圖片

圖片圖片

圖片圖片

如果Type類同時被兩個類加載器加載在JVM內(nèi)部產(chǎn)生了Type_1、Type_2兩個版本的類型(Class對象)。

LoadTest類中的Type符號鏈接到了Type_1。

TypeUtil類中的Type符號鏈接到了Type_2。

那么,當(dāng)LoadTest.main方法執(zhí)行時即會產(chǎn)生ClassCastException異常。

因為TypeUtil.newType方法返回的Type_2類型的對象,和LoadTest.mian方法中聲明的Type_1類型的typeVar變量的類型不兼容,無法進(jìn)行隱式的類型轉(zhuǎn)換。

類隔離容器的需求天然需要同名類存在多個版本,因此類隔離容器的實現(xiàn)和使用時需要極小心的設(shè)計、處理該問題。這種問題排查起來非常費勁。

五、類加載編排、委托

綜上分析,我們發(fā)現(xiàn)在Java層面實現(xiàn)load一個類并不復(fù)雜,只需要根據(jù)類名拿到二進(jìn)制的字節(jié)碼,然后調(diào)用JVM提供的工具方法就行了。

到這里,事情已經(jīng)回到我們最熟悉不過的CRUD主場,我們可以用各種我們熟悉的設(shè)計模式來實現(xiàn)特定的類加載業(yè)務(wù)需求,其中最重要的設(shè)計模式即為:委托模式。

第一個業(yè)務(wù)需求是類加載的安全性。Java標(biāo)準(zhǔn)庫自帶了大量易用的工具和數(shù)據(jù)結(jié)構(gòu),這部分代碼的物理位置和業(yè)務(wù)代碼不在一起。為避免項目中的惡意代碼使用標(biāo)準(zhǔn)庫同名的類來干壞事,我們需要實現(xiàn)類加載優(yōu)先級,即加載一個類時優(yōu)先從JRE目錄加載,JRE目錄中加載不到時再從項目中加載。

第二個業(yè)務(wù)需求是類的復(fù)用。如Tomcat場景,一個Tomcat進(jìn)程可以托管多個Web服務(wù)(war包)。每個Web服務(wù)自身的業(yè)務(wù)代碼和依賴是不同的,但是各個Web服務(wù)依賴的Servlet API、Tomcat API是相同的,因為這是Tomcat容器提供的公共的Runtime??紤]到上述【類的相等性】,我們希望這些Runtime類只有一個版本,以避免訪問Runtime API時出現(xiàn)ClassCastException。

那么我們重新實現(xiàn)上述AbstractClassLoader如下:

圖片圖片

繼而我們可以基于上述模板類,構(gòu)造、編排我們的自定義類加載邏輯:

圖片圖片

上述代碼通過編排類加載器,實現(xiàn)了如下項目依賴空間拓?fù)洌?/p>

圖片圖片

綜上,我們在Java Runtime的基礎(chǔ)ClassLoader機制上,通過非常熟悉的業(yè)務(wù)編排實現(xiàn)了類加載的安全性需求、共享復(fù)用需求,最終呈現(xiàn)了一個樹形的類加載器拓?fù)洹?/p>

不同的類加載需求需要編排出不同的類加載器拓?fù)?,比如我們討論的【類隔離容器】需求,需要編排出更復(fù)雜的類加載器拓?fù)?。但是其核心的編排思路都是相似的~

六、類加載劫持

到這里,我們已經(jīng)有足夠的技術(shù)儲備來根據(jù)業(yè)務(wù)需求編排類加載器拓?fù)湟赃_(dá)成目的。但是遺留了一個關(guān)鍵的問題:

怎么樣才能讓Java Runtime在加載、鏈接代碼符號時,使用我們構(gòu)造出來的自定義類加載器呢?

因為如果我們構(gòu)造出來的類加載器不能參與到類加載流程,那其實就是一個普通的Java對象,沒啥用。

要解決這個問題,我們需要參考JVM規(guī)范明確類加載器會被如何獲取和使用,因為類加載器本質(zhì)上是供Java Runtime使用的SPI。

JVM規(guī)范對這一塊的闡述是嚴(yán)謹(jǐn)?shù)橄蟮?,但通俗來說就一個原則:如果一個類C1是由CL加載器加載(defineClass)的,那么,C1觸發(fā)的的其他類如Cn的加載和鏈接,也會委托給CL。示例如下:

圖片圖片

圖片圖片

因為app1.Main類是由app1Loader加載,那么app1.Main依賴的App1Service類也會隱式的交給app1Loader加載。這個過程是JVM在解析、鏈接app1.Main類的時候自動進(jìn)行的。

也就是說,當(dāng)我們指定某個類加載器CL加載項目的EntryPoint并執(zhí)行后,后續(xù)觸發(fā)的類加載動作都會交給指定的類加載器CL或者CL委托的其他類加載器。Java項目中的EntryPoint往往是項目中的main方法。

這里有點繞。換句話說,某個Class類對象C1依賴的其他類的加載都會交給C1.classLoader來進(jìn)行。注意,上面【類的相似性】一節(jié)說過,每個Class對象上都持有加載它的ClassLoader的引用。

那么,想讓3個WebApp在各自類空間中運行的方式就很簡單了:

圖片圖片

上述流程還遺留一個問題,那就是ServiceLoader場景。

為打破呆板的雙親委派機制實現(xiàn)某種意義上的IOC,Java提供了contextClassLoader機制。contextClassLoader關(guān)聯(lián)在Thread對象上,并且會在父子線程中復(fù)制、傳播。

java.lang.Thread#getContextClassLoader

為了讓上述3個WebApp中正常使用ServiceLoaderAPI或類似的SPI框架,我們需要做如下特別處理:

至此,我們就實現(xiàn)了Tomcat場景下的類加載劫持、類隔離、類共享。

到這里,我們已經(jīng)掌握了實現(xiàn)類隔離容器的核心基礎(chǔ)。

總結(jié)來說,只要我們能在應(yīng)用的EntryPoint(main方法)中合理的介入、干預(yù),就能實現(xiàn)靈活的類加載業(yè)務(wù)。

七、類隔離模塊:Bundle

回到最開始的需求,我們希望可以在項目中達(dá)成如下依賴結(jié)構(gòu):

圖片

為了實現(xiàn)版本隔離、共存,上述mave-core、maven-compat、maven-xxx組件會將其依賴的maven-settings Jar文件按特定布局打包到自身的jar包中,形成各自獨立的依賴空間,供運行時提取、加載。

OSGI中將上述隔離的依賴空間或者類隔離容器稱為bundle。需要進(jìn)行類隔離的組件按bundle文件布局來交付自己的代碼和依賴。

圖片圖片

每個bundle是一個FatJar,通俗來說是一個包含自身依賴的Jar文件的Jar文件。類似如下Jar文件:

圖片圖片

可以把上述dubbo-demo jar文件想象成我們熟悉的mybatis框架。該模塊把mybaits框架自身的代碼和它依賴的三方包按設(shè)計的布局打包到同一個Jar中。

我們知道,Java自帶的URLClassLoader天然支持從Jar文件中搜索、讀取class文件,但是不支持上述嵌套Jar。

解決這個問題有兩個方案:

解壓FatJar

在進(jìn)程啟動時,類隔離容器底座識別出ClassPatch中存在上述類型的bundle Jar后,提前將上述FatJar解壓到本地磁盤。后續(xù)就簡單了,無非就是在指定目錄搜索類和Jar。

文件切片

我們知道,Jar文件本質(zhì)上就是ZIP格式的文件,而ZIP文件的邏輯結(jié)構(gòu)是一個Map。

圖片圖片

如上圖,test.jar中有兩個文件,一個是a.b.C.class文件,另一個是dep.jar。該文件在磁盤上的抽象布局如下:

圖片圖片

ZIP文件除文件元數(shù)據(jù)外,整體分為兩部分。

  1. 數(shù)據(jù)區(qū):存放文件的內(nèi)容。
  2. 索引區(qū):存放文件名稱和文件內(nèi)容的偏移量和長度。

因此,在技術(shù)上我們可以對FatJar文件做切片。即在不解壓FatJar的前提下,將其中一個區(qū)間當(dāng)成jar文件讀取。如上圖,我們解析test.jar索引區(qū)得到dep.jar文件的長度為4000字節(jié),在外層jar文件的1000偏移處,那么我們讀取它內(nèi)部嵌套的Jar文件的偽代碼如下:

圖片圖片

這一塊說來話長,全是花活。spring-boot就是使用類似方式來拍平嵌套的Jar文件。

可以參考相關(guān)資料:

【SpringBoot】服務(wù) Jar 包的啟動過程原理(https://www.cnblogs.com/kukuxjx/p/18207068)

八、類導(dǎo)入/導(dǎo)出:Bundle元信息

到這里,我們可以初步勾勒類隔離容器的代碼藍(lán)圖了。

圖片圖片

  1. 【底座】需要在執(zhí)行流進(jìn)入業(yè)務(wù)main方法前,提前執(zhí)行。
  2. 【底座】掃描項目中的依賴,區(qū)分Jar依賴和Bundle依賴。
  3. 【底座】為每個Bundle依賴創(chuàng)建獨立的Bundle類加載器(N個)。
  4. 【底座】為Bundle以外的業(yè)務(wù)代碼和普通Jar創(chuàng)建類加載器(1個)。
  5. 【底座】將上述N+1個類加載器狀態(tài)編排到一起,拼湊成完整的依賴視圖。
  6. 【底座】初始化當(dāng)前線程contextClassLoader。
  7. 【底座】使用業(yè)務(wù)類加載器搜索、加載main方法所在類(EntryPoint)。
  8. 【底座】調(diào)用業(yè)務(wù)代碼的main方法。

這里存在兩個問題:

  • 【底座】怎么區(qū)分ClassPath下的Jar文件是普通Jar還是Bundle Jar?這個一般是通過在打包時向Bundle Jar中注入特征文件來實現(xiàn)。比如sofa-ark在打包Bundle Jar時,會在Jar中注入如下路徑固定的標(biāo)記文件:com/alipay/sofa/ark/plugin/mark

圖片圖片

com.alipay.sofa.ark.spi.constant.Constants#ARK_PLUGIN_MARK_ENTRY

圖片圖片

  • 項目中有那么多類加載器(N+1),當(dāng)我們加載一個類時,到底應(yīng)該由哪個類加載加載呢?這部分信息是控制bundle正常工作的元信息,需要每個bundle的維護(hù)者提供給【底座】讀取、使用。即,每個bundle中必須要提供這個Bundle導(dǎo)出的類、導(dǎo)入的類。這類元信息一般會在bundle jar的Manifest文件提供,下圖是一個OSGI規(guī)范下,bundle jar中Manifest文件提供的類導(dǎo)入/導(dǎo)出信息。

圖片圖片

上述信息表明:

  • 該bundle jar向外暴露com.sample.myservice.api,即該包下的類由這個bundle類加載來加載。
  • 該bundle jar依賴了org.apache.commons.logging,需要由其他類加載器來加載、提供。

是不是有點像 JDK9 的新特性:模塊化?

除此之外,一般還會提供優(yōu)先級等其他用于控制類加載過程的元信息,畢竟可能有多個bundle暴露相同的類。相關(guān)的細(xì)節(jié)信息很多,在各個具體的實現(xiàn)(開源的OSGI、阿里巴巴的Pandora、螞蟻金服的sofa-ark)上可能有差異,但是大同小異。

九、Bundle依賴隔離

到這里,我們終于可以看下怎么實現(xiàn)一個類隔離容器了。

業(yè)務(wù)類加載器

圖片圖片

Bundle類加載器

圖片圖片

類加載器管理器

圖片圖片

類隔離容器底座

圖片圖片

圖片圖片

使用姿勢

圖片圖片

上述代碼僅為理論示意,并不能直接運行,讀者會意即可。

以上,我們就實現(xiàn)了一個簡單的類隔離容器,最終形成的類加載器拓?fù)淙缦拢?/p>

圖片圖片

最終實現(xiàn)了每個Bundle優(yōu)先使用自身內(nèi)部嵌入的Jar依賴,從而實現(xiàn)每個Bundle Jar有一個獨立的依賴空間,避免了依賴沖突。

十、沒有銀彈

當(dāng)bundle jar中的嵌套依賴不向外逃逸時,一切都工作的很好。但是如果嵌套依賴中的API被跨bundle耦合、交互,那事情就變得棘手起來。

考慮如下的場景:

圖片圖片

  • Bundle BBB中導(dǎo)出了如下Service:

圖片圖片

  • Bundle AAA中導(dǎo)出了如下Service:

圖片圖片

  • AAAService依賴了lang3-1.0中的Pair類,BBBService依賴了lang3-2.0中的Pair類。那么請問,AAAService這個類,應(yīng)該使用哪個版本的lang3?1.0還是2.0?

使用1.0版本:action2方法可以正常工作,因為action2方法就是在1.0版本下編寫、編譯的。但是這個這樣action1方法又無法正常工作了。因為action1方法中調(diào)用的BBBService的action方法預(yù)期的參數(shù)類型是2.0版本的Pair類。

使用2.0版本:那還是上面同樣的道理。

如果AAAService使用1.0版本的Pair類,BBBService使用2.0版本的Pair類,那么又會出現(xiàn)我們上面著重強調(diào)過的【類的相等性】問題,一定會產(chǎn)生ClassCastException。

十一

總結(jié)

剛進(jìn)入一個陌生領(lǐng)域就陷入代碼細(xì)節(jié)并不是一個高效的方式,所以本文中筆者盡可能的使用偽代碼、示意代碼來進(jìn)行論述。

一路梳理下來,我們最終通過類加載器編排,實現(xiàn)了一個理論上的類隔離容器。盡管沒有具體的代碼實現(xiàn),但是相信看到這里,讀者們已經(jīng)對類隔離機制有了一個較為系統(tǒng)的認(rèn)識。

總的來說,Java類隔離容器的思路是在Java語言既有特性的基礎(chǔ)上,利用類加載劫持、類加載器編排實現(xiàn)了一套多版本類并存的機制,確實可以減少某些場景下的類版本沖突的問題。但是它解決了一些問題,但是同樣的也帶來了新的問題。

  1. 排障心智:類隔離機制構(gòu)造了一個復(fù)雜的類加載器拓?fù)洌?dāng)因為cornor case出現(xiàn)了類加載異常時,bundle組件的使用者是一臉懵逼的。本來遇到類似ClassNotFoundException、NoSuchMethodException問題時,組件使用者可以根據(jù)項目依賴樹所見即所得的按沉淀的經(jīng)驗排查、處置。但是當(dāng)你用【嵌套Jar+編排加載機制】交付組件后,之前沉淀的相關(guān)排障心智都沒用了。
  2. 遷移成本:在組織從0到1起步階段介入進(jìn)行上述改造是合適的、成本極低的,但是沒人能顧得上這個。在組織從80到100的階段發(fā)現(xiàn)類隔離機制能解決一些問題,但是這個時期各個業(yè)務(wù)項目的代碼結(jié)構(gòu)、組件版本、組件使用姿勢百花齊放。想要技改、收斂到bundle jar模式,成本比較大且客觀上存在一個研發(fā)效率、業(yè)務(wù)穩(wěn)定性的陣痛期。
  3. 元信息維護(hù):如上梳理,bundle jar交付時,bundle維護(hù)者需要梳理其導(dǎo)出的類、導(dǎo)入的類。這個只能人肉梳理,可能會漏、可能會錯;且因為多個bundle在運行期的化學(xué)反應(yīng),漏、錯的異常表現(xiàn)很不直觀,難以診斷、排查。如果各個bundle維護(hù)者都在一個部門下那溝通、處理起來還好,如果是跨部門的多個bundle互相打架,事情就比較麻煩。

筆者以為,類隔離機制的高價值場景應(yīng)該是特定領(lǐng)域內(nèi)部使用的JVM租戶。由于JVM比較吃資源,某些輕量邏輯(FAAS)如果單獨啟動一個進(jìn)程來執(zhí)行,有點類似于用集裝箱運一只籃球,性價比很低,那干脆大家一起眾籌拼集裝箱得了。

圖片

又回到了十年前用Tomcat托管多個WebApp的模式...

如上圖,在JVM進(jìn)程上構(gòu)建一個應(yīng)用引擎,可以根據(jù)JVM資源情況動態(tài)的將包含代碼和依賴的bundle jar調(diào)度到JVM上運行。JVM租戶的主要問題是資源隔離性不夠,比如CPU、MEM和IO。但是如果這個平臺只是內(nèi)部特定場景下、特定開發(fā)人員使用問題倒也不大。

以上均為筆者一家之言,歡迎指正~

參考

  • 微服務(wù)的災(zāi)難-依賴地獄(https://xargin.com/disaster-of-microservice-dephell/)

  • 如何打包 Ark Plugin(https://www.sofastack.tech/projects/sofa-boot/sofa-ark-ark-plugin-demo/)

  • OSGi 捆綁軟件清單文件(https://www.ibm.com/docs/zh/was-zos/9.0.5?topic=files-example-osgi-bundle-manifest-file)
責(zé)任編輯:武曉燕 來源: 得物技術(shù)
相關(guān)推薦

2020-12-30 08:01:07

Java隔離加載

2023-12-18 09:39:13

PreactHooks狀態(tài)管理

2010-09-25 14:01:11

Java跨平臺

2022-01-14 08:08:11

Java依賴沖突

2018-10-25 15:13:23

APP脫殼工具

2011-04-13 15:01:39

2020-08-05 08:21:41

Webpack

2009-08-27 14:29:28

顯式實現(xiàn)接口

2009-09-07 05:24:22

C#窗體繼承

2023-04-28 09:05:20

魔方基礎(chǔ)流程

2023-10-11 12:35:29

Maven

2009-09-04 10:05:16

C#調(diào)用瀏覽器瀏覽器的原理

2018-03-14 08:39:40

2023-02-12 23:23:30

2009-07-03 17:48:34

JSP頁面翻譯

2009-07-06 09:23:51

Servlet定義

2010-08-05 17:35:34

RIP路由協(xié)議

2012-02-29 09:32:01

Java

2020-11-05 11:14:29

Docker底層原理

2012-08-21 11:13:08

點贊
收藏

51CTO技術(shù)棧公眾號