背景:JAR vs WAR 的使用场景
JAR(Java Archive):Spring Boot 内嵌 Tomcat/Jetty/Undertow,打出来一个 Fat JAR,java -jar 一条命令就能跑。适合微服务、Docker 部署、独立应用。
WAR(Web Application Archive):标准 Servlet 容器部署格式,扔到 Tomcat/Jetty 的 webapps 目录即可。适合传统运维环境、企业级应用服务器(WebLogic/WebSphere)。
很多人会遇到这样的场景:开发时用 JAR 方便调试,但生产环境用的是公司统一管理的 Tomcat 集群,必须打 WAR。这时候就需要一套代码,两种打包方式。
核心思路:Maven Profile 动态切换
Maven 的 Profile 机制天然适合这个场景。我们定义两个 profile:
关键点:
<packaging> 在 profile 中直接覆盖
Spring Boot Maven Plugin 的 layout 属性控制打包格式
WAR 模式下将 spring-boot-starter-tomcat 设为 provided
如果配置正常,IDEA也会自动识别,打包前选一下就好了,非常方便

完整 POM 配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>1.0.0</version>
<!-- ========== 关键:默认打包类型设为 jar ========== -->
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 其他依赖... -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- ========== layout 用属性引用,由 profile 控制 ========== -->
<configuration>
<layout>${boot.layout}</layout>
</configuration>
</plugin>
</plugins>
</build>
<!-- =================== Profiles =================== -->
<profiles>
<!--
默认 JAR 打包 Profile
- activeByDefault=true:不指定 -P 参数时自动生效
- packaging=jar:标准 JAR
- boot.layout=ZIP:Spring Boot 可执行 JAR 布局(Fat JAR)
-->
<profile>
<id>jar</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<boot.layout>ZIP</boot.layout>
</properties>
<!-- 不需要覆盖 packaging,因为父级已经是 jar -->
</profile>
<!--
WAR 打包 Profile
- 手动激活:mvn clean package -P war
- packaging=war:Web 应用归档
- boot.layout=WAR:可部署到外部容器的 WAR
- 嵌入式 Tomcat 设为 provided,由外部容器提供
-->
<profile>
<id>war</id>
<!-- ⚠️ 关键:覆盖顶级 packaging 为 war -->
<packaging>war</packaging>
<properties>
<boot.layout>WAR</boot.layout>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</profile>
</profiles>
</project>配置要点解析
⚠️ 为什么不在顶级写 <packaging>${packaging.type}</packaging>?
Maven 的 <packaging> 元素在模型解析阶段就确定了,不支持属性占位符替换。正确的做法是:顶级写默认值 jar,在 war profile 中直接覆盖。
WAR 部署必备:启动类改造
打 WAR 包部署到外部 Tomcat 时,启动类必须继承 SpringBootServletInitializer 并重写 configure 方法:
package com.example.myapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class MyAppApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
/**
* WAR 部署时,外部 Servlet 容器通过此方法启动 Spring Boot 应用。
* JAR 运行时此方法不会被调用(因为用的是 main 方法),
* 所以两种模式能和谐共存。
*/
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(MyAppApplication.class);
}
}为什么 JAR 和 WAR 能共存?
JAR 模式:java -jar 调用 main() → 内嵌 Tomcat 启动
WAR 模式:外部 Tomcat 发现 SpringBootServletInitializer 子类 → 调 configure() → Spring 上下文启动
main() 在 WAR 中完全不会被外部容器调用,互不干扰
构建命令与验证
构建 JAR(默认)
# 默认就是 jar profile,不加参数也行
mvn clean package
# 显式指定(效果一样)
mvn clean package -P jar产物:
target/
├── my-app-1.0.0.jar ← Fat JAR,可直接执行
└── my-app-1.0.0.jar.original ← 原始 JAR(不含依赖)验证:
java -jar target/my-app-1.0.0.jar
# 输出:Started MyAppApplication in 2.5 seconds构建 WAR
mvn clean package -P war产物:
target/
├── my-app-1.0.0.war ← 可部署到 Tomcat 的 WAR
└── my-app-1.0.0.war.original ← 原始 WAR验证(部署到 Tomcat):
cp target/my-app-1.0.0.war /path/to/tomcat/webapps/ROOT.war
/path/to/tomcat/bin/startup.sh
# 访问 http://localhost:8080/验证打包内容差异
# JAR 包含内嵌 Tomcat
jar tf target/my-app-1.0.0.jar | grep -i "tomcat-embed" | head -5
# 输出:BOOT-INF/lib/tomcat-embed-core-10.1.x.jar ...
# WAR 不包含内嵌 Tomcat
jar tf target/my-app-1.0.0.war | grep -i "tomcat-embed" | wc -l
# 输出:0原理深度解析
boot.layout 的三个值
WAR Layout 的双重能力
当你用 boot.layout=WAR 打包时,产物实际上既能在外部 Tomcat 运行,也能 java -jar 直接启动:
my-app-1.0.0.war
├── META-INF/
│ └── MANIFEST.MF
├── WEB-INF/
│ ├── classes/ ← 你的代码
│ ├── lib/ ← 依赖(不含内嵌 Tomcat)
│ └── lib-provided/ ← provided 范围的依赖
└── org/
└── springframework/
└── boot/
└── loader/ ← Spring Boot 启动器(支持 java -jar)这就是 Spring Boot "可执行 WAR" 的精妙设计——一个包,两种运行方式。
provided scope 的作用链
war profile 中:
spring-boot-starter-tomcat scope=provided
→ tomcat-embed-core scope=provided (传递)
→ tomcat-embed-el scope=provided (传递)
→ tomcat-embed-websocket scope=provided(传递)
结果:WAR 的 WEB-INF/lib/ 中不含任何 tomcat-embed-* JAR
→ 外部 Tomcat 提供 Servlet API 实现,不会冲突