开发时服务
开发时服务提供运行应用程序在开发过程中所需的外部依赖项。它们仅应在开发期间使用,并在应用程序部署时被禁用。
Spring Boot 提供了对两种开发时服务的支持:Docker Compose 和 Testcontainers。接下来的章节将提供有关它们的更多详细信息。
Docker Compose 支持
Docker Compose 是一种流行的技术,可用于定义和管理应用程序所需服务的多个容器。通常会在你的应用程序旁边创建一个 compose.yml 文件,用于定义和配置服务容器。
使用 Docker Compose 的典型工作流程是运行 docker compose up,在应用程序运行期间连接到已启动的服务,完成工作后运行 docker compose down。
可以将 spring-boot-docker-compose 模块包含在项目中,以提供使用 Docker Compose 操作容器的支持。将该模块依赖项添加到您的构建文件中,如下列 Maven 和 Gradle 的示例所示:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
dependencies {
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
}
当此模块作为依赖项被引入时,Spring Boot 将执行以下操作:
-
在你的工作目录中搜索
compose.yml及其他常见的 Compose 文件名 -
使用发现的
compose.yml调用docker compose up -
为每个受支持的容器创建服务连接 Bean
-
在应用程序关闭时调用
docker compose stop
如果在启动应用程序时 Docker Compose 服务已经运行,Spring Boot 将仅为每个受支持的容器创建服务连接 Bean。它不会再次调用 docker compose up,并且在应用程序关闭时也不会调用 docker compose stop。
重新打包的归档文件默认不包含 Spring Boot 的 Docker Compose 支持。如果要使用此功能,需要显式包含它。使用 Maven 插件时,请将 excludeDockerCompose 属性设置为 false。使用 Gradle 插件时,请配置任务的 classpath 以包含 developmentOnly 配置。
先决条件
你需要在你的路径中安装 docker 和 docker compose(或 docker-compose)CLI 应用程序。支持的最低 Docker Compose 版本为 2.2.0。
Service Connections
服务连接(service connection)是指到任意远程服务的连接。Spring Boot 的自动配置可以使用服务连接的详细信息,并利用这些信息建立到远程服务的连接。在此过程中,连接详细信息优先于任何与连接相关的配置属性。
使用 Spring Boot 的 Docker Compose 支持时,服务连接会建立到容器映射的端口。
Docker Compose 通常以这样的方式使用:容器内部的端口会被映射到你计算机上的临时端口。例如,Postgres 服务器可能在容器内部使用 5432 端口,但在本地被映射到一个完全不同的端口。服务连接将始终自动发现并使用本地映射的端口。
服务连接通过使用容器的镜像名称建立。目前支持以下服务连接:
| 连接详情 | 匹配于 |
|---|---|
| ActiveMQConnectionDetails | 名为 "symptoma/activemq" 或 "apache/activemq-classic" 的容器 |
| ArtemisConnectionDetails | 名为 "apache/activemq-artemis" 的容器 |
| CassandraConnectionDetails | 名为 "cassandra"、"bitnami/cassandra" 或 "bitnamilegacy/cassandra" 的容器 |
| ElasticsearchConnectionDetails | 名为 "elasticsearch"、"bitnami/elasticsearch" 或 "bitnamilegacy/elasticsearch" 的容器 |
| HazelcastConnectionDetails | 名为 "hazelcast/hazelcast" 的容器。 |
| JdbcConnectionDetails | 名为 "clickhouse/clickhouse-server"、"bitnami/clickhouse"、"bitnamilegacy/clickhouse"、"gvenzl/oracle-free"、"gvenzl/oracle-xe"、"mariadb"、"bitnami/mariadb"、"bitnamilegacy/mariadb"、"mssql/server"、"mysql"、"bitnami/mysql"、"bitnamilegacy/mysql"、"postgres"、"bitnami/postgresql" 或 "bitnamilegacy/postgresql" 的容器 |
| LdapConnectionDetails | 名为 "osixia/openldap"、"lldap/lldap" 的容器 |
| MongoConnectionDetails | 名为 "mongo"、"bitnami/mongodb" 或 "bitnamilegacy/mongodb" 的容器 |
| Neo4jConnectionDetails | 名为 "neo4j"、"bitnami/neo4j" 或 "bitnamilegacy/neo4j" 的容器 |
| OtlpLoggingConnectionDetails | 名为 "otel/opentelemetry-collector-contrib"、"grafana/otel-lgtm" 的容器 |
| OtlpMetricsConnectionDetails | 名为 "otel/opentelemetry-collector-contrib"、"grafana/otel-lgtm" 的容器 |
| OtlpTracingConnectionDetails | 名为 "otel/opentelemetry-collector-contrib"、"grafana/otel-lgtm" 的容器 |
| PulsarConnectionDetails | 名为 "apachepulsar/pulsar" 的容器 |
| R2dbcConnectionDetails | 名为 "clickhouse/clickhouse-server"、"bitnami/clickhouse"、"bitnamilegacy/clickhouse"、"gvenzl/oracle-free"、"gvenzl/oracle-xe"、"mariadb"、"bitnami/mariadb"、"bitnamilegacy/mariadb"、"mssql/server"、"mysql"、"bitnami/mysql"、"bitnamilegacy/mysql"、"postgres"、"bitnami/postgresql" 或 "bitnamilegacy/postgresql" 的容器 |
| RabbitConnectionDetails | 名为 "rabbitmq"、"bitnami/rabbitmq" 或 "bitnamilegacy/rabbitmq" 的容器 |
| RedisConnectionDetails | 名为 "redis"、"bitnami/redis"、"bitnamilegacy/redis"、"redis/redis-stack" 或 "redis/redis-stack-server" 的容器 |
| ZipkinConnectionDetails | 名为 "openzipkin/zipkin" 的容器。 |
SSL 支持
有些镜像开箱即用就启用了 SSL,或者你可能希望为容器启用 SSL 以镜像你的生产环境设置。Spring Boot 支持为受支持的服务连接进行 SSL 配置。请注意,你仍然需要自行在容器内运行的服务上启用 SSL,此功能仅在应用程序的客户端配置 SSL。
以下服务连接支持 SSL:
-
Cassandra
-
Couchbase
-
Elasticsearch
-
Kafka
-
MongoDB
-
RabbitMQ
-
Redis
要为服务启用 SSL 支持,你可以使用 service labels。
对于基于 JKS 的 keystores 和 truststores,你可以使用以下容器标签:
-
org.springframework.boot.sslbundle.jks.key.alias -
org.springframework.boot.sslbundle.jks.key.password -
org.springframework.boot.sslbundle.jks.options.ciphers -
org.springframework.boot.sslbundle.jks.options.enabled-protocols -
org.springframework.boot.sslbundle.jks.protocol -
org.springframework.boot.sslbundle.jks.keystore.type -
org.springframework.boot.sslbundle.jks.keystore.provider -
org.springframework.boot.sslbundle.jks.keystore.location -
org.springframework.boot.sslbundle.jks.keystore.password -
org.springframework.boot.sslbundle.jks.truststore.type -
org.springframework.boot.sslbundle.jks.truststore.provider -
org.springframework.boot.sslbundle.jks.truststore.location -
org.springframework.boot.sslbundle.jks.truststore.password
这些标签对应于 SSL bundles 中可用的属性。
对于基于 PEM 的 keystores 和 truststores,你可以使用以下容器标签:
-
org.springframework.boot.sslbundle.pem.key.alias -
org.springframework.boot.sslbundle.pem.key.password -
org.springframework.boot.sslbundle.pem.options.ciphers -
org.springframework.boot.sslbundle.pem.options.enabled-protocols -
org.springframework.boot.sslbundle.pem.protocol -
org.springframework.boot.sslbundle.pem.keystore.type -
org.springframework.boot.sslbundle.pem.keystore.certificate -
org.springframework.boot.sslbundle.pem.keystore.private-key -
org.springframework.boot.sslbundle.pem.keystore.private-key-password -
org.springframework.boot.sslbundle.pem.truststore.type -
org.springframework.boot.sslbundle.pem.truststore.certificate -
org.springframework.boot.sslbundle.pem.truststore.private-key -
org.springframework.boot.sslbundle.pem.truststore.private-key-password
这些标签对应于 SSL bundles 中可用的属性。
以下示例为 Redis 容器启用了 SSL:
services:
redis:
image: 'redis:latest'
ports:
- '6379'
secrets:
- ssl-ca
- ssl-key
- ssl-cert
command: 'redis-server --tls-port 6379 --port 0 --tls-cert-file /run/secrets/ssl-cert --tls-key-file /run/secrets/ssl-key --tls-ca-cert-file /run/secrets/ssl-ca'
labels:
- 'org.springframework.boot.sslbundle.pem.keystore.certificate=client.crt'
- 'org.springframework.boot.sslbundle.pem.keystore.private-key=client.key'
- 'org.springframework.boot.sslbundle.pem.truststore.certificate=ca.crt'
secrets:
ssl-ca:
file: 'ca.crt'
ssl-key:
file: 'server.key'
ssl-cert:
file: 'server.crt'
自定义镜像
有时你可能需要使用自己的镜像版本来提供服务。只要你的自定义镜像的行为与标准镜像一致,就可以使用。具体来说,标准镜像所支持的任何环境变量也必须在你的自定义镜像中使用。
如果你的镜像使用了不同的名称,可以在 compose.yml 文件中使用一个标签,以便 Spring Boot 能够提供服务连接。使用名为 org.springframework.boot.service-connection 的标签来指定服务名称。
例如:
services:
redis:
image: 'mycompany/mycustomredis:7.0'
ports:
- '6379'
labels:
org.springframework.boot.service-connection: redis
跳过特定容器
如果你在 compose.yml 中定义了一个容器镜像,但不希望它连接到你的应用程序,可以使用一个标签来忽略它。任何带有 org.springframework.boot.ignore 标签的容器都会被 Spring Boot 忽略。
例如:
services:
redis:
image: 'redis:7.0'
ports:
- '6379'
labels:
org.springframework.boot.ignore: true
使用特定的 Compose 文件
如果你的 compose 文件与应用程序不在同一目录中,或者文件名不同,可以在 application.properties 或 application.yaml 中使用 spring.docker.compose.file 指向另一个文件。该属性可以定义为绝对路径,也可以是相对于应用程序的相对路径。
例如:
- Properties
- YAML
spring.docker.compose.file=../my-compose.yml
spring:
docker:
compose:
file: "../my-compose.yml"
等待容器就绪
由 Docker Compose 启动的容器可能需要一些时间才能完全就绪。推荐的检查就绪状态的方法是在 compose.yml 文件中的服务定义下添加一个 healthcheck 部分。
由于在 compose.yml 文件中省略 healthcheck 配置的情况并不少见,Spring Boot 也会直接检查服务的就绪状态。默认情况下,当能够与容器映射的端口建立 TCP/IP 连接时,该容器就被认为已就绪。
你可以通过在 compose.yml 文件中添加 org.springframework.boot.readiness-check.tcp.disable 标签,按容器逐个禁用此功能。
例如:
services:
redis:
image: 'redis:7.0'
ports:
- '6379'
labels:
org.springframework.boot.readiness-check.tcp.disable: true
你也可以在你的 application.properties 或 application.yaml 文件中更改超时值:
- Properties
- YAML
spring.docker.compose.readiness.tcp.connect-timeout=10s
spring.docker.compose.readiness.tcp.read-timeout=5s
spring:
docker:
compose:
readiness:
tcp:
connect-timeout: 10s
read-timeout: 5s
可以使用 spring.docker.compose.readiness.timeout 配置整体超时时间。
控制 Docker Compose 生命周期
默认情况下,Spring Boot 在应用程序启动时调用 docker compose up,在关闭时调用 docker compose stop。如果你希望使用不同的生命周期管理方式,可以使用 spring.docker.compose.lifecycle-management 属性。
支持以下值:
-
none- 不启动或停止 Docker Compose -
start-only- 在应用程序启动时启动 Docker Compose,并保持其运行 -
start-and-stop- 在应用程序启动时启动 Docker Compose,并在 JVM 退出时停止它
此外,你可以使用 spring.docker.compose.start.command 属性来更改是使用 docker compose up 还是 docker compose start。spring.docker.compose.stop.command 允许你配置是使用 docker compose down 还是 docker compose stop。
你还可以向 Docker Compose 命令传递额外的参数。spring.docker.compose.arguments 属性允许你指定传递给所有 Docker Compose 命令的参数。spring.docker.compose.start.arguments 属性允许你指定仅传递给 up(或 start)命令的参数,而 spring.docker.compose.stop.arguments 属性允许你指定仅传递给 down(或 stop)命令的参数。
以下示例展示了如何配置生命周期管理:
- Properties
- YAML
spring.docker.compose.lifecycle-management=start-and-stop
spring.docker.compose.arguments[0]=--project-name=myapp
spring.docker.compose.arguments[1]=--progress=auto
spring.docker.compose.start.command=up
spring.docker.compose.start.arguments[0]=--build
spring.docker.compose.start.arguments[1]=--force-recreate
spring.docker.compose.stop.command=down
spring.docker.compose.stop.timeout=1m
spring.docker.compose.stop.arguments[0]=--volumes
spring.docker.compose.stop.arguments[1]=--remove-orphans
spring:
docker:
compose:
lifecycle-management: start-and-stop
arguments:
- "--project-name=myapp"
- "--progress=auto"
start:
command: up
arguments:
- "--build"
- "--force-recreate"
stop:
command: down
timeout: 1m
arguments:
- "--volumes"
- "--remove-orphans"
激活 Docker Compose 配置文件
Docker Compose profiles 与 Spring profiles 类似,它们允许你针对特定环境调整 Docker Compose 配置。如果你想激活某个特定的 Docker Compose profile,可以在 application.properties 或 application.yaml 文件中使用 spring.docker.compose.profiles.active 属性:
- Properties
- YAML
spring.docker.compose.profiles.active=myprofile
spring:
docker:
compose:
profiles:
active: "myprofile"
在测试中使用 Docker Compose
默认情况下,Spring Boot 的 Docker Compose 支持在运行测试时是禁用的。
要启用测试中的 Docker Compose 支持,请将 spring.docker.compose.skip.in-tests 设置为 false。
使用 Gradle 时,你还需要将 spring-boot-docker-compose 依赖的配置从 developmentOnly 更改为 testAndDevelopmentOnly:
dependencies {
testAndDevelopmentOnly("org.springframework.boot:spring-boot-docker-compose")
}
Testcontainers 支持
除了在集成测试中使用 Testcontainers 之外,也可以在开发阶段使用它们。接下来的章节将提供更多相关细节。
在开发时使用 Testcontainers
这种方法允许开发者快速启动应用程序所依赖的服务容器,无需手动配置数据库服务器等组件。以这种方式使用 Testcontainers 提供了与 Docker Compose 类似的功能,不同之处在于你的容器配置是用 Java 编写的,而不是 YAML。
要在开发时使用 Testcontainers,你需要使用“test”类路径而非“main”类路径来启动应用程序。这样可以让你访问所有声明的测试依赖项,并为你提供一个自然的位置来编写测试配置。
要创建一个可启动的测试版本应用程序,你应该在 src/test 目录下创建一个 “Application” 类。例如,如果你的主应用程序位于 src/main/java/com/example/MyApplication.java,那么你应该创建 src/test/java/com/example/TestMyApplication.java。
TestMyApplication 类可以使用 SpringApplication.from(…) 方法来启动真实的应用程序:
- Java
- Kotlin
import org.springframework.boot.SpringApplication;
public class TestMyApplication {
public static void main(String[] args) {
SpringApplication.from(MyApplication::main).run(args);
}
}
import org.springframework.boot.fromApplication
fun main(args: Array<String>) {
fromApplication<MyApplication>().run(*args)
}
你还需要定义想要随应用程序一起启动的 Container 实例。为此,你需要确保已将 spring-boot-testcontainers 模块作为 test 依赖项添加进来。完成该操作后,你可以创建一个 @TestConfiguration 类,并在其中声明用于启动所需容器的 @Bean 方法。
你也可以在 @Bean 方法上添加 @ServiceConnection 注解,以创建 ConnectionDetails Bean。有关支持的技术的详细信息,请参阅 服务连接(service connections) 章节。
一个典型的 Testcontainers 配置如下所示:
- Java
- Kotlin
import org.testcontainers.containers.Neo4jContainer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
@TestConfiguration(proxyBeanMethods = false)
public class MyContainersConfiguration {
@Bean
@ServiceConnection
public Neo4jContainer<?> neo4jContainer() {
return new Neo4jContainer<>("neo4j:5");
}
}
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.Neo4jContainer
@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {
@Bean
@ServiceConnection
fun neo4jContainer(): Neo4jContainer<*> {
return Neo4jContainer("neo4j:5")
}
}
Container Bean 的生命周期由 Spring Boot 自动管理。容器将自动启动和停止。
你可以使用 spring.testcontainers.beans.startup 属性来更改容器的启动方式。默认使用 sequential(顺序)启动,但如果你希望并行启动多个容器,也可以选择 parallel(并行)。
一旦你定义好了测试配置,就可以使用 with(…) 方法将其附加到你的测试启动器上:
- Java
- Kotlin
import org.springframework.boot.SpringApplication;
public class TestMyApplication {
public static void main(String[] args) {
SpringApplication.from(MyApplication::main).with(MyContainersConfiguration.class).run(args);
}
}
import org.springframework.boot.fromApplication
import org.springframework.boot.with
fun main(args: Array<String>) {
fromApplication<MyApplication>().with(MyContainersConfiguration::class).run(*args)
}
现在,你可以像启动任何常规的 Java main 方法应用程序一样启动 TestMyApplication,以启动你的应用程序及其运行所需的容器。
你可以使用 Maven 目标 spring-boot:test-run 或 Gradle 任务 bootTestRun 从命令行执行此操作。
在开发时贡献动态属性
如果你想在开发时从你的 Container @Bean 方法中贡献动态属性,请定义一个额外的 DynamicPropertyRegistrar bean。该 registrar 应通过一个 @Bean 方法来定义,并将作为属性来源的容器以参数形式注入。这种安排可确保在使用这些属性之前,容器已经启动。
一个典型的配置如下所示:
- Java
- Kotlin
import org.testcontainers.containers.MongoDBContainer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.DynamicPropertyRegistrar;
@TestConfiguration(proxyBeanMethods = false)
public class MyContainersConfiguration {
@Bean
public MongoDBContainer mongoDbContainer() {
return new MongoDBContainer("mongo:5.0");
}
@Bean
public DynamicPropertyRegistrar mongoDbProperties(MongoDBContainer container) {
return (properties) -> {
properties.add("spring.data.mongodb.host", container::getHost);
properties.add("spring.data.mongodb.port", container::getFirstMappedPort);
};
}
}
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.test.context.DynamicPropertyRegistrar;
import org.testcontainers.containers.MongoDBContainer
@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {
@Bean
fun mongoDbContainer(): MongoDBContainer {
return MongoDBContainer("mongo:5.0")
}
@Bean
fun mongoDbProperties(container: MongoDBContainer): DynamicPropertyRegistrar {
return DynamicPropertyRegistrar { properties ->
properties.add("spring.data.mongodb.host") { container.host }
properties.add("spring.data.mongodb.port") { container.firstMappedPort }
}
}
}
只要可能,建议尽可能使用 @ServiceConnection;然而,对于尚未支持 @ServiceConnection 的技术,动态属性(dynamic properties)可以作为一种有用的备选方案。
导入 Testcontainers 声明类
使用 Testcontainers 时的一个常见模式是将 Container 实例声明为静态字段。这些字段通常直接定义在测试类上,也可以声明在父类或测试类所实现的接口上。
例如,下面的 MyContainers 接口声明了 mongo 和 neo4j 容器:
- Java
- Kotlin
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
public interface MyContainers {
@Container
@ServiceConnection
MongoDBContainer mongoContainer = new MongoDBContainer("mongo:5.0");
@Container
@ServiceConnection
Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5");
}
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.testcontainers.containers.MongoDBContainer
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container
interface MyContainers {
companion object {
@Container
@ServiceConnection
@JvmField
val mongoContainer = MongoDBContainer("mongo:5.0")
@Container
@ServiceConnection
@JvmField
val neo4jContainer = Neo4jContainer("neo4j:5")
}
}
如果你已经以这种方式定义了容器,或者你更喜欢这种风格,你可以导入这些声明类,而不必将容器定义为 @Bean 方法。为此,请在你的测试配置类上添加 @ImportTestcontainers 注解:
- Java
- Kotlin
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.context.ImportTestcontainers;
@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers.class)
public class MyContainersConfiguration {
}
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.testcontainers.context.ImportTestcontainers
@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers::class)
class MyContainersConfiguration
如果你不打算使用 service connections 功能,而是想改用 @DynamicPropertySource,请从 Container 字段中移除 @ServiceConnection 注解。你也可以在声明类中添加带有 @DynamicPropertySource 注解的方法。
在开发时结合 DevTools 使用 Testcontainers
使用 devtools 时,你可以使用 @RestartScope 注解来标注 Bean 和 Bean 方法。当 devtools 重启应用程序时,这类 Bean 不会被重新创建。这对于 Container Bean 尤其有用,因为即使应用程序重启,它们也能保持其状态。
- Java
- Kotlin
import org.testcontainers.containers.MongoDBContainer;
import org.springframework.boot.devtools.restart.RestartScope;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
@TestConfiguration(proxyBeanMethods = false)
public class MyContainersConfiguration {
@Bean
@RestartScope
@ServiceConnection
public MongoDBContainer mongoDbContainer() {
return new MongoDBContainer("mongo:5.0");
}
}
import org.springframework.boot.devtools.restart.RestartScope
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.MongoDBContainer
@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {
@Bean
@RestartScope
@ServiceConnection
fun mongoDbContainer(): MongoDBContainer {
return MongoDBContainer("mongo:5.0")
}
}
如果你使用的是 Gradle 并希望使用此功能,则需要将 spring-boot-devtools 依赖的配置从 developmentOnly 更改为 testAndDevelopmentOnly。在默认的 developmentOnly 范围下,bootTestRun 任务将无法检测到代码中的更改,因为 devtools 不会生效。