重要提示与核心理念:
- 非首选方案: Spring Boot 的设计哲学是“应用即服务”(Application as a Service),其推荐和默认方式是使用可执行 JAR 包,内置 Tomcat/Jetty/Undertow 等 Web 服务器。这极大地简化了部署,实现了“开箱即用”。
- 外部 Tomcat 部署是例外: 这种方式主要用于必须与遗留系统集成、需要共享外部容器资源(如 JNDI 数据源)、企业强制要求使用特定外部容器(如 WebLogic, WebSphere)或已有成熟的 Tomcat 运维体系的场景。
- 目标: 本指南旨在让你在必须使用外部 Tomcat 时,能够快速、准确地完成部署。
一、核心概念
1. 为什么需要部署到外部 Tomcat?
- 企业策略: 某些企业有严格的 IT 策略,要求所有 Web 应用必须部署在统一管理的、经过安全加固的外部 Tomcat 集群上。
- 资源共享: 多个应用可以共享同一个 Tomcat 实例的线程池、连接池、JNDI 资源(如数据库连接池、JMS 队列),便于集中管理和监控。
- 运维习惯: 运维团队已经对 Tomcat 的监控、日志、调优、故障排查有成熟的经验和工具链。
- 特定功能依赖: 应用依赖 Tomcat 提供的某些特定功能或 JNDI 资源,这些资源在应用内难以配置或管理。
- 遗留系统集成: 需要与部署在同一 Tomcat 上的其他非 Spring Boot 应用进行深度集成或共享会话。
2. 部署原理
- 打包方式: 将 Spring Boot 应用打包成 WAR (Web Application Archive) 文件。
- 排除内置服务器: 必须从依赖中排除
spring-boot-starter-tomcat
,因为外部 Tomcat 会提供 Servlet 容器功能,避免冲突。 - 提供 Servlet API: 需要将
javax.servlet-api
(Spring Boot 2.x) 或jakarta.servlet-api
(Spring Boot 3.x) 作为provided
依赖,因为这些 API 由外部 Tomcat 提供。 - 启动入口 (
SpringBootServletInitializer
): 对于 Maven 项目,通常需要一个继承自SpringBootServletInitializer
的类,并重写configure
方法,告诉外部 Tomcat 如何引导 Spring Boot 应用上下文。Gradle 项目通常能自动处理。 - 部署过程: 将生成的
.war
文件复制到外部 Tomcat 的webapps/
目录下,Tomcat 会自动解压并部署(或通过 Manager 应用手动部署)。
二、操作步骤(非常详细)
前提条件
- Java 环境: 确保已安装与 Spring Boot 版本兼容的 JDK (如 JDK 8, 11, 17)。
- Maven/Gradle: 安装并配置好构建工具。
- 外部 Tomcat: 下载并安装 Apache Tomcat (例如 9.x for Spring Boot 2.x, 10.x for Spring Boot 3.x)。确保
CATALINA_HOME
环境变量设置正确。 - Spring Boot 项目: 已创建一个基本的 Spring Boot Web 项目。
详细步骤
步骤 1:修改项目配置文件
目标: 将项目从默认的 JAR 打包改为 WAR 打包,并排除内置 Tomcat。
A. Maven 项目 (pom.xml
)
修改打包方式 (Packaging):
<packaging>jar</packaging>
改为
<packaging>war</packaging>
排除
spring-boot-starter-tomcat
并添加servlet-api
: 在<dependencies>
部分找到spring-boot-starter-web
依赖。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
修改为:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!-- 排除嵌入式 Tomcat 容器 --> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <!-- 添加 Servlet API 依赖,范围为 provided --> <!-- Spring Boot 2.x 使用 javax --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <!-- Spring Boot 3.x 使用 jakarta (注意版本) --> <!-- <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <scope>provided</scope> </dependency> -->
provided
范围表示该依赖在编译和测试时需要,但在运行时由外部容器(Tomcat)提供,不会被打包进 WAR 文件。
B. Gradle 项目 (build.gradle
)
应用
war
插件: 在plugins
块中添加war
插件。plugins { id 'org.springframework.boot' version '2.7.14' // 或你的版本 id 'io.spring.dependency-management' version '1.0.15.RELEASE' id 'java' // 添加 war 插件 id 'war' }
排除
spring-boot-starter-tomcat
并添加servlet-api
: 在dependencies
块中修改。dependencies { // implementation 'org.springframework.boot:spring-boot-starter-web' // 原始 implementation('org.springframework.boot:spring-boot-starter-web') { // 排除嵌入式 Tomcat exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' } // 提供运行时依赖 (providedRuntime),由外部容器提供 // Spring Boot 2.x providedRuntime 'javax.servlet:javax.servlet-api:4.0.1' // Spring Boot 3.x // providedRuntime 'jakarta.servlet:jakarta.servlet-api:6.0.0' }
providedRuntime
是 Gradle 中war
插件提供的配置,作用等同于 Maven 的provided
范围。
步骤 2:创建 Servlet 初始化器 (仅 Maven 项目强烈推荐)
目的: 为外部 Servlet 容器提供一个入口点来启动 Spring Boot 应用上下文。
创建类文件:
- 在你的主应用包下(例如
com.example.demo
)创建一个名为ServletInitializer.java
的类。 - 路径:
src/main/java/com/example/demo/ServletInitializer.java
- 在你的主应用包下(例如
编写代码:
package com.example.demo; // 替换为你的实际包名 import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; /** * 这个类用于在外部 Servlet 容器(如 Tomcat)中引导 Spring Boot 应用。 * 它继承自 SpringBootServletInitializer。 */ public class ServletInitializer extends SpringBootServletInitializer { /** * 重写 configure 方法,指定主应用类。 * @param application SpringApplicationBuilder * @return 配置好的 SpringApplicationBuilder */ @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { // 将你的主应用类 (带有 @SpringBootApplication 注解的类) 传递给 builder return application.sources(DemoApplication.class); // 替换 DemoApplication 为你的主类名 } }
- 关键点:
DemoApplication.class
必须是你的主启动类(通常带有@SpringBootApplication
注解)。
- 关键点:
步骤 3:构建 WAR 文件
- 打开命令行: 进入你的项目根目录(
pom.xml
或build.gradle
所在目录)。 - 执行构建命令:
- Maven:
mvn clean package # 或使用包装器 ./mvnw clean package
- Gradle:
./gradlew clean build # 或 gradle clean build
- Maven:
- 验证构建结果:
- Maven: 检查
target/
目录。你应该能看到类似your-artifact-id-0.0.1-SNAPSHOT.war
的文件。 - Gradle: 检查
build/libs/
目录。你应该能看到类似your-artifact-id-0.0.1-SNAPSHOT.war
的文件。 - 检查内容 (可选): 使用
jar -tf your-app.war | grep servlet-api
确认javax.servlet-api.jar
或jakarta.servlet-api.jar
没有出现在WEB-INF/lib/
目录下(因为它应该是provided
)。
- Maven: 检查
步骤 4:部署到外部 Tomcat
启动 Tomcat (如果未运行):
- 进入 Tomcat 安装目录。
- Linux/macOS: 执行
bin/startup.sh
- Windows: 执行
bin/startup.bat
- 检查
logs/catalina.out
或logs/catalina.log
确认 Tomcat 启动成功。
部署 WAR 文件:
- 方式一:自动部署 (推荐用于测试)
- 将构建好的
.war
文件(例如demo-0.0.1-SNAPSHOT.war
)复制到 Tomcat 的webapps/
目录下。 - Tomcat 会检测到新文件,自动将其解压成一个同名的文件夹(例如
demo-0.0.1-SNAPSHOT/
),并开始部署应用。 - 观察
logs/catalina.out
或logs/localhost*.log
查看部署日志。
- 将构建好的
- 方式二:通过 Manager Web 应用 (推荐用于生产/管理)
- 确保 Tomcat 的 Manager 应用已启用(通常在
conf/tomcat-users.xml
中配置了具有manager-gui
角色的用户)。 - 访问
http://localhost:8080/manager/html
(端口可能不同)。 - 使用配置的用户名和密码登录。
- 在 "Deploy" 部分,选择你的 WAR 文件,点击 "Deploy" 按钮。
- 部署成功后,应用会出现在列表中。
- 确保 Tomcat 的 Manager 应用已启用(通常在
- 方式一:自动部署 (推荐用于测试)
验证部署:
- 检查日志: 仔细查看
logs/catalina.out
和logs/localhost_<date>.log
。寻找Started DemoApplication in X seconds
这样的成功启动日志。特别注意是否有ClassNotFoundException
或NoClassDefFoundError
。 - 访问应用:
- 如果你的主应用类是
DemoApplication
,WAR 文件名是demo-0.0.1-SNAPSHOT.war
,且没有设置context-path
,则访问:http://<tomcat-host>:<tomcat-port>/demo-0.0.1-SNAPSHOT/
- 如果你在
application.yml
中设置了server.servlet.context-path=/myapp
,则访问:http://<tomcat-host>:<tomcat-port>/myapp/
- 如果你将 WAR 文件重命名为
ROOT.war
并部署,则应用将成为默认应用,访问根路径http://<tomcat-host>:<tomcat-port>/
即可。
- 如果你的主应用类是
- 检查日志: 仔细查看
三、常见错误
java.lang.ClassNotFoundException: javax.servlet.ServletContextListener
或javax.servlet.Filter
等:- 原因:
javax.servlet-api
(或jakarta.servlet-api
) 依赖缺失或未正确设置为provided
/providedRuntime
。 - 解决: 检查
pom.xml
/build.gradle
是否添加了正确的servlet-api
依赖,且scope
是provided
(Maven) 或providedRuntime
(Gradle)。确保这个 JAR 没有被打包进 WAR 的WEB-INF/lib/
目录。
- 原因:
java.lang.NoClassDefFoundError: org/springframework/boot/SpringApplication
:- 原因: 通常不是直接原因,但可能与依赖冲突或类路径问题有关。更常见的是下一个错误。
- 解决: 检查依赖树 (
mvn dependency:tree
),确保没有版本冲突。
java.lang.IllegalStateException: Cannot load configuration class: com.example.demo.DemoApplication
:- 原因:
ServletInitializer
类中configure
方法指定的主类名错误,或者主类本身有编译问题。 - 解决: 仔细检查
ServletInitializer.java
中sources(...)
方法传入的类名是否完全正确(包名+类名)。确保主类能正常编译。
- 原因:
应用部署后返回 404 (Not Found):
- 原因 1:
ServletInitializer
类缺失 (Maven 项目)。 - 解决: 确保创建了
ServletInitializer.java
并正确配置。 - 原因 2: WAR 文件名或
context-path
配置导致访问路径错误。 - 解决: 检查
application.yml
中的server.servlet.context-path
,并根据 WAR 文件名或部署方式确定正确的访问 URL。 - 原因 3: Tomcat 未能成功部署应用。检查
webapps/
目录下是否生成了对应的解压文件夹。查看logs
目录下的日志文件。 - 解决: 查看
catalina.out
和localhost*.log
,寻找部署失败的详细错误信息。
- 原因 1:
Failed to start component [StandardEngine[Catalina].StandardHost[localhost].StandardContext[/your-app]]
:- 原因: 这是一个通用的上下文启动失败错误。必须查看详细的日志堆栈 (stack trace)。
- 解决: 在
logs/localhost_<date>.log
或logs/catalina.out
中查找更具体的错误信息,如Caused by: ...
。常见的具体原因包括缺少ServletInitializer
、依赖冲突、配置文件错误等。
Port already in use
:- 原因: Tomcat 的端口 (默认 8080) 被其他进程占用。
- 解决: 修改
conf/server.xml
中的<Connector port="8080" ... />
为其他端口,或终止占用端口的进程。
四、注意事项
ServletInitializer
是关键: 对于 Maven 项目,强烈建议创建ServletInitializer
类。虽然某些简单场景可能不强制,但它是确保兼容性和正确启动的最佳实践。Gradle 项目通常能通过插件自动处理。provided
依赖: 这是避免ClassNotFoundException
的核心。务必确保servlet-api
依赖的scope
是provided
(Maven) 或providedRuntime
(Gradle),并且它不会出现在最终 WAR 的WEB-INF/lib/
目录中。- 排除
tomcat-starter
: 必须从spring-boot-starter-web
中排除spring-boot-starter-tomcat
,防止与外部容器冲突。 - Tomcat 版本兼容性: 确保你的 Spring Boot 版本与外部 Tomcat 版本兼容。
- Spring Boot 2.x -> Tomcat 9.x
- Spring Boot 3.x -> Tomcat 10.x/11.x (Jakarta EE)
context-path
优先级: Spring Boot 的server.servlet.context-path
配置会覆盖 WAR 文件名决定的上下文路径。如果设置了context-path=/api
,则无论 WAR 文件叫什么,应用都通过/api
访问。- 外部配置: 部署到外部 Tomcat 后,Spring Boot 的外部配置机制(如
--spring.config.location
)仍然有效,但需要在 Tomcat 的启动脚本(setenv.sh
/setenv.bat
)中通过JAVA_OPTS
环境变量传递。 - 日志: 应用的日志默认输出到 Tomcat 的
logs/
目录下,通常是catalina.out
或localhost_<date>.log
。你可以在application.yml
中配置logging.file.name
指向特定文件。
五、使用技巧
- 快速验证依赖: 使用
jar -tf your-app.war | grep servlet-api
检查servlet-api
JAR 是否被错误地打包进去。 - 调试部署: 在
ServletInitializer
的configure
方法中加断点(如果在 IDE 中运行 Tomcat),或在启动时加--debug
参数查看自动配置报告。 - 使用
setenv.sh
/setenv.bat
: 在 Tomcat 的bin/
目录下创建setenv.sh
(Linux/macOS) 或setenv.bat
(Windows) 文件,用于设置 JVM 参数和环境变量。setenv.sh
示例:export JAVA_OPTS="-Xms512m -Xmx1024m -Dspring.profiles.active=prod -Dlogging.level.root=INFO"
- 部署到 ROOT: 将 WAR 文件重命名为
ROOT.war
并部署,可以使应用成为 Tomcat 的默认应用,通过根路径/
访问。 - 热部署 (开发时): 在开发阶段,可以将项目以 WAR 形式添加到 IDE 的 Tomcat 服务器中,实现代码修改后的自动热部署。
- 利用 Tomcat Manager: 使用 Tomcat Manager Web 应用可以方便地进行部署、启动、停止、重新加载、卸载应用,以及查看应用状态。
六、最佳实践
- 评估必要性: 首先问自己:真的需要外部 Tomcat 吗? 绝大多数情况下,可执行 JAR 是更好的选择。
- 清晰的文档: 记录为什么选择外部 Tomcat 部署,以及相关的配置步骤。
- 自动化部署: 使用脚本或 CI/CD 工具(如 Jenkins, Ansible)自动化 WAR 构建和部署到 Tomcat 的过程。
- 配置外部化: 将数据库连接、API 密钥等敏感或环境相关的配置通过外部文件、环境变量或配置中心管理,避免硬编码。
- 健康检查: 确保
spring-boot-actuator
的/actuator/health
端点可用,并让 Tomcat 的监控工具或负载均衡器能探测它。 - 日志集中化: 将 Tomcat 和应用的日志输出到集中式日志系统(如 ELK, Splunk)。
- 安全加固: 遵循 Tomcat 安全最佳实践,如禁用 Manager 应用(生产环境)、配置 SSL、设置强密码等。
七、性能优化
- Tomcat 调优:
- 线程池: 根据负载调整
conf/server.xml
中<Connector>
的maxThreads
,minSpareThreads
。 - 连接器: 考虑使用
NIO
或APR/native
连接器以获得更好的性能。 - JVM: 为 Tomcat 进程配置合适的
-Xms
,-Xmx
,-XX:MaxMetaspaceSize
,并选择合适的 GC 算法(如 G1GC)。
- 线程池: 根据负载调整
- 应用优化:
- 减少自动配置: 使用
@EnableAutoConfiguration(exclude = {...})
或spring.autoconfigure.exclude
排除不需要的自动配置,加快启动。 - 延迟初始化: 在
application.yml
中设置spring.main.lazy-initialization=true
,延迟 Bean 的创建,直到首次使用。 - 缓存: 使用
@Cacheable
注解缓存频繁访问的数据或计算结果。
- 减少自动配置: 使用
- 监控:
- Tomcat: 使用 JMX 监控 Tomcat 的线程、内存、请求处理情况。
- 应用: 使用
spring-boot-actuator
+Micrometer
监控应用的性能指标(HTTP 延迟、JVM 内存、数据库连接池等),并集成到 Prometheus/Grafana。
- 资源优化:
- 分析依赖: 移除未使用的 Starter 和库,减小 WAR 包大小。
- 静态资源: 将 CSS, JS, 图片等静态资源放在 CDN 上,减轻 Tomcat 负担。
总结
部署 Spring Boot 到外部 Tomcat 是一个有特定应用场景的非主流方案。核心在于:
- 修改打包方式为
war
。 - 排除
spring-boot-starter-tomcat
依赖。 - 添加
servlet-api
依赖并设置为provided
/providedRuntime
。 - (Maven) 创建
ServletInitializer
类。 - 构建 WAR 并部署到 Tomcat 的
webapps
目录或通过 Manager 部署。
再次强调:除非有明确且必要的理由,否则请优先选择可执行 JAR 包进行部署。 它更简单、更可靠、更符合云原生和微服务架构的趋势。