Skip to content

Spring Boot 开发时服务支持 🚀

什么是开发时服务?

在日常开发中,我们的应用往往需要依赖各种外部服务,比如数据库、消息队列、缓存等。传统的做法是在本地安装这些服务,但这样会带来很多问题:

  • 环境配置复杂:需要在每个开发者的机器上安装和配置各种服务
  • 版本不一致:不同开发者使用的服务版本可能不同,导致环境差异
  • 资源占用:本地运行多个服务会消耗大量系统资源
  • 清理困难:开发完成后,这些服务可能继续占用系统资源

NOTE

开发时服务(Development-time Services)是 Spring Boot 提供的一种解决方案,它可以在开发期间自动管理外部依赖服务,仅在开发时使用,部署时会被禁用。

Spring Boot 目前支持两种开发时服务:

  • Docker Compose 支持:通过 Docker 容器管理服务
  • Testcontainers 支持:使用 Java 代码定义和管理容器

Docker Compose 支持 🐳

核心理念与价值

Docker Compose 是一个用于定义和管理多容器应用的工具。Spring Boot 的 Docker Compose 支持让开发者能够:

  1. 一键启动依赖服务:应用启动时自动启动所需的外部服务
  2. 自动服务发现:无需手动配置连接信息,Spring Boot 会自动发现并连接服务
  3. 生命周期管理:应用关闭时自动停止相关服务

快速开始

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 会根据容器镜像名称自动创建服务连接。支持的服务连接包括:

服务类型支持的镜像连接详情类型
PostgreSQLpostgres, bitnami/postgresqlJdbcConnectionDetails
MySQLmysql, bitnami/mysqlJdbcConnectionDetails
Redisredis, bitnami/redisRedisConnectionDetails
MongoDBmongo, bitnami/mongodbMongoConnectionDetails
RabbitMQrabbitmq, bitnami/rabbitmqRabbitConnectionDetails
Elasticsearchelasticsearch, bitnami/elasticsearchElasticsearchConnectionDetails

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 Compose
  • start-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. 启动开发环境

运行 TestMyApplicationmain 方法,或使用命令:

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,都能显著改善开发体验。建议根据团队技术栈和项目需求选择合适的方案,让开发变得更加高效和愉快! 🚀