Appearance
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 的传统部署之路上走得更加顺畅! 🚀