Featured image of post Spring Boot 项目打包:War 与 Jar 的区别和注意事项

Spring Boot 项目打包:War 与 Jar 的区别和注意事项

Spring Boot 默认推荐 jar 但很多老项目还是 war,本文把两种打包方式的差异、踩坑点和切换姿势讲清楚

写在前面

新人接触 Spring Boot 时常困惑一个问题——明明同样是 Java Web 项目,为什么有的项目用 mvn package 出来是个 jar,有的项目却出来一个 war,部署方式还截然不同?

答案要从 Spring Boot 的设计哲学说起。Spring Boot 提倡"内嵌容器、可执行 jar"的方式——一个 java -jar app.jar 就跑起来,再也不用 Tomcat 这样的外部容器。但传统的 Java EE 时代,war 包 + 外部容器才是标准玩法。Spring Boot 也兼容这种做法,让老项目能平滑迁移。

这两种打包方式选错了,轻则启动姿势别扭,重则配置失效、监控失灵。本文把它们的区别、坑、和实操要点一次讲清楚。


War 与 Jar 的本质区别

Jar 包War 包
用途通用 Java 库、可执行应用Java Web 应用归档(Web Application Archive)
入口META-INF/MANIFEST.MF 的 Main-ClassWEB-INF/web.xmlServletContainerInitializer
部署java -jar app.jar拷到 Tomcat/Jetty 的 webapps 目录
容器内嵌(Tomcat/Jetty/Undertow)外部
静态资源BOOT-INF/classes/static项目根目录或 webapp
类加载顺序由 Spring Boot Loader 控制由外部容器控制

最关键一句话——jar 把容器装进了应用,war 把应用装进了容器。这条决定了两者后续所有差异。


Spring Boot 默认推荐 Jar,原因是什么

Spring Boot 默认 jar 不是凭感觉,是经过权衡的。

Jar 的好处:

  • 零运维:不需要装 Tomcat,不需要管 server.xml,部署=拷贝 jar 包
  • 环境一致:开发、测试、生产用的是同一个内嵌 Tomcat,行为一致
  • 容器版本可控:升级 Tomcat 只是改 pom 依赖
  • 天然适合容器化:一个 jar 就是一个 Docker 镜像的核心
  • 每个应用独立运行:互不影响,Crash 隔离

War 的传统优势在云原生时代逐渐弱化:

  • 原本"一个 Tomcat 跑多个应用"的模式,被 Kubernetes 一个 Pod 一个应用替代了
  • 原本"运维统一管 Tomcat"的优势,被容器编排取代了
  • 原本"war 包小,复用容器"的优势,对几百 MB 起步的 Spring 应用根本不算优势

所以现在的工程默认建议:除非有明确历史包袱或合规要求,新项目一律 jar。


怎么打 Jar 包

Spring Boot 项目默认就是 jar。pom.xml 里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<packaging>jar</packaging>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

spring-boot-maven-pluginrepackage 目标会把项目打成"可执行 jar"——里面包含一个特殊的 BOOT-INF/lib/ 装所有依赖,外层有 Spring Boot 自己的 JarLauncher

1
2
3
4
5
6
7
8
app.jar
├── META-INF/
   └── MANIFEST.MF       Main-Class: org.springframework.boot.loader.JarLauncher
├── org/springframework/boot/loader/    Spring Boot 启动类加载器
└── BOOT-INF/
    ├── classes/          你的代码
    ├── lib/              所有第三方依赖 jar
    └── classpath.idx

启动方式:

1
2
java -jar app.jar
java -Xms512m -Xmx2g -jar app.jar --spring.profiles.active=prod

怎么打 War 包

把同一个项目改成 war 输出,需要四步:

1. 改 packaging

1
<packaging>war</packaging>

2. 排除内嵌 Tomcat

外部容器已经提供了 Servlet 容器,再带一份会冲突:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>   <!-- 编译用,不打进 war -->
</dependency>

3. 启动类继承 SpringBootServletInitializer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@SpringBootApplication
public class Application extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(Application.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

SpringBootServletInitializer 会让 war 既能 java -jar 跑,也能丢进 Tomcat 跑。保留 main 方法是个好习惯——本地调试还是 java -jar 最方便。

4. 部署

把生成的 app.war 拷贝到 $TOMCAT_HOME/webapps/ 下,Tomcat 自动解压并启动:

1
http://host:port/app/...   ← context path 默认是 war 包名

七个最常见的 War 部署坑

坑 1:context path 不一致

java -jar 启动时,Spring Boot 默认 context path 是 /;war 部署到 Tomcat 时,context path 默认是 war 文件名。前端写死接口路径的话,部署完所有请求 404。

对策:要么把 war 重命名为 ROOT.war(context path = /),要么前端通过相对路径拼接。

坑 2:内嵌 Tomcat 没排干净

如果忘了 <scope>provided</scope>,war 里会带一份 Tomcat。在外部 Tomcat 里跑会出现 ClassLoader 冲突——NoSuchMethodErrorLinkageError 各种奇葩报错。

对策:用 mvn dependency:tree 检查 war 里没有 tomcat-embed-*

坑 3:server.port 配置失效

application.yml 里的 server.port: 8081 在 war 部署里完全无效——端口由外部 Tomcat 决定。同理 server.servlet.context-path 也无效。

对策:明确知道这些"server.*“配置只对内嵌容器生效。

坑 4:@PropertySource 找不到外部配置

java -jar 时 Spring Boot 会自动加载 jar 同级目录的 application.yml 覆盖内置;war 部署时没有"jar 同级目录"这个概念,外部配置要显式放在 Tomcat 的 lib 或者通过 -Dspring.config.location= 指定。

对策

1
2
# 在 Tomcat 启动参数里加
-Dspring.config.location=file:/etc/myapp/application.yml

坑 5:日志文件路径混乱

Spring Boot 的 logging.file.name=app.log 是相对当前工作目录的,war 部署时这个目录是 Tomcat 的 bin/,不是你以为的项目目录。

对策:日志路径永远写绝对路径

坑 6:JSP 默认不支持

Spring Boot 的可执行 jar 不支持 JSP(这也是为什么老项目改造时会被卡)。war 部署时虽然支持,但前提是:

1
2
3
4
5
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>

对策:新项目用 Thymeleaf / Freemarker / 前后端分离,远离 JSP。

坑 7:Filter / Listener 触发顺序

外部 Tomcat 加载 war 时,@WebFilter 等注解的扫描机制和 Spring Boot 自带容器有差异。某些 Filter 可能比 Spring 容器还早初始化,导致 @Autowirednull

对策:用 FilterRegistrationBean 显式注册 Filter,由 Spring 控制生命周期。


镜像化时代的最佳实践

如果你的项目要打 Docker 镜像,强烈推荐用 jar

1
2
3
FROM eclipse-temurin:17-jre
COPY target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

简单、清晰、可复制。war + Tomcat 的镜像虽然也能跑,但要装 Tomcat、改 server.xml、暴露端口,且镜像体积更大、启动更慢。

进一步的优化是用 Spring Boot 的 Layered Jar 把不变的依赖和频繁变化的业务代码分层,加速 Docker 镜像构建:

1
2
3
4
5
6
7
8
9
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <layers>
            <enabled>true</enabled>
        </layers>
    </configuration>
</plugin>
1
2
3
4
5
6
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/app.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
# Spring Boot 3.2+ 路径
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

版本注意:ENTRYPOINT 类路径在 Boot 3.2 处发生过迁移——Boot 3.2+ 用 org.springframework.boot.loader.launch.JarLauncher,3.1 及以下是 org.springframework.boot.loader.JarLauncher。两者写错都会启动报 ClassNotFoundException,按你实际的 Boot 版本选。


选择决策树


小结

War 与 Jar 的选择,本质上是"跟着 Java EE 时代的部署模式走,还是跟着云原生的部署模式走”。

给一句决策建议:

新项目优先用 Jar。老项目能改就改。除非有合规/历史/JSP 强约束,否则没有理由再用 War。

Spring Boot 仍然完整支持 war 是为了向后兼容,不是推荐方向。把这点拎清楚,后面所有选择就都顺了。

使用 Hugo 构建
主题 StackJimmy 设计