
本文深入探讨了Spring Boot应用以不同打包方式(JAR vs. WAR)运行时,接口响应速度出现显著差异的根本原因。当应用以 java -jar app.war 方式直接运行War包时,会观察到接口响应变慢的现象,尤其是在涉及数据库IO操作时。本文将详细分析其背后的类加载机制差异,并提供明确的解决方案。
目录
- 一、背景与问题引入
- 二、问题现象:接口响应异常变慢
- 三、问题排查与解决方案
- 四、问题根源深度剖析
- (一)Spring Boot的可执行War包
- (二)JAR与WAR包的文件结构差异
- (三)启动引导类:JarLauncher vs. WarLauncher
- (四)类加载器与资源扫描过程差异
- (五)性能差异根源总结
一、背景与问题引入
在项目容器化改造过程中,我们计划将原本部署在主机Tomcat中的Spring Boot应用迁移至K8s平台,以Docker容器形式运行。为了简化镜像构建与部署流程,决定放弃传统的外置Tomcat容器部署模式,改为使用更轻量的Jar包方式,通过 java -jar 命令直接运行应用。
Spring Boot应用主流的打包方式有两种:
- JAR包:使用Spring Boot内嵌的Web服务器(如Tomcat),通过
java -jar 命令直接运行。
- WAR包:部署到外部的Servlet容器(如Tomcat、Jetty)中运行。
然而,由于沟通失误,运维人员最终将应用构建成了War包,并在Docker容器中尝试使用 java -jar app.war 的方式启动。应用虽能正常启动且功能无碍,却为后续的性能问题埋下了隐患。
二、问题现象:接口响应异常变慢
迁移至K8s环境后,测试人员反馈系统响应速度显著下降,部分页面加载时间甚至长达十几秒。通过日志分析发现,响应延迟集中出现在需要进行数据库操作的业务环节。
即使是执行最简单的单条记录查询(SELECT),接口耗时也达到了3秒左右,这比在IDE本地直接运行的速度要慢得多。

图:Chrome开发者工具Network面板显示的慢请求瀑布图
三、问题排查与解决方案
面对性能问题,我们首先设想了以下几种可能性:
- 数据库服务响应缓慢。
- 容器内网络环境存在延迟或波动。
- 容器分配的内存过小,导致频繁的垃圾回收(GC)。
针对以上猜想,我们逐一进行了排查:
- 在容器内使用MySQL客户端直接连接数据库并执行SQL,响应速度正常。
- 从容器内
ping数据库地址,网络稳定,无丢包或高延迟。
- 通过JVM参数
-XX:+PrintGC 启用GC日志,未发现频繁的GC或Full GC活动。
排除了上述常见原因后,我们开始审视应用本身。在本地复现Docker环境时,偶然发现容器内运行的竟是一个 War包(通过 java -jar app.war 启动)。这立刻成为了新的排查方向。
我们对比了多种运行场景下的性能表现:
- Docker容器 + War包(
java -jar):响应慢。
- Docker容器 + Jar包(
java -jar):响应正常。
- Windows命令行 + War包(
java -jar):响应慢。
- Windows命令行 + Jar包(
java -jar):响应正常。
- IDE中直接运行:响应正常。
结论清晰:问题根源在于使用 java -jar 命令直接运行War包。检查项目pom.xml文件,发现打包方式(<packaging>)仍被错误地设置为war。将其修改为jar后重新构建部署,接口响应速度立即恢复正常。
因此,对于不依赖外部Servlet容器的Spring Boot应用,务必采用Jar包方式进行打包和部署,这也是官方推荐的最佳实践。
四、问题根源深度剖析
为什么直接运行War包会导致性能下降?我们需要从Spring Boot的打包和启动机制中寻找答案。
(一)Spring Boot的可执行War包
在Maven的XSD定义中,packaging元素通常用于指定项目输出类型。

图:Maven XSD中关于packaging元素的定义
关键在于,Spring Boot官方文档(12.17.1. Create a Deployable War File)指出:
如果你使用Spring Boot构建工具,并将内嵌Servlet容器依赖标记为provided,那么将会生成一个可执行的WAR文件。这意味着,除了可以部署到Servlet容器,你也可以在命令行中使用 java -jar 来运行它。
这正是我们的应用能够以 java -jar app.war 方式启动的理论依据。
(二)JAR与WAR包的文件结构差异
解压两种格式的包,可以看到明显的结构区别:

图:War包解压后的文件结构

图:Jar包(Spring Boot可执行Jar)解压后的文件结构
主要差异点:
- 源码目录:Jar包为
BOOT-INF/classes/,War包为WEB-INF/classes/。
- 依赖库目录:Jar包依赖位于
BOOT-INF/lib/;War包依赖则分为WEB-INF/lib/和WEB-INF/lib-provided/(provided作用域的依赖)。
- 清单文件(MANIFEST.MF):该文件指明了应用启动的入口类。
- War包的
Main-Class为 org.springframework.boot.loader.WarLauncher
- Jar包的
Main-Class为 org.springframework.boot.loader.JarLauncher
(三)启动引导类:JarLauncher vs. WarLauncher
WarLauncher和JarLauncher都是Launcher的子类,它们决定了如何定位和加载应用资源(类和依赖Jar)。
WarLauncher的核心方法表明它只关心WEB-INF/目录下的内容:
protected boolean isNestedArchive(Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals("WEB-INF/classes/");
} else {
// 关注WEB-INF/lib/和WEB-INF/lib-provided/下的jar
return entry.getName().startsWith("WEB-INF/lib/") || entry.getName().startsWith("WEB-INF/lib-provided/");
}
}
而JarLauncher则对应地关注BOOT-INF/目录:
static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
return entry.isDirectory() ? entry.getName().equals("BOOT-INF/classes/") : entry.getName().startsWith("BOOT-INF/lib/");
};

(四)类加载器与资源扫描过程差异
不同引导类会创建不同的类加载器(ClassLoader)。在初始化过程中,它们扫描和加载依赖的方式存在本质区别,尤其是在处理像数据库驱动这样的外部组件时。
- War模式(
WarLauncher):其类加载器(如TomcatEmbeddedWebappClassLoader)通常采用文件系统遍历的方式,在WEB-INF/lib/和WEB-INF/lib-provided/目录下逐个扫描Jar文件来查找和加载类。这个过程涉及更多的文件IO操作。
- Jar模式(
JarLauncher):其类加载器(如LaunchedURLClassLoader)则通过预先构建的类路径索引(如BOOT-INF/classpath.idx)或直接读取BOOT-INF/lib/下的URL列表来定位资源,扫描效率更高,IO开销更小。
以加载com.mysql.jdbc.MySQLConnection类为例:在War模式下,类加载器需要遍历更多、结构更复杂的目录来寻找包含该类的Jar包,导致每次数据库连接建立时,类加载的IO耗时显著增加,从而拖慢接口响应。
(五)性能差异根源总结
根本原因在于:使用 java -jar 直接运行Spring Boot可执行War包时,其启动和运行过程中所使用的类加载机制,并非为这种运行方式做过深度优化。它继承了传统Web应用部署时更复杂、更耗时的资源扫描逻辑。
而在标准的Jar包运行方式下,Spring Boot的类加载机制经过了精心设计,能够高效地定位和加载内嵌的依赖,避免了不必要的文件系统遍历,从而保证了最佳性能。这也是为何在容器化改造和K8s平台部署中,优先推荐使用Jar包方式的重要原因之一。