当前位置: 代码网 > it编程>编程语言>Java > SpringBoot可执行jar包中使用自定义字符集失败问题的原因及解决方案

SpringBoot可执行jar包中使用自定义字符集失败问题的原因及解决方案

2025年07月30日 Java 我要评论
最近遇到一个诡异的问题:程序中需要用到一个自定义的字符集包my-charset.jar,里面有一个x-gb18030-2022字符集。程序功能要求把utf-8编码的汉字转换成x-gb18030-202

最近遇到一个诡异的问题:程序中需要用到一个自定义的字符集包my-charset.jar,里面有一个x-gb18030-2022字符集。程序功能要求把utf-8编码的汉字转换成x-gb18030-2022编码的汉字。整个程序使用springboot开发(基于java8),整个程序需要打包成一个可执行jar包(即fatjar)部署到服务器上。

功能实现很简单:

string data = "䐀";		// utf-8编码: e49080。这里直接写成了字面值
charset gb18030 = charset.forname("x-gb18030-2022");
byte[] gbdata = data.getbytes(gb18030);	// gb18030编码:82338e35

在idea上开发测试一切正常,可以把utf-8编码的汉字转成gb18030编码。然后使用spring-boot-maven-plugin把整个程序打包成一个可执行jar包my-springboot-app.jar后,部署到服务器上,再测试,却发现程序报错:unsupportedcharsetexception: x-gb18030-2022

根据错误信息,可以定位是charset.forname("x-gb18030-2022")这个语句报错。明明在idea中调试没有问题,为什么部署到服务器上就报错了?

要解决这个问题,需要以下知识:

  • 类加载器和类加载的双亲委托模型
  • charset.forname()的spi机制原理,以及spi对双亲委派模型的突破
  • springboot的可执行jar包(fatjar)的启动原理

掌握了这些知识,再解决这个问题,就很容易了。

1. 类加载器和类加载的双亲委托模型

java程序中我们用到的所有类,例如我们常用的string、integer以及我们自己写的类,都是由类加载器加载到java虚拟机中的。类加载器分为三类:

  • 启动类加载器(bootstrap class loader):是最顶层的类加载器,加载rt.jar、tools.jar等基础包,包括object.class、string.class、 charset.class等。
  • 扩展类加载器(extension class loader):加载扩展包。其父加载器是启动类加载器。
  • 应用程序类加载器(application class loader):加载由-classpath参数指定的jar包。其父加载器是扩展类加载器。

它们的关系如图所示:

+----------------------------+                     
|                            |                     
|   bootstrap class loader   |                     
|                            |                     
+--------------^-------------+                     
               |                                   
               |                                   
               |                                   
+----------------------------+                     
|                            |                     
|      ext class loader      |                     
|                            |                     
+--------------^-------------+                     
               |                                   
               |                                   
               |                                   
+----------------------------+                     
|                            |                     
|      app class loader      |                     
|                            |                     
+----------------------------+ 

类加载器加载类的规则为:

  • 双亲委派模型(或者翻译为父亲委模型制更贴切):一个类加载器加载某个类时,先交给它的父亲执行类加载,父亲无法加载时,再有子类加载。
  • 类a使用到类b时,会由类a的类加载器加载类b
  • 子类加载器加载的类,可以看到父亲加载器加载的类。但是反过来则不行,即父亲加载器加载的类,无法看到子类加载器加载的类。

这些规则比较抽象,我们暂且有个印象,后面会结合spi具体讲解。

2. spi机制

spi是service provider interface的缩写,其作用就是java在标准库中提供一个接口(interface),用户使用时直接操作这个接口,而不用关心接口的具体实现。接口的具体功能由供应商(service provider)实现,通过某种机制(即spi),把这个实现和接口关联到一起,用户操作的接口可以直接调用供应商实现的功能。

最典型的spi就是数据库驱动。java标准库中仅提供了数据库驱动的接口,而具体的驱动由各个数据库供应商实现,例如oracle驱动或者mysql驱动。我们使用不同供应商的驱动时,就会把驱动的实现和驱动的接口关联到一起。我们写的程序不用改变,只要更换了驱动的供应商,就可以连接不同的数据库。

字符集也是同样的道理。charset.forname()函数就使用了spi机制,来加载不同的字符集。java标准库中的提供了字符集的接口java.nio.charset.spi.charsetprovider(它实际是一个虚拟类,为了简便,我们把它称为接口),各个供应商实现了这个接口,把自己实现的字符集贡献出来。前面出问题的my-charset.jar就是供应商实现的一个字符集,它实现的接口类是com.x.gb18030_2022_charsetprovider

供应商实现了字符集之后,在my-charset.jar包的meta-inf/services/目录下创建一个java.nio.charset.spi.charsetprovider文件(注意它是一个文件名),文件中的内容就是供应商实现的接口类,例如com.x.gb18030_2022_charsetprovider,它的作用就是提供"x-gb18030-2022"字符集。

charset.forname()函数会执行serviceloader.load()函数,在所有jar包中搜索"meta-inf/services/java.nio.charset.spi.charsetprovider"文件,执行文件中指定的类,这样就获取到了供应商提供的字符集。在这里,我们就获取到了"x-gb18030-2022"字符集。

这里就会有一个问题,charset、serviceloader都是由bootstrap-class-loader加载的,而供应商提供的my-charset.jar一般会放到-classpath指定的路径上,即my-charset.jar是由app-class-loader加载的,进而可知com.x.gb18030_2022_charsetprovider是由app-class-loader加载的。根据前面类加载器的规则,父类加载器加载的类无法看到子类加载器加载的类,也就是说charsetserviceloader无法看到gb18030_2022_charsetprovider,这怎么办?

这就需要突破双亲委托模型的限制:serviceloader.load()函数有一个参数就是classloader,即这个函数可以指定类加载器。查看charset.forname()的源码:

classloader cl = classloader.getsystemclassloader();	// 获取app-class-loader
serviceloader<charsetprovider> sl = serviceloader.load(charsetprovider.class, cl);

可以看到这里使用了app-class-loader作为类加载器,来加载charsetprovider的所有实现类。my-charset.jar中就有一个此接口的实现类。

在idea中启动应用时,会自动添加-classpath参数,指向my-charset.jar。根据第2节的内容可知,app-class-loader会加载-classpath指向的jar包,这些jar包不会被bootstrap-class-loader加载的类(例如string.class, charset.class)看到。但是由于charset.forname()函数中显示的指定了使用app-class-loader来加载类,所有可以加载到my-charset.jar中的类。

我们可以做个假设:如果charset.forname()函数中使用的类加载器cl不是app-class-loader,而是ext-class-loader,那可以获取到my-charset.jar中的类吗?答案是不能。因为ext-class-loader只能看到<java_home>\lib\ext路径中的jar包和类以及bootstrap-class-loader加载的jar包和类,而无法看到-classpath指定的jar包和类,所以它无法加载my-charset.jar中的类。

到这里,我们开头的问题就有了一个可能的答案:是不是打包成的fatjar使用的类加载器有问题呢?我们接着往下看。

3. fatjar启动原理

我们使用spring-boot-maven-plugin把springboot项目打包成一个可执行jar包,其目录结构如下:

my-springboot-app.jar/
│
├── meta-inf/
│   ├── manifest.mf               # 清单文件,包含主类信息等
│   └── maven/                    # maven元数据
│
├── boot-inf/
│   ├── classes/                  # 应用程序编译后的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        # 供应商的字符集jar包
│       └── ... (其他依赖jar)
│
└── org/
    └── springframework/
        └── boot/
            └── loader/           # spring boot启动器相关类
                ├── jarlauncher.class
                ├── launchedurlclassloader.class
                └── ... (启动器其他类)

使用命令java -jar my-springboot-app.jar启动此应用。应用启动时会使用launchedurlclassloader作为类加载器,加载boot-inf/中class目录中的类和lib目录中的依赖jar包,我们的my-charset.jar就被放到了这里。

由此可知,fatjar启动时不是使用app-class-loader来加载依赖jar包的,而是使用了自定义的一个类加载器launchedurlclassloader,它的父亲是app-class-loader,也满足双亲委托模型。

注意fatjar是无法使用-classpath来指定依赖包路径的,这些依赖包在fatjar的内部。例如my-charset.jar就被封装在了boot-inf/lib里,外部是无法看到的,因此也就无法用-classpath来指定,所以也无法用app-class-loader直接加载。同时,java -jar命令还会忽略-classpath参数,即使把my-charset.jar拿出来用-classpath指定,也不会被加载。

就是这个springboot自定义的类加载器导致了开头的问题:my-charset.jar包是被launchedurlclassloader加载的,但是charset.forname()函数中使用app-class-loader来加载charsetprovider的所有实现类,app-class-loader看不到launchedurlclassloader加载的类,所以就会找不到x-gb18030-2022字符集,导致报错。

4. 解决方案

知道了问题,那解决方案也就随之而出:

  • 让my-charset.jar被app-class-loader、ext-class-loader或者bootstrap-class-loader加载
  • 自己实现charset.forname()函数,使用launchedurlclassloader获取my-charset.jar中的类

方法一:指定类加载器

  • java8的jvm启动时,可以使用-xbootclasspath/a:<path-to-jar>指定my-charset.jar的路径,让bootstrap-class-loader加载这个jar包
  • 可以使用-djava.ext.dir=<path-to-dir>指定一个目录,让扩展类加载器加载这个目录中的所有jar包

方法二:修改函数

代码如下:

try {
    charset target = charset.forname("x-gb18030-2022");
} catch (exception ex) {
    // 关键代码
    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()这条语句会获取到当前线程的类加载器,对于我们自己写的应用来说,这就是springboot的launchedurlclassloader类加载器。然后使用这个类加载器执行serviceloader.load(charsetprovider.class, loader)函数,可以加载到my-charset.jar中的类了。

以上就是springboot可执行jar包中使用自定义字符集失败问题的原因及解决方案的详细内容,更多关于springboot jar自定义字符集失败的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com