SpringBoot可執(zhí)行jar包中使用自定義字符集失敗問題的原因及解決方案
最近遇到一個(gè)詭異的問題:程序中需要用到一個(gè)自定義的字符集包my-charset.jar,里面有一個(gè)X-GB18030-2022字符集。程序功能要求把UTF-8編碼的漢字轉(zhuǎn)換成X-GB18030-2022編碼的漢字。整個(gè)程序使用springboot開發(fā)(基于java8),整個(gè)程序需要打包成一個(gè)可執(zhí)行jar包(即fatjar)部署到服務(wù)器上。
功能實(shí)現(xiàn)很簡單:
String data = "?"; // UTF-8編碼: E49080。這里直接寫成了字面值
Charset gb18030 = Charset.forName("X-GB18030-2022");
byte[] gbdata = data.getBytes(gb18030); // GB18030編碼:82338E35
在IDEA上開發(fā)測試一切正常,可以把UTF-8編碼的漢字轉(zhuǎn)成GB18030編碼。然后使用spring-boot-maven-plugin把整個(gè)程序打包成一個(gè)可執(zhí)行jar包my-springboot-app.jar后,部署到服務(wù)器上,再測試,卻發(fā)現(xiàn)程序報(bào)錯(cuò):UnsupportedCharsetException: X-GB18030-2022。
根據(jù)錯(cuò)誤信息,可以定位是Charset.forName("X-GB18030-2022")這個(gè)語句報(bào)錯(cuò)。明明在IDEA中調(diào)試沒有問題,為什么部署到服務(wù)器上就報(bào)錯(cuò)了?
要解決這個(gè)問題,需要以下知識(shí):
- 類加載器和類加載的雙親委托模型
- Charset.forName()的SPI機(jī)制原理,以及SPI對雙親委派模型的突破
- Springboot的可執(zhí)行jar包(fatjar)的啟動(dòng)原理
掌握了這些知識(shí),再解決這個(gè)問題,就很容易了。
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í)行類加載,父親無法加載時(shí),再有子類加載。
- 類A使用到類B時(shí),會(huì)由類A的類加載器加載類B
- 子類加載器加載的類,可以看到父親加載器加載的類。但是反過來則不行,即父親加載器加載的類,無法看到子類加載器加載的類。
這些規(guī)則比較抽象,我們暫且有個(gè)印象,后面會(huì)結(jié)合SPI具體講解。
2. SPI機(jī)制
SPI是Service Provider Interface的縮寫,其作用就是java在標(biāo)準(zhǔn)庫中提供一個(gè)接口(Interface),用戶使用時(shí)直接操作這個(gè)接口,而不用關(guān)心接口的具體實(shí)現(xiàn)。接口的具體功能由供應(yīng)商(Service Provider)實(shí)現(xiàn),通過某種機(jī)制(即SPI),把這個(gè)實(shí)現(xiàn)和接口關(guān)聯(lián)到一起,用戶操作的接口可以直接調(diào)用供應(yīng)商實(shí)現(xiàn)的功能。
最典型的SPI就是數(shù)據(jù)庫驅(qū)動(dòng)。Java標(biāo)準(zhǔn)庫中僅提供了數(shù)據(jù)庫驅(qū)動(dòng)的接口,而具體的驅(qū)動(dòng)由各個(gè)數(shù)據(jù)庫供應(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ù)庫。
字符集也是同樣的道理。Charset.forName()函數(shù)就使用了SPI機(jī)制,來加載不同的字符集。Java標(biāo)準(zhǔn)庫中的提供了字符集的接口java.nio.charset.spi.CharsetProvider(它實(shí)際是一個(gè)虛擬類,為了簡便,我們把它稱為接口),各個(gè)供應(yīng)商實(shí)現(xiàn)了這個(gè)接口,把自己實(shí)現(xiàn)的字符集貢獻(xià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è)問題,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ī)則,父類加載器加載的類無法看到子類加載器加載的類,也就是說Charset和ServiceLoader無法看到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作為類加載器,來加載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來加載類,所有可以加載到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包和類,而無法看到-classpath指定的jar包和類,所以它無法加載my-charset.jar中的類。
到這里,我們開頭的問題就有了一個(gè)可能的答案:是不是打包成的fatjar使用的類加載器有問題呢?我們接著往下看。
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來加載依賴JAR包的,而是使用了自定義的一個(gè)類加載器LaunchedURLClassLoader,它的父親是App-class-loader,也滿足雙親委托模型。
注意fatjar是無法使用-classpath來指定依賴包路徑的,這些依賴包在fatjar的內(nèi)部。例如my-charset.jar就被封裝在了BOOT-INF/lib里,外部是無法看到的,因此也就無法用-classpath來指定,所以也無法用App-class-loader直接加載。同時(shí),java -jar命令還會(huì)忽略-classpath參數(shù),即使把my-charset.jar拿出來用-classpath指定,也不會(huì)被加載。
就是這個(gè)springboot自定義的類加載器導(dǎo)致了開頭的問題:my-charset.jar包是被LaunchedURLClassLoader加載的,但是Charset.forName()函數(shù)中使用App-class-loader來加載CharsetProvider的所有實(shí)現(xiàn)類,App-class-loader看不到LaunchedURLClassLoader加載的類,所以就會(huì)找不到X-GB18030-2022字符集,導(dǎo)致報(bào)錯(cuò)。
4. 解決方案
知道了問題,那解決方案也就隨之而出:
- 讓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()這條語句會(huì)獲取到當(dāng)前線程的類加載器,對于我們自己寫的應(yīng)用來說,這就是springboot的LaunchedURLClassLoader類加載器。然后使用這個(gè)類加載器執(zhí)行ServiceLoader.load(CharsetProvider.class, loader)函數(shù),可以加載到my-charset.jar中的類了。
以上就是SpringBoot可執(zhí)行jar包中使用自定義字符集失敗問題的原因及解決方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot jar自定義字符集失敗的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Mybatis-plus使用TableNameHandler分表詳解(附完整示例源碼)
這篇文章主要介紹了Mybatis-plus使用TableNameHandler分表詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
springboot+websocket實(shí)現(xiàn)并發(fā)搶紅包功能
本文主要介紹了springboot+websocket實(shí)現(xiàn)并發(fā)搶紅包功能,主要包含了4種步驟,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
基于SSM+Shiro+Bootstrap實(shí)現(xiàn)用戶權(quán)限管理系統(tǒng)
這篇文章主要介紹了基于SSM+Shiro實(shí)現(xiàn)一個(gè)用戶權(quán)限管理系統(tǒng),每位用戶只可訪問指定的頁面,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)或工作有一定幫助,快跟隨小編一起學(xué)習(xí)吧2021-12-12
Java多線程中不同條件下編寫生產(chǎn)消費(fèi)者模型方法介紹
這篇文章主要介紹了Java多線程中不同條件下編寫生產(chǎn)消費(fèi)者模型方法介紹,介紹了生產(chǎn)消費(fèi)者模型,然后分享了相關(guān)代碼示例,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11
java多線程開發(fā)ScheduledExecutorService簡化方式
這篇文章主要為大家介紹了java多線程開發(fā)ScheduledExecutorService的簡化方式,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03
tk.Mybatis 插入數(shù)據(jù)獲取Id問題
本文主要介紹了tk.Mybatis 插入數(shù)據(jù)獲取Id問題,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
SpringBoot 項(xiàng)目搭建的 4 種常用方式(從入門到實(shí)踐)
本文將詳細(xì)介紹 4 種常用的 SpringBoot 項(xiàng)目搭建方式,無論你是新手還是有經(jīng)驗(yàn)的開發(fā)者,都能找到適合自己的方式快速上手,感興趣的朋友一起看看吧2025-07-07

