最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot可執(zhí)行jar包中使用自定義字符集失敗問(wèn)題的原因及解決方案

 更新時(shí)間:2025年07月30日 10:55:35   作者:槲櫟  
這篇文章主要介紹了Spring Boot fatjar部署時(shí),因類加載器雙親委托模型與SPI機(jī)制沖突導(dǎo)致自定義字符集無(wú)法加載的問(wèn)題,解決方案是修改代碼使用當(dāng)前線程類加載器或調(diào)整JVM參數(shù),確保字符集實(shí)現(xiàn)被正確識(shí)別,需要的朋友可以參考下

最近遇到一個(gè)詭異的問(wèn)題:程序中需要用到一個(gè)自定義的字符集包my-charset.jar,里面有一個(gè)X-GB18030-2022字符集。程序功能要求把UTF-8編碼的漢字轉(zhuǎn)換成X-GB18030-2022編碼的漢字。整個(gè)程序使用springboot開(kāi)發(fā)(基于java8),整個(gè)程序需要打包成一個(gè)可執(zhí)行jar包(即fatjar)部署到服務(wù)器上。

功能實(shí)現(xiàn)很簡(jiǎn)單:

String data = "?";		// UTF-8編碼: E49080。這里直接寫成了字面值
Charset gb18030 = Charset.forName("X-GB18030-2022");
byte[] gbdata = data.getBytes(gb18030);	// GB18030編碼:82338E35

在IDEA上開(kāi)發(fā)測(cè)試一切正常,可以把UTF-8編碼的漢字轉(zhuǎn)成GB18030編碼。然后使用spring-boot-maven-plugin把整個(gè)程序打包成一個(gè)可執(zhí)行jar包my-springboot-app.jar后,部署到服務(wù)器上,再測(cè)試,卻發(fā)現(xiàn)程序報(bào)錯(cuò):UnsupportedCharsetException: X-GB18030-2022。

根據(jù)錯(cuò)誤信息,可以定位是Charset.forName("X-GB18030-2022")這個(gè)語(yǔ)句報(bào)錯(cuò)。明明在IDEA中調(diào)試沒(méi)有問(wèn)題,為什么部署到服務(wù)器上就報(bào)錯(cuò)了?

要解決這個(gè)問(wèn)題,需要以下知識(shí):

  • 類加載器和類加載的雙親委托模型
  • Charset.forName()的SPI機(jī)制原理,以及SPI對(duì)雙親委派模型的突破
  • Springboot的可執(zhí)行jar包(fatjar)的啟動(dòng)原理

掌握了這些知識(shí),再解決這個(gè)問(wèn)題,就很容易了。

1. 類加載器和類加載的雙親委托模型

Java程序中我們用到的所有類,例如我們常用的String、Integer以及我們自己寫的類,都是由類加載器加載到j(luò)ava虛擬機(jī)中的。類加載器分為三類:

  • 啟動(dòng)類加載器(bootstrap class loader):是最頂層的類加載器,加載rt.jar、tools.jar等基礎(chǔ)包,包括Object.class、String.class、 Charset.class等。
  • 擴(kuò)展類加載器(extension class loader):加載擴(kuò)展包。其父加載器是啟動(dòng)類加載器。
  • 應(yīng)用程序類加載器(application class loader):加載由-classpath參數(shù)指定的jar包。其父加載器是擴(kuò)展類加載器。

它們的關(guān)系如圖所示:

+----------------------------+                     
|                            |                     
|   Bootstrap class loader   |                     
|                            |                     
+--------------^-------------+                     
               |                                   
               |                                   
               |                                   
+----------------------------+                     
|                            |                     
|      Ext class loader      |                     
|                            |                     
+--------------^-------------+                     
               |                                   
               |                                   
               |                                   
+----------------------------+                     
|                            |                     
|      App class loader      |                     
|                            |                     
+----------------------------+ 

類加載器加載類的規(guī)則為:

  • 雙親委派模型(或者翻譯為父親委模型制更貼切):一個(gè)類加載器加載某個(gè)類時(shí),先交給它的父親執(zhí)行類加載,父親無(wú)法加載時(shí),再有子類加載。
  • 類A使用到類B時(shí),會(huì)由類A的類加載器加載類B
  • 子類加載器加載的類,可以看到父親加載器加載的類。但是反過(guò)來(lái)則不行,即父親加載器加載的類,無(wú)法看到子類加載器加載的類。

這些規(guī)則比較抽象,我們暫且有個(gè)印象,后面會(huì)結(jié)合SPI具體講解。

2. SPI機(jī)制

SPI是Service Provider Interface的縮寫,其作用就是java在標(biāo)準(zhǔn)庫(kù)中提供一個(gè)接口(Interface),用戶使用時(shí)直接操作這個(gè)接口,而不用關(guān)心接口的具體實(shí)現(xiàn)。接口的具體功能由供應(yīng)商(Service Provider)實(shí)現(xiàn),通過(guò)某種機(jī)制(即SPI),把這個(gè)實(shí)現(xiàn)和接口關(guān)聯(lián)到一起,用戶操作的接口可以直接調(diào)用供應(yīng)商實(shí)現(xiàn)的功能。

最典型的SPI就是數(shù)據(jù)庫(kù)驅(qū)動(dòng)。Java標(biāo)準(zhǔn)庫(kù)中僅提供了數(shù)據(jù)庫(kù)驅(qū)動(dòng)的接口,而具體的驅(qū)動(dòng)由各個(gè)數(shù)據(jù)庫(kù)供應(yīng)商實(shí)現(xiàn),例如Oracle驅(qū)動(dòng)或者M(jìn)ySQL驅(qū)動(dòng)。我們使用不同供應(yīng)商的驅(qū)動(dòng)時(shí),就會(huì)把驅(qū)動(dòng)的實(shí)現(xiàn)和驅(qū)動(dòng)的接口關(guān)聯(lián)到一起。我們寫的程序不用改變,只要更換了驅(qū)動(dòng)的供應(yīng)商,就可以連接不同的數(shù)據(jù)庫(kù)。

字符集也是同樣的道理。Charset.forName()函數(shù)就使用了SPI機(jī)制,來(lái)加載不同的字符集。Java標(biāo)準(zhǔn)庫(kù)中的提供了字符集的接口java.nio.charset.spi.CharsetProvider(它實(shí)際是一個(gè)虛擬類,為了簡(jiǎn)便,我們把它稱為接口),各個(gè)供應(yīng)商實(shí)現(xiàn)了這個(gè)接口,把自己實(shí)現(xiàn)的字符集貢獻(xiàn)出來(lái)。前面出問(wèn)題的my-charset.jar就是供應(yīng)商實(shí)現(xiàn)的一個(gè)字符集,它實(shí)現(xiàn)的接口類是com.x.GB18030_2022_CharsetProvider。

供應(yīng)商實(shí)現(xiàn)了字符集之后,在my-charset.jar包的META-INF/services/目錄下創(chuàng)建一個(gè)java.nio.charset.spi.CharsetProvider文件(注意它是一個(gè)文件名),文件中的內(nèi)容就是供應(yīng)商實(shí)現(xiàn)的接口類,例如com.x.GB18030_2022_CharsetProvider,它的作用就是提供"X-GB18030-2022"字符集。

Charset.forName()函數(shù)會(huì)執(zhí)行ServiceLoader.load()函數(shù),在所有jar包中搜索"META-INF/services/java.nio.charset.spi.CharsetProvider"文件,執(zhí)行文件中指定的類,這樣就獲取到了供應(yīng)商提供的字符集。在這里,我們就獲取到了"X-GB18030-2022"字符集。

這里就會(huì)有一個(gè)問(wèn)題,Charset、ServiceLoader都是由Bootstrap-class-loader加載的,而供應(yīng)商提供的my-charset.jar一般會(huì)放到-classpath指定的路徑上,即my-charset.jar是由App-class-loader加載的,進(jìn)而可知com.x.GB18030_2022_CharsetProvider是由App-class-loader加載的。根據(jù)前面類加載器的規(guī)則,父類加載器加載的類無(wú)法看到子類加載器加載的類,也就是說(shuō)CharsetServiceLoader無(wú)法看到GB18030_2022_CharsetProvider,這怎么辦?

這就需要突破雙親委托模型的限制:ServiceLoader.load()函數(shù)有一個(gè)參數(shù)就是ClassLoader,即這個(gè)函數(shù)可以指定類加載器。查看Charset.forName()的源碼:

ClassLoader cl = ClassLoader.getSystemClassLoader();	// 獲取App-class-loader
ServiceLoader<CharsetProvider> sl = ServiceLoader.load(CharsetProvider.class, cl);

可以看到這里使用了App-class-loader作為類加載器,來(lái)加載CharsetProvider的所有實(shí)現(xiàn)類。my-charset.jar中就有一個(gè)此接口的實(shí)現(xiàn)類。

在IDEA中啟動(dòng)應(yīng)用時(shí),會(huì)自動(dòng)添加-classpath參數(shù),指向my-charset.jar。根據(jù)第2節(jié)的內(nèi)容可知,App-class-loader會(huì)加載-classpath指向的jar包,這些jar包不會(huì)被Bootstrap-class-loader加載的類(例如String.class, Charset.class)看到。但是由于Charset.forName()函數(shù)中顯示的指定了使用App-class-loader來(lái)加載類,所有可以加載到my-charset.jar中的類。

我們可以做個(gè)假設(shè):如果Charset.forName()函數(shù)中使用的類加載器cl不是App-class-loader,而是Ext-class-loader,那可以獲取到my-charset.jar中的類嗎?答案是不能。因?yàn)镋xt-class-loader只能看到<JAVA_HOME>\lib\ext路徑中的jar包和類以及Bootstrap-class-loader加載的jar包和類,而無(wú)法看到-classpath指定的jar包和類,所以它無(wú)法加載my-charset.jar中的類。

到這里,我們開(kāi)頭的問(wèn)題就有了一個(gè)可能的答案:是不是打包成的fatjar使用的類加載器有問(wèn)題呢?我們接著往下看。

3. Fatjar啟動(dòng)原理

我們使用spring-boot-maven-plugin把springboot項(xiàng)目打包成一個(gè)可執(zhí)行jar包,其目錄結(jié)構(gòu)如下:

my-springboot-app.jar/
│
├── META-INF/
│   ├── MANIFEST.MF               # 清單文件,包含主類信息等
│   └── maven/                    # Maven元數(shù)據(jù)
│
├── BOOT-INF/
│   ├── classes/                  # 應(yīng)用程序編譯后的class文件
│   └── lib/                      # 依賴的第三方JAR包
│       ├── spring-boot-2.7.0.jar
│       ├── spring-boot-autoconfigure-2.7.0.jar
│       ├── spring-web-5.3.21.jar
│       ├── my-charset.jar        # 供應(yīng)商的字符集JAR包
│       └── ... (其他依賴JAR)
│
└── org/
    └── springframework/
        └── boot/
            └── loader/           # Spring Boot啟動(dòng)器相關(guān)類
                ├── JarLauncher.class
                ├── LaunchedURLClassLoader.class
                └── ... (啟動(dòng)器其他類)

使用命令java -jar my-springboot-app.jar啟動(dòng)此應(yīng)用。應(yīng)用啟動(dòng)時(shí)會(huì)使用LaunchedURLClassLoader作為類加載器,加載BOOT-INF/中class目錄中的類和lib目錄中的依賴JAR包,我們的my-charset.jar就被放到了這里。

由此可知,fatjar啟動(dòng)時(shí)不是使用App-class-loader來(lái)加載依賴JAR包的,而是使用了自定義的一個(gè)類加載器LaunchedURLClassLoader,它的父親是App-class-loader,也滿足雙親委托模型。

注意fatjar是無(wú)法使用-classpath來(lái)指定依賴包路徑的,這些依賴包在fatjar的內(nèi)部。例如my-charset.jar就被封裝在了BOOT-INF/lib里,外部是無(wú)法看到的,因此也就無(wú)法用-classpath來(lái)指定,所以也無(wú)法用App-class-loader直接加載。同時(shí),java -jar命令還會(huì)忽略-classpath參數(shù),即使把my-charset.jar拿出來(lái)用-classpath指定,也不會(huì)被加載。

就是這個(gè)springboot自定義的類加載器導(dǎo)致了開(kāi)頭的問(wèn)題:my-charset.jar包是被LaunchedURLClassLoader加載的,但是Charset.forName()函數(shù)中使用App-class-loader來(lái)加載CharsetProvider的所有實(shí)現(xiàn)類,App-class-loader看不到LaunchedURLClassLoader加載的類,所以就會(huì)找不到X-GB18030-2022字符集,導(dǎo)致報(bào)錯(cuò)。

4. 解決方案

知道了問(wèn)題,那解決方案也就隨之而出:

  • 讓my-charset.jar被App-class-loader、Ext-class-loader或者Bootstrap-class-loader加載
  • 自己實(shí)現(xiàn)Charset.forName()函數(shù),使用LaunchedURLClassLoader獲取my-charset.jar中的類

方法一:指定類加載器

  • java8的jvm啟動(dòng)時(shí),可以使用-Xbootclasspath/a:<path-to-jar>指定my-charset.jar的路徑,讓Bootstrap-class-loader加載這個(gè)jar包
  • 可以使用-Djava.ext.dir=<path-to-dir>指定一個(gè)目錄,讓擴(kuò)展類加載器加載這個(gè)目錄中的所有jar包

方法二:修改函數(shù)

代碼如下:

try {
    Charset target = Charset.forName("X-GB18030-2022");
} catch (Exception ex) {
    // 關(guān)鍵代碼
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    ServiceLoader<CharsetProvider> providers = ServiceLoader.load(CharsetProvider.class, loader);
    
    Iterator<CharsetProvider> iterator = providers.iterator();
    while (iteartor.hasNext()) {
        CharsetProvider provider = iterator.next();
        try {
            target = provider.charsetForName("X-GB18030-2022");
        	if (target != null) {
            	return target;
        	}
        } catch (Exception ignored) {}
    }
    throw ex;
}

Thread.currentThread().getContextClassLoader()這條語(yǔ)句會(huì)獲取到當(dāng)前線程的類加載器,對(duì)于我們自己寫的應(yīng)用來(lái)說(shuō),這就是springboot的LaunchedURLClassLoader類加載器。然后使用這個(gè)類加載器執(zhí)行ServiceLoader.load(CharsetProvider.class, loader)函數(shù),可以加載到my-charset.jar中的類了。

以上就是SpringBoot可執(zhí)行jar包中使用自定義字符集失敗問(wèn)題的原因及解決方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot jar自定義字符集失敗的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論

乐清市| 团风县| 扎赉特旗| 天镇县| 姚安县| 紫阳县| 嘉禾县| 宜州市| 白银市| 兴山县| 巴彦县| 岐山县| 灵璧县| 江永县| 瑞丽市| 闸北区| 安义县| 合山市| 始兴县| 南部县| 牟定县| 遵义市| 宝坻区| 新沂市| 抚顺市| 磐石市| 龙岩市| 沙田区| 昌江| 府谷县| 定远县| 孟津县| 东阳市| 翁牛特旗| 仲巴县| 信宜市| 子洲县| 大名县| 渭源县| 新蔡县| 陵水|