spring-boot-docker-compose使用和源码分析

757 阅读15分钟

1.前言

  • 我们知道docker-compose是一款docker的工具,它可以通过编写yml文件,定义和管理应用程序所需服务的多个容器,也就是说可以通过它来管理我们的docker容器
  • 在SpringBoot3.2中已经有了docker compose的支持,它可以根据我们项目中的docker compose配置文件来创建和管理应用所需的环境依赖,并且零配置的让我们使用和连接上对应的服务,比如我们要使用redis、mysql等等
  • 官方文档:docs.spring.io/spring-boot…

2.使用

使用前提,当前主机必须安装好docker-compose命令,当然docker也需要安装,如果没有,可以指定docker的host地址,使用外部的docker

2.1 创建测试项目

创建SpringBoot项目我们可以通过Spring提供的初始化来创建,网址为start.spring.io/,注意,我们选择了docker compose和spring data redis的支持 image.png

2.2 查看项目结构

通过查看项目结构,我们可以看到给我们生成了一个compose.yaml的文件,在项目的根路径下,这个文件就是docker compose需要的配置文件

image.png

  • 文件内容

查看文件内容,会看到这里是创建redis容器相关的配置

services:
  redis:
    image: 'redis:latest'
    ports:
      - '6379'
  • 查看pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.yuwen</groupId>
	<artifactId>SpringBoot3Test</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>SpringBoot3Test</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>21</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
    <!-- docker-compoose支持 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-docker-compose</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

当然如果你不想通过spring的初始化项目功能来创建新项目,想在老项目上使用,那就需要手动增加依赖和创建compose配置文件

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-docker-compose</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
</dependency>

2.3 启动系统

启动系统,从日志上可以看出,找到了当前项目下的compose配置文件,然后创建了网络和相关的容器,我这里因为不是第一次启动,所以的话,就少了镜像的拉取过程

image.png

2.4 测试使用

这里的话,我们就测试redis的连接和使用

image.png

访问接口进行测试

image.png

好了,通过访问接口,可以看出来已经连接到我们的redis了,而且可以正常使用,这里可能就会存在疑惑了,这玩意是怎么链接上的,我都还没在application.yml文件中配置redis的相关信息,这个疑惑的话就需要通过源码解析查看了,后面会讲

2.5 服务链接

说白了就是说,我们自己的项目怎么链接到的服务,它其实内置了很多链接详情,然后通过对应的镜像来获取连接,举个例子,我们redis为例,那我们使用的连接详情类就是RedisConnectionDetails,它会连接镜像名称为redis的容器,下面是官网其他连接器

  • 其他连接器
ActiveMQConnectionDetailsContainers named "symptoma/activemq"
CassandraConnectionDetailsContainers named "cassandra"
ElasticsearchConnectionDetailsContainers named "elasticsearch"
JdbcConnectionDetailsContainers named "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres"
MongoConnectionDetailsContainers named "mongo"
Neo4jConnectionDetailsContainers named "neo4j"
OtlpMetricsConnectionDetailsContainers named "otel/opentelemetry-collector-contrib"
OtlpTracingConnectionDetailsContainers named "otel/opentelemetry-collector-contrib"
PulsarConnectionDetailsContainers named "apachepulsar/pulsar"
R2dbcConnectionDetailsContainers named "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres"
RabbitConnectionDetailsContainers named "rabbitmq"
RedisConnectionDetailsContainers named "redis"
ZipkinConnectionDetailsContainers named "openzipkin/zipkin".

2.6 自定义镜像

举个例子,上面我们知道redis连接详情对象是通过搜索找到redis的镜像来获取对应的连接的,那如果我们自己打包的redis镜像或者说我们的镜像名称不是redis怎么办,那我们可以给容器增加label,来指定连接为redis,通过名称为org.springframework.boot.service-connection来指定label

services:
  redis:
    image: 'mycompany/mycustomredis:7.0'
    ports:
      - '6379'
    labels:
      org.springframework.boot.service-connection: redis

2.7 跳过镜像连接

如果你在compose指定了容器镜像,但是呢,我又不想要系统自动链接上去,那怎么办呢,我们可以增加忽略,可以通过增加自定义label,key为org.springframework.boot.ignore,值为true,来忽略

services:
  redis:
    image: 'redis:7.0'
    ports:
      - '6379'
    labels:
      org.springframework.boot.ignore: true

2.8 使用自定义的compose文件

如果我们想不使用项目默认下的compose文件,想使用外部的,或者其他路径下的,那我们可以在application.propertis文件中增加以下属性

spring.docker.compose.file=../my-compose.yml

2.9 等待容器就绪

在我们启动项目的时候,会默认检测是否启动容器,如果没有启动,则会根据compose来启动容器,并且会一直等待,等待到容器的启动完成,项目才会启动成功,如果我们第一次启动,还会有拉取镜像的过程,这个过程就会很长了,那我们想要跳过等待检测要怎么办?添加自定义org.springframework.boot.readiness-check.tcp.disable值为true的标签

services:
  redis:
    image: 'redis:7.0'
    ports:
      - '6379'
    labels:
      org.springframework.boot.readiness-check.tcp.disable: true

2.10 控制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.lifecycle-management=start-and-stop
spring.docker.compose.start.command=start
spring.docker.compose.stop.command=down
spring.docker.compose.stop.timeout=1m

2.11 激活 Docker Compose 配置文件

compose支持和spring中active一样的功能,就是说可以区分环境,可以根据不同环境来指定不同的active,如果要使用,在application.propertis中,增加下面配置

spring.docker.compose.profiles.active=myprofile
version: '3.9'
services:
  web:
    image: nginx:latest
    profiles: [dev] # 这个服务仅在dev配置被激活时运行
    ports:
      - "80:80"
  db:
    image: postgres:latest
    profiles: [dev, test] # 这个服务在dev和test配置被激活时运行
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: example
  cache:
    image: redis:latest
    profiles: [test, prod] # 这个服务在test和prod配置被激活时运行

profiles:
  - name: dev
    services: [web, db]
  - name: test
    services: [db, cache]
  - name: prod
    services: [cache]

2.12 在测试环境中使用docker compose

默认情况下测试环境是会跳过compose的,如果需要使用,则需要增加如下配置

spring.docker.compose.skip.in-tests=false

3 源码分析

  • 疑惑

这里到了源码分析环节,在分析的前提,我带着以下几个问题去查看

  1. 什么时候加载项目下的compose.yml文件,我换个名字还有效嘛,或者说除了这个默认名字,是否还有其他名字?
  2. 它是怎么给我们创建容器,检查容器是否已经启动的?
  3. 它是怎么帮我们自动配置的,怎么能让我们没有设置相关信息自动连接到了对应的服务?

3.1 运行大致流程

这里有大概的运行流程

  1. 读取compose文件
  2. 创建compose-cli命令运行对象
  3. 读取compose文件中的应用,并且不小于0
  4. 获取正在运行中的容器
    1. 如果没有运行,则启动容器
    2. 启动了根据lifecycleManagement规则,是否启动容器
  5. 根据规则需要,等待容器准备就绪
  6. 发送容器启动完成事件DockerComposeServicesReadyEvent,通知启动完成
  7. 监听事件DockerComposeServiceConnectionsApplicationListener,注册相关的连接器详情
    1. 内部会根据当前运行的容器和当前容器的镜像匹配对应的连接器详情,如果匹配到了则通过BeanDefinitionRegistry来注册对应的对象到ioc容器中
    2. 然后xxx自动配置类会读取对应的连接器详情对象,来自动创建对应的类,完成自动连接

3.2 分析,具体类定位

好了,安装往常惯例,因为是使用的Spring Boot,那我们应该首先考虑到做到这些都是自动装配了

image.png

在compose的依赖下,我们看到了spring.factories文件,我们知道自动配置都都是通过这个文件开始的,但官网文档表示从2.7开始就不再使用此文件了,而是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports此文件,但原来的spring.factories还是兼容的

通过日志显示和debuger,还有spring.factories,核心类就是org.springframework.boot.docker.compose.lifecycle.DockerComposeListener

  • DockerComposeListener

查看当前类,我们看到了实现了ApplicationListener接口,监听了ApplicationPreparedEvent事件,这个是spring的发布订阅模式,而ApplicationPreparedEvent就是spring要启动完成,但是没有refreshed时候事件,也就是说spring上下文准备就系

class DockerComposeListener implements ApplicationListener<ApplicationPreparedEvent> {

    private final SpringApplicationShutdownHandlers shutdownHandlers;

    DockerComposeListener() {
        this(SpringApplication.getShutdownHandlers());
    }

    DockerComposeListener(SpringApplicationShutdownHandlers shutdownHandlers) {
        this.shutdownHandlers = shutdownHandlers;
    }

    @Override
    public void onApplicationEvent(ApplicationPreparedEvent event) {
        ConfigurableApplicationContext applicationContext = event.getApplicationContext();
        Binder binder = Binder.get(applicationContext.getEnvironment());
        DockerComposeProperties properties = DockerComposeProperties.get(binder);
        Set<ApplicationListener<?>> eventListeners = event.getSpringApplication().getListeners();
        // 创建docker compose生命周期管理,核心就是DockerComposeLifecycleManager
        createDockerComposeLifecycleManager(applicationContext, binder, properties, eventListeners).start();
    }

    protected DockerComposeLifecycleManager createDockerComposeLifecycleManager(
        ConfigurableApplicationContext applicationContext, Binder binder, DockerComposeProperties properties,
        Set<ApplicationListener<?>> eventListeners) {
        return new DockerComposeLifecycleManager(applicationContext, binder, this.shutdownHandlers, properties,
                                                 eventListeners);
    }

}

3.3 容器生命周期源码分析

这个类是docker compose核心类,容器的生命周期通过该类管理

void start() {
		if (Boolean.getBoolean("spring.aot.processing") || AotDetector.useGeneratedArtifacts()) {
			logger.trace("Docker Compose support disabled with AOT and native images");
			return;
		}
		if (!this.properties.isEnabled()) {
			logger.trace("Docker Compose support not enabled");
			return;
		}
		if (this.skipCheck.shouldSkip(this.classLoader, this.properties.getSkip())) {
			logger.trace("Docker Compose support skipped");
			return;
		}
        // 获取compose文件
		DockerComposeFile composeFile = getComposeFile();
        // 获取active环境
		Set<String> activeProfiles = this.properties.getProfiles().getActive();
        // 获取compose-cli运行对象
		DockerCompose dockerCompose = getDockerCompose(composeFile, activeProfiles);
        // 检查是否存在容器配置对象
		if (!dockerCompose.hasDefinedServices()) {
			logger.warn(LogMessage.format("No services defined in Docker Compose file '%s' with active profiles %s",
					composeFile, activeProfiles));
			return;
		}
        // 生命周期管理
		LifecycleManagement lifecycleManagement = this.properties.getLifecycleManagement();
		Start start = this.properties.getStart();
		Stop stop = this.properties.getStop();
		Wait wait = this.properties.getReadiness().getWait();
        // 获取运行容器
		List<RunningService> runningServices = dockerCompose.getRunningServices();
        // 启动
		if (lifecycleManagement.shouldStart()) {
			Skip skip = this.properties.getStart().getSkip();
			if (skip.shouldSkip(runningServices)) {
				logger.info(skip.getLogMessage());
			}
			else {
                // 启动容器
				start.getCommand().applyTo(dockerCompose, start.getLogLevel());
                // 获取运行容器
				runningServices = dockerCompose.getRunningServices();
				if (wait == Wait.ONLY_IF_STARTED) {
					wait = Wait.ALWAYS;
				}
                // 注册销毁方法
				if (lifecycleManagement.shouldStop()) {
					this.shutdownHandlers.add(() -> stop.getCommand().applyTo(dockerCompose, stop.getTimeout()));
				}
			}
		}
		List<RunningService> relevantServices = new ArrayList<>(runningServices);
        // 排除需要忽略的容器
		relevantServices.removeIf(this::isIgnored);
		if (wait == Wait.ALWAYS || wait == null) {
            // 检查容器是否秀徐
			this.serviceReadinessChecks.waitUntilReady(relevantServices);
		}
        // 发送服务准备就绪事件
		publishEvent(new DockerComposeServicesReadyEvent(this.applicationContext, relevantServices));
	}

3.3.1 获取compose文件

查找compose文件,如果没有指定文件路径,则从当前的工作目录找,也就是项目的根路径

  • 从当前项目根路径找主要查看DockerComposeFile.find(this.workingDirectory);方法
protected DockerComposeFile getComposeFile() {
        // 如果配置文件中指定了file,则先读取配置文件中的,如果没有则获取当前系统根路径下的
		DockerComposeFile composeFile = (this.properties.getFile() != null)
				? DockerComposeFile.of(this.properties.getFile()) : DockerComposeFile.find(this.workingDirectory); // 从当前工作路径查找compose文件
		Assert.state(composeFile != null, () -> "No Docker Compose file found in directory '%s'".formatted(
				((this.workingDirectory != null) ? this.workingDirectory : new File(".")).toPath().toAbsolutePath()));
		logger.info(LogMessage.format("Using Docker Compose file '%s'", composeFile));
		return composeFile;
	}

从当前工作路径查找compose文件 这里可以看出,我们默认使用的compose.yaml文件,当然除了这个,还支持compose.yml、docker-compose.yaml、docker-compose.yml文件

	private static final List<String> SEARCH_ORDER = List.of("compose.yaml", "compose.yml", "docker-compose.yaml",
			"docker-compose.yml");

public static DockerComposeFile find(File workingDirectory) {
		File base = (workingDirectory != null) ? workingDirectory : new File(".");
		if (!base.exists()) {
			return null;
		}
		Assert.isTrue(base.isDirectory(), () -> "'%s' is not a directory".formatted(base));
		Path basePath = base.toPath();
        // SEARCH_ORDER哪个文件存在就返回哪个
		for (String candidate : SEARCH_ORDER) {
			Path resolved = basePath.resolve(candidate);
			if (Files.exists(resolved)) {
				return of(resolved.toAbsolutePath().toFile());
			}
		}
		return null;
	}

3.3.2 获取compose执行对象

compose执行对象,也就是说,所有compose的相关操作都是通过此对象,这里对应的对象是DefaultDockerCompose

	static DockerCompose get(DockerComposeFile file, String hostname, Set<String> activeProfiles) {
		// 创建docker命令行对象
        DockerCli cli = new DockerCli(null, file, activeProfiles);
		return new DefaultDockerCompose(cli, hostname);
	}

DefaultDockerCompose的构造器会创建对应的DefaultDockerCompose对象,然后会处理hostname,因为hostname可能为空,当然也可以通过配置指定spring.docker.compose.host=192.168.0.1

DefaultDockerCompose(DockerCli cli, String host) {
		this.cli = cli;
		this.hostname = DockerHost.get(host, () -> cli.run(new DockerCliCommand.Context()));
	}

这里主要查看new DockerCliCommand.Context(),内部是执行此方法,获取docker主机对应的容器,这个是本机的docker 上下文

Context() {
    super(Type.DOCKER, DockerCliContextResponse.class, true, "context", "ls", "--format={{ json . }}");
}

获取docker ip详情

  1. 从系统环境变量中,获取名称为SERVICES_HOST的内容
  2. 从系统环境变量中,获取名称为DOCKER_HOST的内容
  3. 从docker上下文中获取,如果存在current属性为true的,并且dockerEndpoint内容为http或者https或者tcp开头的
  4. 使用默认ip:127.0.0.1
	private static final String LOCALHOST = "127.0.0.1";

static DockerHost get(String host, Supplier<List<DockerCliContextResponse>> contextsSupplier) {
		return get(host, System::getenv, contextsSupplier);
	}

	/**
	 * Get or deduce a new {@link DockerHost} instance.
	 * @param host the host to use or {@code null} to deduce
	 * @param systemEnv access to the system environment
	 * @param contextsSupplier a supplier to provide a list of
	 * {@link DockerCliContextResponse}
	 * @return a new docker host instance
	 */
	static DockerHost get(String host, Function<String, String> systemEnv,
			Supplier<List<DockerCliContextResponse>> contextsSupplier) {
		host = (StringUtils.hasText(host)) ? host : fromServicesHostEnv(systemEnv);
		host = (StringUtils.hasText(host)) ? host : fromDockerHostEnv(systemEnv);
        // 从当前上下文获取,如果能获取到内容并且current属性为true,则使用默认ip
		host = (StringUtils.hasText(host)) ? host : fromCurrentContext(contextsSupplier);
		host = (StringUtils.hasText(host)) ? host : LOCALHOST; // 默认地址
		return new DockerHost(host); 
	}

private static DockerCliContextResponse getCurrentContext(List<DockerCliContextResponse> candidates) {
		return candidates.stream().filter(DockerCliContextResponse::current).findFirst().orElse(null);
	}

	private static String fromEndpoint(String endpoint) {
		return (StringUtils.hasLength(endpoint)) ? fromUri(URI.create(endpoint)) : null;
	}

	private static String fromUri(URI uri) {
		try {
			return switch (uri.getScheme()) {
				case "http", "https", "tcp" -> uri.getHost();
				default -> null;
			};
		}
		catch (Exception ex) {
			return null;
		}
	}

3.3.3 检查compose文件是否存在容器配置

其实就是执行了docker-compose config命令,检查是否存在容器配置,如果不存在直接结束

@Override
	public boolean hasDefinedServices() {
		return !this.cli.run(new DockerCliCommand.ComposeConfig()).services().isEmpty();
	}
static final class ComposeConfig extends DockerCliCommand<DockerCliComposeConfigResponse> {

		ComposeConfig() {
			super(Type.DOCKER_COMPOSE, DockerCliComposeConfigResponse.class, false, "config", "--format=json");
		}

	}

3.3.4 获取运行时容器

内部会通过docker-compose ps 获取正在运行的容器,如果不存在会返回空,存在会获取容器的详细信息也就是inspect,通过 docker-compose inspect获取

@Override
	public List<RunningService> getRunningServices() {
        // 获取运行时容器
		List<DockerCliComposePsResponse> runningPsResponses = runComposePs().stream().filter(this::isRunning).toList();
		if (runningPsResponses.isEmpty()) {
			return Collections.emptyList();
		}
		DockerComposeFile dockerComposeFile = this.cli.getDockerComposeFile();
		List<RunningService> result = new ArrayList<>();
        // 获取容器的详细信息
		Map<String, DockerCliInspectResponse> inspected = inspect(runningPsResponses);
		for (DockerCliComposePsResponse psResponse : runningPsResponses) {
			DockerCliInspectResponse inspectResponse = inspectContainer(psResponse.id(), inspected);
			Assert.notNull(inspectResponse, () -> "Failed to inspect container '%s'".formatted(psResponse.id()));
			result.add(new DefaultRunningService(this.hostname, dockerComposeFile, psResponse, inspectResponse));
		}
		return Collections.unmodifiableList(result);
	}

获取运行容器命令

	static final class ComposePs extends DockerCliCommand<List<DockerCliComposePsResponse>> {

		ComposePs() {
			super(Type.DOCKER_COMPOSE, DockerCliComposePsResponse.class, true, "ps", "--format=json");
		}

	}

获取容器详情数据

static final class Inspect extends DockerCliCommand<List<DockerCliInspectResponse>> {

		Inspect(Collection<String> ids) {
			super(Type.DOCKER, DockerCliInspectResponse.class, true,
					join(List.of("inspect", "--format={{ json . }}"), ids));
		}

	}

3.3.5 启动容器

启动容器的话会有启动规则,根据LifecycleManagement,其实本质就是调用docker-compose up 方法,并且会等待

static final class ComposeUp extends DockerCliCommand<Void> {

		ComposeUp(LogLevel logLevel) {
			super(Type.DOCKER_COMPOSE, logLevel, Void.class, false, "up", "--no-color", "--detach", "--wait");
		}

	}

3.3.6 检查容器是否就绪

  • 如果开启了检查容器是否就绪,内部就会获取容器的所有tcp接口,然后发送连接来校验是否启动完成
  • 如果某个容器要跳过检查我们知道org.springframework.boot.readiness-check.disable=true(当然通过源码查看,只要存在key就行了,不管值)的标签就行了
private static final String DISABLE_LABEL = "org.springframework.boot.readiness-check.disable";


void waitUntilReady(List<RunningService> runningServices) {
		Duration timeout = this.properties.getTimeout();
		Instant start = this.clock.instant();
		while (true) {
            // 检查是否就绪
			List<ServiceNotReadyException> exceptions = check(runningServices);
			if (exceptions.isEmpty()) {
				return;
			}
			Duration elapsed = Duration.between(start, this.clock.instant());
			if (elapsed.compareTo(timeout) > 0) {
				throw new ReadinessTimeoutException(timeout, exceptions);
			}
			this.sleep.accept(SLEEP_BETWEEN_READINESS_TRIES);
		}
	}


private List<ServiceNotReadyException> check(List<RunningService> runningServices) {
		List<ServiceNotReadyException> exceptions = null;
		for (RunningService service : runningServices) {
            // 过滤关闭的容器
			if (isDisabled(service)) {
				continue;
			}
			logger.trace(LogMessage.format("Checking readiness of service '%s'", service));
			try {
                // 检查链接
				this.check.check(service);
				logger.trace(LogMessage.format("Service '%s' is ready", service));
			}
			catch (ServiceNotReadyException ex) {
				logger.trace(LogMessage.format("Service '%s' is not ready", service), ex);
				exceptions = (exceptions != null) ? exceptions : new ArrayList<>();
				exceptions.add(ex);
			}
		}
		return (exceptions != null) ? exceptions : Collections.emptyList();
	}


private boolean isDisabled(RunningService service) {
    return service.labels().containsKey(DISABLE_LABEL);
}
void check(RunningService service) {
		if (service.labels().containsKey(DISABLE_LABEL)) {
			return;
		}
        // 获取所有tcp端口
		for (int port : service.ports().getAll("tcp")) {
			check(service, port);
		}
	}

private void check(RunningService service, int port) {
		int connectTimeout = (int) this.properties.getConnectTimeout().toMillis();
		int readTimeout = (int) this.properties.getReadTimeout().toMillis();
        // 使用socket连接查看是否开启
		try (Socket socket = new Socket()) {
			socket.setSoTimeout(readTimeout);
			socket.connect(new InetSocketAddress(service.host(), port), connectTimeout);
			check(service, port, socket);
		}
		catch (IOException ex) {
			throw new ServiceNotReadyException(service, "IOException while connecting to port %s".formatted(port), ex);
		}
	}

3.3.7 关闭容器

当我们配置文件spring.docker.compose.lifecycle-management=start-and-stop时候,当然默认的就是这个,应用启动的时候会启动容器,应用销毁的时候,会关闭容器,它其实是用的SpringApplicationShutdownHandlers �来实现的

this.shutdownHandlers.add(() -> stop.getCommand().applyTo(dockerCompose, stop.getTimeout()));

3.4 自动连接源码分析

通过上面的分析我们可以看到容器的生命周期是通过DockerComposeLifecycleManager类的start方法来控制的,并且在最后容器就绪的时候,会发送一个DockerComposeServicesReadyEvent事件来告诉我们容器准备就绪,所以我们查看那个类监听了这个事件

3.4.1 事件监听

通过寻找源码,发现了监听对应事件的类为DockerComposeServiceConnectionsApplicationListener


/**
 * {@link ApplicationListener} that listens for an {@link DockerComposeServicesReadyEvent}
 * in order to establish service connections.
 *
 * @author Moritz Halbritter
 * @author Andy Wilkinson
 * @author Phillip Webb
 */
class DockerComposeServiceConnectionsApplicationListener
		implements ApplicationListener<DockerComposeServicesReadyEvent> {

	private final ConnectionDetailsFactories factories;

	DockerComposeServiceConnectionsApplicationListener() {
		this(new ConnectionDetailsFactories());
	}

	DockerComposeServiceConnectionsApplicationListener(ConnectionDetailsFactories factories) {
		this.factories = factories;
	}

	@Override
	public void onApplicationEvent(DockerComposeServicesReadyEvent event) {
		ApplicationContext applicationContext = event.getSource();
        // 当前上下文是BeanDefinitionRegistry的子类则进行注册连接详情对象
		if (applicationContext instanceof BeanDefinitionRegistry registry) {
			registerConnectionDetails(registry, event.getRunningServices());
		}
	}

	private void registerConnectionDetails(BeanDefinitionRegistry registry, List<RunningService> runningServices) {
		for (RunningService runningService : runningServices) {
			DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService);
			// 根据容器获取连接详情
            this.factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> {
				// 往ioc容器中注册对应的连接对象
                register(registry, runningService, connectionDetailsType, connectionDetails);
				this.factories.getConnectionDetails(connectionDetails, false)
					.forEach((adaptedType, adaptedDetails) -> register(registry, runningService, adaptedType,
							adaptedDetails));
			});
		}
	}

	@SuppressWarnings("unchecked")
	private <T> void register(BeanDefinitionRegistry registry, RunningService runningService,
			Class<?> connectionDetailsType, ConnectionDetails connectionDetails) {
		String beanName = getBeanName(runningService, connectionDetailsType);
		Class<T> beanType = (Class<T>) connectionDetails.getClass();
		Supplier<T> beanSupplier = () -> (T) connectionDetails;
		registry.registerBeanDefinition(beanName, new RootBeanDefinition(beanType, beanSupplier));
	}

	private String getBeanName(RunningService runningService, Class<?> connectionDetailsType) {
		List<String> parts = new ArrayList<>();
		parts.add(ClassUtils.getShortNameAsProperty(connectionDetailsType));
		parts.add("for");
		parts.addAll(Arrays.asList(runningService.name().split("-")));
		return StringUtils.uncapitalize(parts.stream().map(StringUtils::capitalize).collect(Collectors.joining()));
	}

}

3.4.2 注册连接详情

通过查看,只有上下文对象是BeanDefinitionRegistry的子类才会进行注册,在web运行中,则上下文对象是AnnotationConfigServletWebServerApplicationContext,通过类关系查看当前类是BeanDefinitionRegistry的子类 image.png

@Override
	public void onApplicationEvent(DockerComposeServicesReadyEvent event) {
		ApplicationContext applicationContext = event.getSource();
        // 当前上下文是BeanDefinitionRegistry的子类则进行注册连接详情对象
		if (applicationContext instanceof BeanDefinitionRegistry registry) {
			registerConnectionDetails(registry, event.getRunningServices());
		}
	}

3.4.3 获取连接器详情对象

从这里开始我们就以redis为例子了,获取连接器对象的主要流程如下

  1. 遍历所有的连接工厂,然后通过当前运行容器的image名称然后是否匹配来返回
    1. 举例:当前的redis的镜像名称为library/redis,对应的连接RedisDockerComposeConnectionDetailsFactory,如果镜像为"redis", "bitnami/redis" 则支持,返回创建对应的连接对象
    2. 当然如果镜像名称不匹配,可以通过指定标签来是使条件成立org.springframework.boot.service-connection�
public <S> Map<Class<?>, ConnectionDetails> getConnectionDetails(S source, boolean required)
			throws ConnectionDetailsFactoryNotFoundException, ConnectionDetailsNotFoundException {
		List<Registration<S, ?>> registrations = getRegistrations(source, required);
		Map<Class<?>, ConnectionDetails> result = new LinkedHashMap<>();
		for (Registration<S, ?> registration : registrations) {
            // 获取连接工厂获取连接器详情对象,这里可能获为空
			ConnectionDetails connectionDetails = registration.factory().getConnectionDetails(source);
			if (connectionDetails != null) {
				Class<?> connectionDetailsType = registration.connectionDetailsType();
				ConnectionDetails previous = result.put(connectionDetailsType, connectionDetails);
				Assert.state(previous == null, () -> "Duplicate connection details supplied for %s"
					.formatted(connectionDetailsType.getName()));
			}
		}
		if (required && result.isEmpty()) {
			throw new ConnectionDetailsNotFoundException(source);
		}
		return Map.copyOf(result);
	}

这里以RedisDockerComposeConnectionDetailsFactory举例 主要是查看镜像是否匹配,匹配则返回对应的连接对象

	private final Predicate<DockerComposeConnectionSource> predicate;


@Override
	public final D getConnectionDetails(DockerComposeConnectionSource source) {
		return (!accept(source)) ? null : getDockerComposeConnectionDetails(source);
	}


private boolean accept(DockerComposeConnectionSource source) {
        // 查看镜像是否匹配
		return hasRequiredClasses() && this.predicate.test(source);
	}


RedisDockerComposeConnectionDetailsFactory对象镜像

	private static final String[] REDIS_CONTAINER_NAMES = { "redis", "bitnami/redis" };

通过ConnectionNamePredicate的getActual方法,我们可以看到如果镜像名称不匹配,还可以通过指定label为org.springframework.boot.service-connection来解决匹配问题

class ConnectionNamePredicate implements Predicate<DockerComposeConnectionSource> {

	private final Set<String> required;

	ConnectionNamePredicate(String... required) {
		Assert.notEmpty(required, "Required must not be empty");
		this.required = Arrays.stream(required).map(this::asCanonicalName).collect(Collectors.toSet());
	}

	@Override
	public boolean test(DockerComposeConnectionSource source) {
		String actual = getActual(source.getRunningService());
		return this.required.contains(actual);
	}

	private String getActual(RunningService service) {
		String label = service.labels().get("org.springframework.boot.service-connection");
		return (label != null) ? asCanonicalName(label) : service.image().getName();
	}

	private String asCanonicalName(String name) {
		return ImageReference.of(name).getName();
	}

}

假设条件成立则会返回对应的连接详情对象

	@Override
	protected RedisConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
		return new RedisDockerComposeConnectionDetails(source.getRunningService());
	}

3.4.4 注册连接对象

连接详情对象获取成功后则会注册到ioc容器中

private void registerConnectionDetails(BeanDefinitionRegistry registry, List<RunningService> runningServices) {
		for (RunningService runningService : runningServices) {
			DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService);
			this.factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> {
				// 注册对象
                register(registry, runningService, connectionDetailsType, connectionDetails);
				this.factories.getConnectionDetails(connectionDetails, false)
					.forEach((adaptedType, adaptedDetails) -> register(registry, runningService, adaptedType,
							adaptedDetails));
			});
		}
	}


	private <T> void register(BeanDefinitionRegistry registry, RunningService runningService,
			Class<?> connectionDetailsType, ConnectionDetails connectionDetails) {
		String beanName = getBeanName(runningService, connectionDetailsType);
		Class<T> beanType = (Class<T>) connectionDetails.getClass();
		Supplier<T> beanSupplier = () -> (T) connectionDetails;
		registry.registerBeanDefinition(beanName, new RootBeanDefinition(beanType, beanSupplier));
	}

3.4.5 redis自动装配

通过下断点RedisDockerComposeConnectionDetails的getStandalone方法,可以看到RedisConnectionConfiguration中获取了对应配置

protected final RedisStandaloneConfiguration getStandaloneConfig() {
		if (this.standaloneConfiguration != null) {
			return this.standaloneConfiguration;
		}
		RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
		config.setHostName(this.connectionDetails.getStandalone().getHost());
		config.setPort(this.connectionDetails.getStandalone().getPort());
		config.setUsername(this.connectionDetails.getUsername());
		config.setPassword(RedisPassword.of(this.connectionDetails.getPassword()));
		config.setDatabase(this.connectionDetails.getStandalone().getDatabase());
		return config;
	}



而getStandaloneConfig是LettuceConnectionConfiguration的redisConnectionFactory来调用的,这里就可以看到整个的流程了,怎么做到自动装配流程

	@Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	@ConditionalOnThreading(Threading.PLATFORM)
	LettuceConnectionFactory redisConnectionFactory(
			ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
			ClientResources clientResources) {
		return createConnectionFactory(builderCustomizers, clientResources);
	}