Appearance
Spring Boot 开发时服务支持 🚀
什么是开发时服务?
在日常开发中,我们的应用往往需要依赖各种外部服务,比如数据库、消息队列、缓存等。传统的做法是在本地安装这些服务,但这样会带来很多问题:
- 环境配置复杂:需要在每个开发者的机器上安装和配置各种服务
- 版本不一致:不同开发者使用的服务版本可能不同,导致环境差异
- 资源占用:本地运行多个服务会消耗大量系统资源
- 清理困难:开发完成后,这些服务可能继续占用系统资源
NOTE
开发时服务(Development-time Services)是 Spring Boot 提供的一种解决方案,它可以在开发期间自动管理外部依赖服务,仅在开发时使用,部署时会被禁用。
Spring Boot 目前支持两种开发时服务:
- Docker Compose 支持:通过 Docker 容器管理服务
- Testcontainers 支持:使用 Java 代码定义和管理容器
Docker Compose 支持 🐳
核心理念与价值
Docker Compose 是一个用于定义和管理多容器应用的工具。Spring Boot 的 Docker Compose 支持让开发者能够:
- 一键启动依赖服务:应用启动时自动启动所需的外部服务
- 自动服务发现:无需手动配置连接信息,Spring Boot 会自动发现并连接服务
- 生命周期管理:应用关闭时自动停止相关服务
快速开始
1. 添加依赖
kotlin
dependencies {
// 开发时依赖,不会被打包到生产环境
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
}
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. 创建 compose.yml 文件
在项目根目录创建 compose.yml
文件:
yaml
services:
# PostgreSQL 数据库
postgres:
image: "postgres:15"
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- "5432"
# Redis 缓存
redis:
image: "redis:7.0"
ports:
- "6379"
# RabbitMQ 消息队列
rabbitmq:
image: "rabbitmq:3-management"
environment:
RABBITMQ_DEFAULT_USER: admin
RABBITMQ_DEFAULT_PASS: admin
ports:
- "5672"
- "15672"
TIP
注意端口配置使用 '5432'
而不是 '5432:5432'
,这样 Docker 会自动分配本地端口,避免端口冲突。
3. 创建应用配置
kotlin
@SpringBootApplication
class MyApplication
fun main(args: Array<String>) {
runApplication<MyApplication>(*args)
}
4. 使用数据库服务
kotlin
@Entity
@Table(name = "users")
data class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
@Column(nullable = false)
val username: String,
@Column(nullable = false)
val email: String
)
@Repository
interface UserRepository : JpaRepository<User, Long> {
fun findByUsername(username: String): User?
}
@Service
class UserService(
private val userRepository: UserRepository,
private val redisTemplate: RedisTemplate<String, Any>
) {
fun createUser(username: String, email: String): User {
val user = User(username = username, email = email)
val savedUser = userRepository.save(user)
// 使用 Redis 缓存用户信息
redisTemplate.opsForValue().set("user:${savedUser.id}", savedUser)
return savedUser
}
fun getUserById(id: Long): User? {
// 先从缓存获取
val cachedUser = redisTemplate.opsForValue().get("user:$id") as? User
if (cachedUser != null) {
return cachedUser
}
// 缓存未命中,从数据库获取
val user = userRepository.findById(id).orElse(null)
if (user != null) {
redisTemplate.opsForValue().set("user:$id", user)
}
return user
}
}
自动服务连接
Spring Boot 会根据容器镜像名称自动创建服务连接。支持的服务连接包括:
服务类型 | 支持的镜像 | 连接详情类型 |
---|---|---|
PostgreSQL | postgres , bitnami/postgresql | JdbcConnectionDetails |
MySQL | mysql , bitnami/mysql | JdbcConnectionDetails |
Redis | redis , bitnami/redis | RedisConnectionDetails |
MongoDB | mongo , bitnami/mongodb | MongoConnectionDetails |
RabbitMQ | rabbitmq , bitnami/rabbitmq | RabbitConnectionDetails |
Elasticsearch | elasticsearch , bitnami/elasticsearch | ElasticsearchConnectionDetails |
IMPORTANT
这些连接详情会自动覆盖 application.yml
中的相关配置,确保应用连接到容器中的服务。
高级配置
自定义镜像支持
如果使用自定义镜像,可以通过标签指定服务类型:
yaml
services:
my-custom-redis:
image: "mycompany/custom-redis:latest"
ports:
- "6379"
labels:
org.springframework.boot.service-connection: redis
跳过特定容器
某些容器可能不需要服务连接:
yaml
services:
monitoring:
image: "prometheus/prometheus"
ports:
- "9090"
labels:
org.springframework.boot.ignore: true
生命周期管理
yaml
spring:
docker:
compose:
lifecycle-management: start-and-stop
start:
command: start # 使用 docker compose start
stop:
command: down # 使用 docker compose down
timeout: 1m
properties
spring.docker.compose.lifecycle-management=start-and-stop
spring.docker.compose.start.command=start
spring.docker.compose.stop.command=down
spring.docker.compose.stop.timeout=1m
生命周期管理选项:
none
:不启动或停止 Docker Composestart-only
:仅启动服务,应用关闭时不停止start-and-stop
:启动和停止服务(默认)
容器就绪检查
yaml
services:
postgres:
image: "postgres:15"
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- "5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 10s
timeout: 5s
retries: 5
如果没有配置 healthcheck
,Spring Boot 会通过 TCP 连接检查服务是否就绪。
Testcontainers 支持 🧪
核心理念与价值
Testcontainers 允许使用 Java 代码定义和管理容器,相比 Docker Compose 有以下优势:
- 类型安全:使用 Java 代码配置,编译时检查错误
- 动态配置:可以根据运行时条件动态创建容器
- 更好的 IDE 支持:代码补全、重构等功能
- 与测试集成:同一套容器配置可用于开发和测试
快速开始
1. 添加依赖
kotlin
dependencies {
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.testcontainers:postgresql")
testImplementation("org.testcontainers:redis")
}
2. 创建测试启动类
在 src/test/kotlin
目录下创建测试启动类:
kotlin
// src/test/kotlin/com/example/TestMyApplication.kt
import org.springframework.boot.SpringApplication
class TestMyApplication {
companion object {
@JvmStatic
fun main(args: Array<String>) {
SpringApplication
.from(MyApplication::main)
.with(MyContainersConfiguration::class.java)
.run(*args)
}
}
}
3. 配置容器
kotlin
// src/test/kotlin/com/example/MyContainersConfiguration.kt
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.springframework.context.annotation.Bean
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.containers.GenericContainer
import org.testcontainers.utility.DockerImageName
@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {
@Bean
@ServiceConnection
fun postgresContainer(): PostgreSQLContainer<*> {
return PostgreSQLContainer(DockerImageName.parse("postgres:15"))
.withDatabaseName("myapp")
.withUsername("user")
.withPassword("password")
}
@Bean
@ServiceConnection
fun redisContainer(): GenericContainer<*> {
return GenericContainer(DockerImageName.parse("redis:7.0"))
.withExposedPorts(6379)
}
}
4. 启动开发环境
运行 TestMyApplication
的 main
方法,或使用命令:
bash
# Maven
./mvnw spring-boot:test-run
# Gradle
./gradlew bootTestRun
高级特性
动态属性配置
对于不支持 @ServiceConnection
的服务,可以使用动态属性:
kotlin
@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {
@Bean
fun mongoContainer(): MongoDBContainer {
return MongoDBContainer(DockerImageName.parse("mongo:5.0"))
}
@Bean
fun mongoProperties(container: MongoDBContainer): DynamicPropertyRegistrar {
return DynamicPropertyRegistrar { properties ->
properties.add("spring.data.mongodb.host", container::getHost)
properties.add("spring.data.mongodb.port", container::getFirstMappedPort)
}
}
}
导入容器声明类
如果已有静态容器定义,可以直接导入:
kotlin
// 容器声明接口
interface MyContainers {
companion object {
@Container
@ServiceConnection
val mongoContainer = MongoDBContainer(DockerImageName.parse("mongo:5.0"))
@Container
@ServiceConnection
val neo4jContainer = Neo4jContainer<Nothing>(DockerImageName.parse("neo4j:5"))
}
}
// 配置类
@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers::class)
class MyContainersConfiguration
与 DevTools 集成
使用 @RestartScope
确保容器在应用重启时保持状态:
kotlin
@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {
@Bean
@RestartScope
@ServiceConnection
fun mongoContainer(): MongoDBContainer {
return MongoDBContainer(DockerImageName.parse("mongo:5.0"))
}
}
WARNING
使用 Gradle 时,需要将 spring-boot-devtools
的配置从 developmentOnly
改为 testAndDevelopmentOnly
。
实际应用场景 💡
场景一:微服务开发
在微服务架构中,一个服务可能依赖多个外部服务:
kotlin
// 电商订单服务的开发环境配置
@TestConfiguration(proxyBeanMethods = false)
class OrderServiceContainers {
@Bean
@ServiceConnection
fun postgresContainer(): PostgreSQLContainer<*> {
return PostgreSQLContainer(DockerImageName.parse("postgres:15"))
.withDatabaseName("orders")
}
@Bean
@ServiceConnection
fun redisContainer(): GenericContainer<*> {
return GenericContainer(DockerImageName.parse("redis:7.0"))
.withExposedPorts(6379)
}
@Bean
@ServiceConnection
fun rabbitmqContainer(): RabbitMQContainer {
return RabbitMQContainer(DockerImageName.parse("rabbitmq:3-management"))
}
@Bean
@ServiceConnection
fun elasticsearchContainer(): ElasticsearchContainer {
return ElasticsearchContainer(DockerImageName.parse("elasticsearch:8.11.0"))
.withEnv("discovery.type", "single-node")
.withEnv("xpack.security.enabled", "false")
}
}
场景二:数据库迁移测试
kotlin
@Service
class DatabaseMigrationService(
private val jdbcTemplate: JdbcTemplate
) {
fun migrateData() {
// 执行数据迁移逻辑
jdbcTemplate.execute("""
CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""".trimIndent())
// 插入测试数据
jdbcTemplate.update(
"INSERT INTO products (name, price) VALUES (?, ?)",
"测试商品", 99.99
)
}
}
最佳实践 ⭐
1. 选择合适的方案
TIP
选择建议
- Docker Compose:适合团队已有 Docker Compose 经验,或需要复杂的服务编排
- Testcontainers:适合 Java 开发者,需要动态配置或与测试集成
2. 性能优化
kotlin
@TestConfiguration(proxyBeanMethods = false)
class OptimizedContainersConfiguration {
@Bean
@ServiceConnection
fun postgresContainer(): PostgreSQLContainer<*> {
return PostgreSQLContainer(DockerImageName.parse("postgres:15-alpine"))
.withDatabaseName("myapp")
.withUsername("user")
.withPassword("password")
.withReuse(true) // 重用容器
}
}
3. 环境隔离
yaml
# application-dev.yml
spring:
docker:
compose:
profiles:
active: "development"
# compose.yml
services:
postgres:
image: "postgres:15"
profiles: ["development"]
environment:
POSTGRES_DB: myapp_dev
postgres-test:
image: "postgres:15"
profiles: ["test"]
environment:
POSTGRES_DB: myapp_test
4. 资源管理
kotlin
// 并行启动容器以提高启动速度
// application.yml
spring:
testcontainers:
beans:
startup: parallel # [!code highlight]
总结 🎉
Spring Boot 的开发时服务支持为现代应用开发提供了强大的工具:
- 简化环境配置:无需手动安装和配置外部服务
- 提高开发效率:一键启动完整的开发环境
- 保证环境一致性:所有开发者使用相同的服务版本
- 降低资源消耗:按需启动和停止服务
无论选择 Docker Compose 还是 Testcontainers,都能显著改善开发体验。建议根据团队技术栈和项目需求选择合适的方案,让开发变得更加高效和愉快! 🚀