Loading...

文章背景图

Spring Boot Maven 多 Profile 灵活切换 JAR/WAR 打包指南

2026-07-23
8
-
- 分钟
|

背景: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:

Profile

激活方式

产物

jar

默认激活

可独立运行的 Fat JAR

war

-P war 手动指定

可部署到外部容器的 WAR

关键点:

  • <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>jar</packaging>

默认打包类型

pom.xml 根级

War profile 中 <packaging>war</packaging>

覆盖默认值

<profile> 内

<boot.layout>ZIP</boot.layout>

Spring Boot 可执行 JAR 布局

jar profile

<boot.layout>WAR</boot.layout>

标准 WAR + 可执行双重支持

war profile

spring-boot-starter-tomcat + provided

WAR 部署时不打包嵌入 Tomcat

war profile 依赖

⚠️ 为什么不在顶级写 <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 的三个值

含义

产物特征

ZIP

Spring Boot 可执行 JAR 布局

Fat JAR,包含 BOOT-INF/ 目录结构

WAR

可执行 + 可部署双重 WAR

同时支持 java -jar 和外部容器部署

NONE

无特殊布局

仅 repackage 依赖,不改变结构

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 实现,不会冲突

总结

场景

命令

产物

运行方式

本地开发 / Docker

mvn package

.jar

java -jar

传统 Tomcat 部署

mvn package -P war

.war

放入 webapps/

原创

Spring Boot Maven 多 Profile 灵活切换 JAR/WAR 打包指南

本文链接: Spring Boot Maven 多 Profile 灵活切换 JAR/WAR 打包指南

本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

文章目录