重要提示与核心理念:

  • 非首选方案: 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 应用手动部署)。

二、操作步骤(非常详细)

前提条件

  1. Java 环境: 确保已安装与 Spring Boot 版本兼容的 JDK (如 JDK 8, 11, 17)。
  2. Maven/Gradle: 安装并配置好构建工具。
  3. 外部 Tomcat: 下载并安装 Apache Tomcat (例如 9.x for Spring Boot 2.x, 10.x for Spring Boot 3.x)。确保 CATALINA_HOME 环境变量设置正确。
  4. Spring Boot 项目: 已创建一个基本的 Spring Boot Web 项目。

详细步骤

步骤 1:修改项目配置文件

目标: 将项目从默认的 JAR 打包改为 WAR 打包,并排除内置 Tomcat。

A. Maven 项目 (pom.xml)
  1. 修改打包方式 (Packaging):

    <packaging>jar</packaging>
    

    改为

    <packaging>war</packaging>
    
  2. 排除 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)
  1. 应用 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'
    }
    
  2. 排除 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 应用上下文。

  1. 创建类文件:

    • 在你的主应用包下(例如 com.example.demo)创建一个名为 ServletInitializer.java 的类。
    • 路径:src/main/java/com/example/demo/ServletInitializer.java
  2. 编写代码:

    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 文件

  1. 打开命令行: 进入你的项目根目录(pom.xmlbuild.gradle 所在目录)。
  2. 执行构建命令:
    • Maven:
      mvn clean package
      # 或使用包装器
      ./mvnw clean package
      
    • Gradle:
      ./gradlew clean build
      # 或
      gradle clean build
      
  3. 验证构建结果:
    • 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.jarjakarta.servlet-api.jar 没有出现在 WEB-INF/lib/ 目录下(因为它应该是 provided)。

步骤 4:部署到外部 Tomcat

  1. 启动 Tomcat (如果未运行):

    • 进入 Tomcat 安装目录。
    • Linux/macOS: 执行 bin/startup.sh
    • Windows: 执行 bin/startup.bat
    • 检查 logs/catalina.outlogs/catalina.log 确认 Tomcat 启动成功。
  2. 部署 WAR 文件:

    • 方式一:自动部署 (推荐用于测试)
      1. 将构建好的 .war 文件(例如 demo-0.0.1-SNAPSHOT.war复制到 Tomcat 的 webapps/ 目录下。
      2. Tomcat 会检测到新文件,自动将其解压成一个同名的文件夹(例如 demo-0.0.1-SNAPSHOT/),并开始部署应用。
      3. 观察 logs/catalina.outlogs/localhost*.log 查看部署日志。
    • 方式二:通过 Manager Web 应用 (推荐用于生产/管理)
      1. 确保 Tomcat 的 Manager 应用已启用(通常在 conf/tomcat-users.xml 中配置了具有 manager-gui 角色的用户)。
      2. 访问 http://localhost:8080/manager/html (端口可能不同)。
      3. 使用配置的用户名和密码登录。
      4. 在 "Deploy" 部分,选择你的 WAR 文件,点击 "Deploy" 按钮。
      5. 部署成功后,应用会出现在列表中。
  3. 验证部署:

    • 检查日志: 仔细查看 logs/catalina.outlogs/localhost_<date>.log。寻找 Started DemoApplication in X seconds 这样的成功启动日志。特别注意是否有 ClassNotFoundExceptionNoClassDefFoundError
    • 访问应用:
      • 如果你的主应用类是 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>/ 即可。

三、常见错误

  1. java.lang.ClassNotFoundException: javax.servlet.ServletContextListenerjavax.servlet.Filter 等:

    • 原因: javax.servlet-api (或 jakarta.servlet-api) 依赖缺失或未正确设置为 provided/providedRuntime
    • 解决: 检查 pom.xml/build.gradle 是否添加了正确的 servlet-api 依赖,且 scopeprovided (Maven) 或 providedRuntime (Gradle)。确保这个 JAR 没有被打包进 WAR 的 WEB-INF/lib/ 目录。
  2. java.lang.NoClassDefFoundError: org/springframework/boot/SpringApplication:

    • 原因: 通常不是直接原因,但可能与依赖冲突或类路径问题有关。更常见的是下一个错误。
    • 解决: 检查依赖树 (mvn dependency:tree),确保没有版本冲突。
  3. java.lang.IllegalStateException: Cannot load configuration class: com.example.demo.DemoApplication:

    • 原因: ServletInitializer 类中 configure 方法指定的主类名错误,或者主类本身有编译问题。
    • 解决: 仔细检查 ServletInitializer.javasources(...) 方法传入的类名是否完全正确(包名+类名)。确保主类能正常编译。
  4. 应用部署后返回 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.outlocalhost*.log,寻找部署失败的详细错误信息。
  5. Failed to start component [StandardEngine[Catalina].StandardHost[localhost].StandardContext[/your-app]]:

    • 原因: 这是一个通用的上下文启动失败错误。必须查看详细的日志堆栈 (stack trace)
    • 解决:logs/localhost_<date>.loglogs/catalina.out 中查找更具体的错误信息,如 Caused by: ...。常见的具体原因包括缺少 ServletInitializer、依赖冲突、配置文件错误等。
  6. Port already in use:

    • 原因: Tomcat 的端口 (默认 8080) 被其他进程占用。
    • 解决: 修改 conf/server.xml 中的 <Connector port="8080" ... /> 为其他端口,或终止占用端口的进程。

四、注意事项

  1. ServletInitializer 是关键: 对于 Maven 项目,强烈建议创建 ServletInitializer 类。虽然某些简单场景可能不强制,但它是确保兼容性和正确启动的最佳实践。Gradle 项目通常能通过插件自动处理。
  2. provided 依赖: 这是避免 ClassNotFoundException核心。务必确保 servlet-api 依赖的 scopeprovided (Maven) 或 providedRuntime (Gradle),并且它不会出现在最终 WAR 的 WEB-INF/lib/ 目录中。
  3. 排除 tomcat-starter: 必须从 spring-boot-starter-web 中排除 spring-boot-starter-tomcat,防止与外部容器冲突。
  4. Tomcat 版本兼容性: 确保你的 Spring Boot 版本与外部 Tomcat 版本兼容。
    • Spring Boot 2.x -> Tomcat 9.x
    • Spring Boot 3.x -> Tomcat 10.x/11.x (Jakarta EE)
  5. context-path 优先级: Spring Boot 的 server.servlet.context-path 配置会覆盖 WAR 文件名决定的上下文路径。如果设置了 context-path=/api,则无论 WAR 文件叫什么,应用都通过 /api 访问。
  6. 外部配置: 部署到外部 Tomcat 后,Spring Boot 的外部配置机制(如 --spring.config.location仍然有效,但需要在 Tomcat 的启动脚本(setenv.sh/setenv.bat)中通过 JAVA_OPTS 环境变量传递。
  7. 日志: 应用的日志默认输出到 Tomcat 的 logs/ 目录下,通常是 catalina.outlocalhost_<date>.log。你可以在 application.yml 中配置 logging.file.name 指向特定文件。

五、使用技巧

  1. 快速验证依赖: 使用 jar -tf your-app.war | grep servlet-api 检查 servlet-api JAR 是否被错误地打包进去。
  2. 调试部署:ServletInitializerconfigure 方法中加断点(如果在 IDE 中运行 Tomcat),或在启动时加 --debug 参数查看自动配置报告。
  3. 使用 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"
      
  4. 部署到 ROOT: 将 WAR 文件重命名为 ROOT.war 并部署,可以使应用成为 Tomcat 的默认应用,通过根路径 / 访问。
  5. 热部署 (开发时): 在开发阶段,可以将项目以 WAR 形式添加到 IDE 的 Tomcat 服务器中,实现代码修改后的自动热部署。
  6. 利用 Tomcat Manager: 使用 Tomcat Manager Web 应用可以方便地进行部署、启动、停止、重新加载、卸载应用,以及查看应用状态。

六、最佳实践

  1. 评估必要性: 首先问自己:真的需要外部 Tomcat 吗? 绝大多数情况下,可执行 JAR 是更好的选择。
  2. 清晰的文档: 记录为什么选择外部 Tomcat 部署,以及相关的配置步骤。
  3. 自动化部署: 使用脚本或 CI/CD 工具(如 Jenkins, Ansible)自动化 WAR 构建和部署到 Tomcat 的过程。
  4. 配置外部化: 将数据库连接、API 密钥等敏感或环境相关的配置通过外部文件、环境变量或配置中心管理,避免硬编码。
  5. 健康检查: 确保 spring-boot-actuator/actuator/health 端点可用,并让 Tomcat 的监控工具或负载均衡器能探测它。
  6. 日志集中化: 将 Tomcat 和应用的日志输出到集中式日志系统(如 ELK, Splunk)。
  7. 安全加固: 遵循 Tomcat 安全最佳实践,如禁用 Manager 应用(生产环境)、配置 SSL、设置强密码等。

七、性能优化

  1. Tomcat 调优:
    • 线程池: 根据负载调整 conf/server.xml<Connector>maxThreads, minSpareThreads
    • 连接器: 考虑使用 NIOAPR/native 连接器以获得更好的性能。
    • JVM: 为 Tomcat 进程配置合适的 -Xms, -Xmx, -XX:MaxMetaspaceSize,并选择合适的 GC 算法(如 G1GC)。
  2. 应用优化:
    • 减少自动配置: 使用 @EnableAutoConfiguration(exclude = {...})spring.autoconfigure.exclude 排除不需要的自动配置,加快启动。
    • 延迟初始化:application.yml 中设置 spring.main.lazy-initialization=true,延迟 Bean 的创建,直到首次使用。
    • 缓存: 使用 @Cacheable 注解缓存频繁访问的数据或计算结果。
  3. 监控:
    • Tomcat: 使用 JMX 监控 Tomcat 的线程、内存、请求处理情况。
    • 应用: 使用 spring-boot-actuator + Micrometer 监控应用的性能指标(HTTP 延迟、JVM 内存、数据库连接池等),并集成到 Prometheus/Grafana。
  4. 资源优化:
    • 分析依赖: 移除未使用的 Starter 和库,减小 WAR 包大小。
    • 静态资源: 将 CSS, JS, 图片等静态资源放在 CDN 上,减轻 Tomcat 负担。

总结

部署 Spring Boot 到外部 Tomcat 是一个有特定应用场景的非主流方案。核心在于:

  1. 修改打包方式为 war
  2. 排除 spring-boot-starter-tomcat 依赖。
  3. 添加 servlet-api 依赖并设置为 provided/providedRuntime
  4. (Maven) 创建 ServletInitializer 类。
  5. 构建 WAR 并部署到 Tomcat 的 webapps 目录或通过 Manager 部署。

再次强调:除非有明确且必要的理由,否则请优先选择可执行 JAR 包进行部署。 它更简单、更可靠、更符合云原生和微服务架构的趋势。