Skip to content

Spring Boot 传统部署指南 🚀

概述

在现代微服务架构中,Spring Boot 以其内嵌服务器的方式革命性地简化了应用部署。然而,在企业环境中,我们仍然需要面对传统的部署方式——将应用打包成 WAR 文件并部署到外部的 Servlet 容器中。

IMPORTANT

传统部署方式主要用于企业级环境,特别是需要与现有基础设施集成或有特定运维要求的场景。

为什么需要传统部署? 🤔

核心痛点与解决方案

在 Spring Boot 出现之前,Java Web 应用的部署是一个复杂的过程:

kotlin
// 传统 Web 应用需要复杂的配置
class TraditionalWebApp {
    // 需要手动配置 web.xml
    // 需要手动管理依赖
    // 需要外部容器才能运行
    // 部署过程复杂且容易出错
}
kotlin
@SpringBootApplication
class ModernWebApp : SpringBootServletInitializer() {
    // 自动配置
    // 内嵌容器
    // 简化部署
    // 既可以传统部署,也可以独立运行
}

传统部署的应用场景

  • 企业级环境:需要统一的容器管理和监控
  • 合规要求:某些行业要求使用特定的应用服务器
  • 现有基础设施:已有成熟的 Tomcat/WebLogic 运维体系
  • 资源共享:多个应用共享同一个容器实例

创建可部署的 WAR 文件 📦

核心原理

Spring Boot 通过 SpringBootServletInitializer 类桥接了传统 Servlet 容器和 Spring Boot 应用之间的差异。这个类实现了 Servlet 3.0 的 WebApplicationInitializer 接口,让 Spring Boot 应用能够在传统容器中正确启动。

步骤一:修改主应用类

kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.boot.runApplication
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer

@SpringBootApplication
class MyApplication : SpringBootServletInitializer() {

    // 重写 configure 方法,配置应用源
    override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder {
        return application.sources(MyApplication::class.java) 
    }
}

// 保留 main 方法,支持独立运行
fun main(args: Array<String>) {
    runApplication<MyApplication>(*args)
}
kotlin
@SpringBootApplication
class MyApplication : SpringBootServletInitializer() {

    override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder {
        return customizerBuilder(application) 
    }

    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            customizerBuilder(SpringApplicationBuilder()).run(*args)
        }

        // 共享配置方法,确保 WAR 和独立运行的一致性
        private fun customizerBuilder(builder: SpringApplicationBuilder): SpringApplicationBuilder {
            return builder.sources(MyApplication::class.java)
                .bannerMode(Banner.Mode.OFF) 
                .profiles("production") 
        }
    }
}

TIP

通过共享配置方法,可以确保应用在 WAR 部署和独立运行时具有相同的行为。

步骤二:修改构建配置

Maven 配置

xml
<!-- 修改打包类型为 war -->
<packaging>war</packaging>

<dependencies>
    <!-- 其他依赖 -->

    <!-- 将内嵌容器标记为 provided -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope> 
    </dependency>
</dependencies>

Gradle 配置

kotlin
// 应用 war 插件
apply plugin: 'war'

dependencies {
    // 其他依赖

    // 使用 providedRuntime 而不是 compileOnly
    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
}

WARNING

在 Gradle 中,必须使用 providedRuntime 而不是 compileOnly,否则集成测试会失败,因为测试类路径中缺少必要的依赖。

关键概念解析

provided 作用域的重要性

转换现有应用到 Spring Boot 🔄

转换策略

将传统 Spring 应用转换为 Spring Boot 应用是一个渐进的过程:

第一步:基础转换

kotlin
// 传统的 ApplicationContext 配置
class TraditionalConfig {
    @Bean
    fun dataSource(): DataSource {
        // 手动配置数据源
        val dataSource = BasicDataSource()
        dataSource.url = "jdbc:mysql://localhost/mydb"
        dataSource.username = "user"
        dataSource.password = "password"
        return dataSource
    }

    @Bean
    fun transactionManager(): PlatformTransactionManager {
        // 手动配置事务管理器
        return DataSourceTransactionManager(dataSource())
    }
}
kotlin
@SpringBootApplication
class ModernApplication : SpringBootServletInitializer() {

    override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder {
        return application.sources(ModernApplication::class.java)
    }
}

// application.yml 中的配置
/*
spring:
  datasource:
    url: jdbc:mysql://localhost/mydb
    username: user
    password: password
  jpa:
    hibernate:
      ddl-auto: update
*/

第二步:迁移静态资源

kotlin
// 静态资源位置迁移
/*
传统位置:
├── src/main/webapp/
│   ├── css/
│   ├── js/
│   └── images/

Spring Boot 位置:
├── src/main/resources/
│   ├── static/          // 静态资源
│   ├── templates/       // 模板文件
│   └── public/          // 公共资源
*/

第三步:配置迁移

xml
<!-- 传统的 web.xml 配置 -->
<web-app>
    <servlet>
        <servlet-name>myServlet</servlet-name>
        <servlet-class>com.example.MyServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>myServlet</servlet-name>
        <url-pattern>/api/*</url-pattern>
    </servlet-mapping>
</web-app>
kotlin
@Configuration
class ServletConfig {

    @Bean
    fun myServlet(): ServletRegistrationBean<MyServlet> {
        return ServletRegistrationBean(MyServlet(), "/api/*") 
    }

    @Bean
    fun myFilter(): FilterRegistrationBean<MyFilter> {
        val registration = FilterRegistrationBean<MyFilter>()
        registration.filter = MyFilter()
        registration.urlPatterns = listOf("/api/*") 
        return registration
    }
}

转换最佳实践

NOTE

转换过程中的关键原则:

  • 渐进式迁移:逐步替换配置,而不是一次性重写
  • 保持兼容性:确保现有功能不受影响
  • 测试驱动:每个迁移步骤都要有对应的测试

WebLogic 部署特殊处理 🏢

WebLogic 的特殊要求

WebLogic 作为企业级应用服务器,有其特殊的类加载机制和要求:

kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer
import org.springframework.web.WebApplicationInitializer

@SpringBootApplication
class MyApplication : SpringBootServletInitializer(), WebApplicationInitializer {
    // 必须直接实现 WebApplicationInitializer 接口
    // 这是 WebLogic 的特殊要求
}

解决日志冲突

WebLogic 预装了自己的日志实现,需要通过配置文件告诉它优先使用应用中的版本:

WEB-INF/weblogic.xml 配置
xml
<?xml version="1.0" encoding="UTF-8"?>
<wls:weblogic-web-app
    xmlns:wls="http://xmlns.oracle.com/weblogic/weblogic-web-app"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
        https://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd
        http://xmlns.oracle.com/weblogic/weblogic-web-app
        https://xmlns.oracle.com/weblogic/weblogic-web-app/1.4/weblogic-web-app.xsd">
    <wls:container-descriptor>
        <wls:prefer-application-packages>
            <wls:package-name>org.slf4j</wls:package-name>
        </wls:prefer-application-packages>
    </wls:container-descriptor>
</wls:weblogic-web-app>

实战示例:完整的部署流程 💼

让我们通过一个完整的示例来演示整个部署过程:

示例应用结构

kotlin
@SpringBootApplication
@RestController
class ECommerceApplication : SpringBootServletInitializer() {

    override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder {
        return application.sources(ECommerceApplication::class.java)
    }

    @GetMapping("/api/products")
    fun getProducts(): List<Product> {
        return listOf(
            Product(1, "Laptop", 999.99),
            Product(2, "Phone", 699.99)
        )
    }

    @GetMapping("/health")
    fun health(): Map<String, String> {
        return mapOf("status" to "UP", "timestamp" to LocalDateTime.now().toString())
    }
}

data class Product(val id: Long, val name: String, val price: Double)

fun main(args: Array<String>) {
    runApplication<ECommerceApplication>(*args)
}

构建配置

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>ecommerce-app</artifactId>
    <version>1.0.0</version>
    <packaging>war</packaging> 

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope> 
        </dependency>
    </dependencies>
</project>
kotlin
plugins {
    kotlin("jvm") version "1.9.20"
    kotlin("plugin.spring") version "1.9.20"
    id("org.springframework.boot") version "3.2.0"
    id("io.spring.dependency-management") version "1.1.4"
    war 
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    providedRuntime("org.springframework.boot:spring-boot-starter-tomcat") 
}

部署验证

bash
# 构建 WAR 文件
./mvnw clean package

# 部署到 Tomcat
cp target/ecommerce-app-1.0.0.war /opt/tomcat/webapps/

# 验证部署
curl http://localhost:8080/ecommerce-app/api/products
curl http://localhost:8080/ecommerce-app/health

常见问题与解决方案 🔧

问题一:类路径冲突

> **症状**:应用启动时出现 `ClassNotFoundException` 或版本冲突错误

解决方案

kotlin
// 在 application.yml 中配置
spring:
  jpa:
    database-platform: org.hibernate.dialect.MySQL8Dialect
    show-sql: false
  main:
    allow-bean-definition-overriding: true

问题二:静态资源访问问题

> **症状**:CSS、JS 文件无法正确加载

解决方案

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/") 
            .setCachePeriod(3600)
    }
}

问题三:上下文路径问题

kotlin
// 在 application.yml 中配置
server:
  servlet:
    context-path: /myapp 

总结 📝

Spring Boot 的传统部署方式完美地平衡了现代开发的便利性和企业环境的要求。通过 SpringBootServletInitializer,我们可以:

保持开发便利性:继续享受 Spring Boot 的自动配置和开发体验
满足企业要求:符合传统部署和运维流程
灵活部署:同一个应用既可以独立运行,也可以部署到容器
平滑迁移:为现有应用提供渐进式的现代化路径

IMPORTANT

记住,传统部署不是倒退,而是在特定场景下的最佳选择。选择合适的部署方式,让技术更好地服务于业务需求。


希望这份指南能帮助你在 Spring Boot 的传统部署之路上走得更加顺畅! 🚀