引言:一个常见的困惑
如果你是从传统的 Spring MVC 项目过渡到 Spring Boot 的开发者,很可能会有这样一个疑问:为什么以前我们总是打一个 WAR
包丢到 Tomcat 的 webapps
目录下,而现在 Spring Boot 项目直接打成一个 JAR
包,用 java -jar
命令就能启动一个 Web 应用?
这个看似简单的打包方式变化的背后,是 Spring Boot "约定大于配置" 和 "自包含" 设计理念的深刻体现。本文将深入探讨 Spring Boot 与 Tomcat 的关系,并解析这一变革为我们带来的便利与思考。
一、传统模式:WAR 包与外部 Tomcat 的"租客-酒店"关系
在 Spring Boot 诞生之前,Java Web 应用的部署方式几乎是统一的。
1.1 如何工作?
- 开发阶段 :我们在项目中编写
web.xml
、Spring 配置文件,配置 DispatcherServlet、ContextLoaderListener 等。 - 打包阶段 :使用 Maven 或 Gradle,将项目及其依赖(除了 Servlet API 这类
provided
依赖)打包成一个WAR
(Web Application Archive) 文件。 - 部署阶段 :我们需要在服务器上预先安装并启动一个 Tomcat 实例 。然后将
WAR
文件复制到 Tomcat 的webapps
目录下。 - 运行阶段 :Tomcat 监控到新的
WAR
文件,会解压它,加载其中的类和相关库,并根据web.xml
的配置来启动应用。
1.2 关系比喻:酒店与租客
- Tomcat :像一个功能齐全的酒店。它独立运行,管理着基础设施(端口 8080、线程池、JNDI 资源、生命周期管理等)。
- WAR 应用 :像一个租客。它"入住"酒店,享受酒店提供的服务,但必须遵守酒店的规则(如指定的 Servlet 版本)。
1.3 这种模式的优缺点
- 优点 :
- 资源共享:一个 Tomcat 可以部署多个应用,节省资源。
- 运维统一:运维人员可以集中管理一个 Tomcat 实例,进行监控、调优和打补丁。
- 缺点 :
- 环境依赖性强:应用的行为严重依赖外部 Tomcat 的版本和配置。"在我这儿跑得好好的,怎么到你那儿就不行了?"是常见问题。
- 部署繁琐:需要先配置环境,再部署应用,步骤较多。
- 移植性差:难以实现完美的"一次构建,到处运行"。
二、Spring Boot 模式:可执行 JAR 与嵌入式容器的"机甲-驾驶员"关系
Spring Boot 引入了一种全新的思路:为什么不把服务器(Tomcat)和应用打包在一起呢?
2.1 如何工作?
-
开发阶段 :我们引入
spring-boot-starter-web
依赖。这个 Starter 已经默默地帮我们引入了嵌入式 Tomcat 的依赖。 -
打包阶段 :Spring Boot 的 Maven/Gradle 插件会将所有依赖(包括嵌入式 Tomcat) 以及项目代码一起打包成一个可执行的、胖乎乎的(Fat)JAR 包。这个 JAR 包内有特殊的结构,知道如何启动自己。
-
部署与运行阶段 :我们不需要安装 Tomcat。只需要在目标机器上安装 JRE,然后执行一条简单的命令:
bashjava -jar my-spring-boot-app-0.0.1.jar
这条命令会启动应用内的
main
方法(通常是SpringApplication.run()
),该方法会从内部实例化并启动一个 Tomcat 实例,并将当前应用部署上去。
2.2 关系比喻:机甲与驾驶员
- 可执行 JAR :是一个配备了机甲的驾驶员 。
- 驾驶员:是你的业务逻辑代码。
- 机甲 :就是内嵌的 Tomcat,它为驾驶员提供了强大的战斗(Web 服务)能力。
两者紧密结合,形成一个独立、完整、可随时投入战斗的个体。
2.3 嵌入式容器的魔法
Spring Boot 的嵌入式支持不仅限于 Tomcat。通过简单的排除和引入依赖,你可以轻松切换容器:
xml
<!-- 默认使用 Tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 切换至 Jetty -->
<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-jetty</artifactId>
</dependency>
<!-- 切换至 Undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
这种灵活性是传统部署模式难以企及的。
2.4 这种模式的优缺点
- 优点 :
- 开发体验极佳:开箱即用,无需关心服务器配置。
- 部署极其简单:只需要有 Java 环境,一行命令即可运行。完美契合云原生和容器化(Docker)的理念。
- 环境一致性:应用在任何地方运行的表现都是一致的,彻底解决了环境差异问题。
- 高度可定制 :通过
application.properties
即可轻松配置容器参数(端口、SSL、连接池等),无需触碰server.xml
。
- 缺点 :
- 资源占用:每个应用都自带一个服务器,如果机器上部署了多个应用,可能会有多份 Tomcat 开销。
- 传统运维不适:传统的运维团队可能需要转变观念,从管理几个大的 Tomcat 实例转变为管理大量独立的应用进程。
三、对比总结:一张图看清差异
特性维度 | Spring Boot (可执行 JAR) | 传统 Spring (WAR) |
---|---|---|
容器关系 | 嵌入式 (Embedded),与应用一体 | 外部 (External),与应用分离 |
打包格式 | 可执行 Fat JAR | WAR |
启动命令 | java -jar app.jar |
$TOMCAT_HOME/bin/startup.sh |
环境依赖 | 仅需 JRE | 需预装匹配版本的 Tomcat/JEE 服务器 |
容器定制 | 应用配置文件 (application.yml ) |
服务器配置文件 (server.xml ) |
应用隔离性 | 强(进程级别隔离) | 弱(共享容器,可能相互影响) |
适用架构 | 微服务、云原生、分布式系统 | 传统单体应用、企业级应用服务器环境 |
四、拓展:Spring Boot 也能打 WAR 包?
值得注意的是,Spring Boot 也提供了打 WAR
包的能力。这通常是为了向旧世界妥协,比如部署到公司强制要求使用的 WebLogic、WebSphere 或现有的 Tomcat 集群。
当你这样做时:
- 修改
pom.xml
中的打包方式为<packaging>war</packaging>
。 - 让主应用类继承
SpringBootServletInitializer
,这是为了生成符合 Servlet 标准的引导程序。 - 打包后的
WAR
文件仍然会包含内嵌的 Tomcat 库,但在部署到外部应用服务器时,这些库会被忽略,应用会使用外部服务器提供的运行时环境。
这就好比你的机甲驾驶员住进了酒店,虽然自带机甲,但酒店规定不准开机,只能使用酒店统一的装备。
结语:理念的进化
从 WAR
到可执行 JAR
,不仅仅是打包方式的改变,更是开发、部署和运维理念的一次重大进化。它标志着Java应用从需要依赖外部环境的"重量级"应用 ,向自我满足、独立统一的"轻量级"应用的转变。
这种转变极大地降低了Spring的开发门槛,简化了部署流程,并为微服务架构的兴起奠定了坚实的技术基础。理解这两种模式背后的哲学,能帮助我们更好地理解Spring Boot的设计之美,并在不同的技术选型中做出最合适的决定。