写在前面
新人接触 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-Class | WEB-INF/web.xml 或 ServletContainerInitializer |
| 部署 | 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 里:
| |
spring-boot-maven-plugin 的 repackage 目标会把项目打成"可执行 jar"——里面包含一个特殊的 BOOT-INF/lib/ 装所有依赖,外层有 Spring Boot 自己的 JarLauncher:
| |
启动方式:
| |
怎么打 War 包
把同一个项目改成 war 输出,需要四步:
1. 改 packaging
| |
2. 排除内嵌 Tomcat
外部容器已经提供了 Servlet 容器,再带一份会冲突:
| |
3. 启动类继承 SpringBootServletInitializer
| |
SpringBootServletInitializer 会让 war 既能 java -jar 跑,也能丢进 Tomcat 跑。保留 main 方法是个好习惯——本地调试还是 java -jar 最方便。
4. 部署
把生成的 app.war 拷贝到 $TOMCAT_HOME/webapps/ 下,Tomcat 自动解压并启动:
| |
七个最常见的 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 冲突——NoSuchMethodError、LinkageError 各种奇葩报错。
对策:用 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= 指定。
对策:
| |
坑 5:日志文件路径混乱
Spring Boot 的 logging.file.name=app.log 是相对当前工作目录的,war 部署时这个目录是 Tomcat 的 bin/,不是你以为的项目目录。
对策:日志路径永远写绝对路径。
坑 6:JSP 默认不支持
Spring Boot 的可执行 jar 不支持 JSP(这也是为什么老项目改造时会被卡)。war 部署时虽然支持,但前提是:
| |
对策:新项目用 Thymeleaf / Freemarker / 前后端分离,远离 JSP。
坑 7:Filter / Listener 触发顺序
外部 Tomcat 加载 war 时,@WebFilter 等注解的扫描机制和 Spring Boot 自带容器有差异。某些 Filter 可能比 Spring 容器还早初始化,导致 @Autowired 是 null。
对策:用 FilterRegistrationBean 显式注册 Filter,由 Spring 控制生命周期。
镜像化时代的最佳实践
如果你的项目要打 Docker 镜像,强烈推荐用 jar:
| |
简单、清晰、可复制。war + Tomcat 的镜像虽然也能跑,但要装 Tomcat、改 server.xml、暴露端口,且镜像体积更大、启动更慢。
进一步的优化是用 Spring Boot 的 Layered Jar 把不变的依赖和频繁变化的业务代码分层,加速 Docker 镜像构建:
| |
| |
版本注意: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 是为了向后兼容,不是推荐方向。把这点拎清楚,后面所有选择就都顺了。