成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

淺析Java類隔離規避依賴沖突的實現原理

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

一、導語

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

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

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

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

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

二、類隔離容器

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

圖片圖片

圖片圖片

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

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

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

圖片圖片

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

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

三、類加載API

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

圖片圖片

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

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

圖片圖片

ClassLoader的實現者負責根據【位置無關】的類標識,定位、裝載類。所謂【位置無關】說的是,JVM不關心這個類文件的物理位置是在網絡上、磁盤里、內存里。

由于Classs類型的返回值無法由開發者自行構造,涉及JVM內部的狀態聯動,因此JVM會暴露一個構造Class對象的工具API。抽象的API如下:

圖片圖片

該parseAndLinkClass方法由JVM實現,JVM內部會進行我們八股文都背過的類驗證、類解析、類初始化等標準動作。

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

圖片圖片

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

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

裝載類

圖片圖片

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

定義類

圖片

java.lang.ClassLoader#defineClass0

JNI方法實現

圖片圖片

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

四、類的相等性:

ClassCastException

盡管在源代碼層面,我們用【類的全限定名】作為編碼時定位類的標識,但是在JVM內部,類的標識是一個聯合索引。

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

通俗的偽代碼來表達的話,上述ClassLoadUtil#parseAndLinkClass方法的實現如下:

圖片圖片

defineClass時,創建的Class對象上會關聯Loader。

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

圖片圖片

java.lang.Class#classLoader

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

圖片圖片

圖片圖片

圖片圖片

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

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

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

那么,當LoadTest.main方法執行時即會產生ClassCastException異常。

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

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

五、類加載編排、委托

綜上分析,我們發現在Java層面實現load一個類并不復雜,只需要根據類名拿到二進制的字節碼,然后調用JVM提供的工具方法就行了。

到這里,事情已經回到我們最熟悉不過的CRUD主場,我們可以用各種我們熟悉的設計模式來實現特定的類加載業務需求,其中最重要的設計模式即為:委托模式。

第一個業務需求是類加載的安全性。Java標準庫自帶了大量易用的工具和數據結構,這部分代碼的物理位置和業務代碼不在一起。為避免項目中的惡意代碼使用標準庫同名的類來干壞事,我們需要實現類加載優先級,即加載一個類時優先從JRE目錄加載,JRE目錄中加載不到時再從項目中加載。

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

那么我們重新實現上述AbstractClassLoader如下:

圖片圖片

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

圖片圖片

上述代碼通過編排類加載器,實現了如下項目依賴空間拓撲:

圖片圖片

綜上,我們在Java Runtime的基礎ClassLoader機制上,通過非常熟悉的業務編排實現了類加載的安全性需求、共享復用需求,最終呈現了一個樹形的類加載器拓撲。

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

六、類加載劫持

到這里,我們已經有足夠的技術儲備來根據業務需求編排類加載器拓撲以達成目的。但是遺留了一個關鍵的問題:

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

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

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

JVM規范對這一塊的闡述是嚴謹但抽象的,但通俗來說就一個原則:如果一個類C1是由CL加載器加載(defineClass)的,那么,C1觸發的的其他類如Cn的加載和鏈接,也會委托給CL。示例如下:

圖片圖片

圖片圖片

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

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

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

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

圖片圖片

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

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

java.lang.Thread#getContextClassLoader

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

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

到這里,我們已經掌握了實現類隔離容器的核心基礎。

總結來說,只要我們能在應用的EntryPoint(main方法)中合理的介入、干預,就能實現靈活的類加載業務。

七、類隔離模塊:Bundle

回到最開始的需求,我們希望可以在項目中達成如下依賴結構:

圖片

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

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

圖片圖片

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

圖片圖片

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

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

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

解壓FatJar

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

文件切片

我們知道,Jar文件本質上就是ZIP格式的文件,而ZIP文件的邏輯結構是一個Map。

圖片圖片

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

圖片圖片

ZIP文件除文件元數據外,整體分為兩部分。

  1. 數據區:存放文件的內容。
  2. 索引區:存放文件名稱和文件內容的偏移量和長度。

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

圖片圖片

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

可以參考相關資料:

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

八、類導入/導出:Bundle元信息

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

圖片圖片

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

這里存在兩個問題:

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

圖片圖片

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

圖片圖片

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

圖片圖片

上述信息表明:

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

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

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

九、Bundle依賴隔離

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

業務類加載器

圖片圖片

Bundle類加載器

圖片圖片

類加載器管理器

圖片圖片

類隔離容器底座

圖片圖片

圖片圖片

使用姿勢

圖片圖片

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

以上,我們就實現了一個簡單的類隔離容器,最終形成的類加載器拓撲如下:

圖片圖片

最終實現了每個Bundle優先使用自身內部嵌入的Jar依賴,從而實現每個Bundle Jar有一個獨立的依賴空間,避免了依賴沖突。

十、沒有銀彈

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

考慮如下的場景:

圖片圖片

  • Bundle BBB中導出了如下Service:

圖片圖片

  • Bundle AAA中導出了如下Service:

圖片圖片

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

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

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

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

十一

總結

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

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

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

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

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

圖片

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

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

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

參考

  • 微服務的災難-依賴地獄(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)
責任編輯:武曉燕 來源: 得物技術
相關推薦

2020-12-30 08:01:07

Java隔離加載

2023-12-18 09:39:13

PreactHooks狀態管理

2010-09-25 14:01:11

Java跨平臺

2018-10-25 15:13:23

APP脫殼工具

2022-01-14 08:08:11

Java依賴沖突

2011-04-13 15:01:39

2020-08-05 08:21:41

Webpack

2009-09-07 05:24:22

C#窗體繼承

2009-08-27 14:29:28

顯式實現接口

2023-04-28 09:05:20

魔方基礎流程

2009-07-06 09:23:51

Servlet定義

2010-08-05 17:35:34

RIP路由協議

2009-07-03 17:48:34

JSP頁面翻譯

2023-02-12 23:23:30

2018-03-14 08:39:40

2009-09-04 10:05:16

C#調用瀏覽器瀏覽器的原理

2023-05-11 07:25:57

ReduxMiddleware函數

2009-07-16 10:23:30

iBATIS工作原理

2020-11-05 11:14:29

Docker底層原理

2023-10-11 12:35:29

Maven
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产日韩欧美激情 | 欧美国产亚洲一区二区 | 国产精品明星裸体写真集 | 久久中文字幕一区 | 狠狠色综合久久丁香婷婷 | 日韩一级免费电影 | 大香网伊人| 欧美久久天堂 | 国产精品久久久久9999鸭 | 成人在线国产 | 免费一区二区三区 | 久久久久久国产精品免费免费 | 久久久久久99 | 欧美成人在线免费 | 色婷婷综合网 | 免费黄色在线观看 | 国产精品国产三级国产aⅴ浪潮 | 欧美黄色片 | 欧美日韩一区二区三区四区 | 伊人网综合在线观看 | 国产精品2 | 国产电影精品久久 | 欧美一区二区视频 | 国产性生活一级片 | 一级a性色生活片久久毛片 一级特黄a大片 | 18性欧美 | 午夜视频大全 | 国产一区二区三区免费观看视频 | 亚洲精品久久久久中文字幕欢迎你 | 日韩欧美国产一区二区 | 久久在线 | 久久男人 | 欧美一区二区三区在线观看 | 日韩在线视频免费观看 | 久久精品视频12 | 精品欧美一区二区中文字幕视频 | 亚洲精品66 | 精品乱码一区二区三四区 | 精品国产一区二区三区日日嗨 | 亚洲国产电影 | 国产一级片一区二区三区 |