重要提示与核心理念:
- 非首选方案: 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-apiJAR 是否被错误地打包进去。 - 调试部署: 在
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 包进行部署。 它更简单、更可靠、更符合云原生和微服务架构的趋势。