소스 검색

feat: add Chapter 5 - container management APIs (create/start/stop/kill/remove/list/inspect)

yangyi 3 일 전
부모
커밋
5646c1f8f0
3개의 변경된 파일543개의 추가작업 그리고 1개의 파일을 삭제
  1. 234 1
      docker-java-sdk-tutorial.md
  2. 186 0
      src/main/java/space/anyi/docker/ContainerManageAPI.java
  3. 123 0
      src/test/java/space/anyi/docker/ContainerManageAPITest.java

+ 234 - 1
docker-java-sdk-tutorial.md

@@ -817,4 +817,237 @@ log.info("找到 {} 个匹配镜像", images.size());
 
 ***
 
-下一章,我们将学习容器管理相关的 API,这是 Docker 使用中最频繁的操作。
+## 容器管理相关API
+
+容器(Container)是 Docker 的另一核心概念,它是镜像的**运行实例**。如果说镜像是"类",那容器就是"对象"。本章将系统学习容器管理的各项 API。
+
+### 容器的完整生命周期
+
+容器的生命周期可以分为几个阶段:
+
+```
+创建 (create) → 启动 (start) → 运行中 (running)
+                                    ↓
+                    停止 (stop) / 强制终止 (kill)
+                                    ↓
+                              删除 (remove)
+```
+
+Docker 的设计哲学是:**创建**和**启动**是分离的操作。创建容器只分配资源和配置,容器处于 `created` 状态;启动后进入 `running` 状态。这种分离设计让我们可以在启动前完成网络、卷、资源限制等配置。
+
+### createContainerCmd - 创建容器
+
+```java
+/**
+ * 创建容器(不启动)
+ * @param imageName 镜像名称
+ * @param containerName 容器名称
+ * @return 创建的容器响应(包含容器 ID)
+ */
+public CreateContainerResponse createContainer(String imageName, String containerName) {
+    // 创建容器:分配资源配置,但不会真正运行
+    // withName 指定容器名称(可选,不指定时 Docker 自动生成)
+    CreateContainerResponse response = dockerClient.createContainerCmd(imageName)
+            .withName(containerName)
+            .exec();
+    log.info("容器创建成功,ID: {}, 名称: {}", response.getId(), containerName);
+    return response;
+}
+```
+
+创建容器时常用的配置项:
+
+| 方法 | 说明 |
+|------|------|
+| `withName(String)` | 指定容器名称 |
+| `withEnv(String...)` | 设置环境变量 |
+| `withExposedPorts(ExposedPort...)` | 声明容器内暴露的端口 |
+| `withPortBindings(Ports)` | 配置端口映射(宿主机端口 -> 容器内端口) |
+| `withCmd(String...)` | 覆盖镜像中定义的启动命令 |
+| `withWorkingDir(String)` | 设置容器内工作目录 |
+| `withHostConfig(HostConfig)` | 设置资源限制、挂载卷、网络模式等 |
+
+创建带环境变量和端口映射的容器:
+
+```java
+// 创建带环境变量的容器
+dockerClient.createContainerCmd("nginx:latest")
+        .withName("my-nginx")
+        .withEnv("NGINX_HOST=localhost", "NGINX_PORT=80")
+        .exec();
+
+// 创建带端口映射的容器
+dockerClient.createContainerCmd("nginx:latest")
+        .withName("my-nginx")
+        .withExposedPorts(ExposedPort.tcp(80))
+        .withPortBindings(new Ports.Binding(8080, 80))
+        .exec();
+```
+
+### startContainerCmd - 启动容器
+
+创建容器后,使用 `startContainerCmd()` 启动它:
+
+```java
+/**
+ * 启动容器
+ * @param containerId 容器 ID
+ */
+public void startContainer(String containerId) {
+    dockerClient.startContainerCmd(containerId).exec();
+    log.info("容器 {} 启动成功", containerId);
+}
+```
+
+**思考:** 在快速入门章节我们看到了 `CreateContainerResponse.getId()`,它返回完整的容器 ID。为什么大多数 API 都要求传容器 ID 而不是名称?
+
+容器 ID 是容器的唯一标识,不会重复。而容器名称虽然也可以用于操作(Docker daemon 会解析),但在某些场景下名称可能不存在或重复。使用 ID 是最稳妥的方式。SDK 也支持传入短 ID 或名称,Docker daemon 会自动解析。
+
+### stopContainerCmd - 停止容器
+
+停止容器采用"优雅退出"机制:首先向容器进程发送 `SIGTERM` 信号,等待一段时间后仍未退出则发送 `SIGKILL` 强制终止:
+
+```java
+/**
+ * 停止容器(等待容器优雅退出)
+ * @param containerId 容器 ID
+ * @param timeoutSeconds 等待时长(秒),超过则强制终止
+ */
+public void stopContainer(String containerId, Integer timeoutSeconds) {
+    dockerClient.stopContainerCmd(containerId)
+            .withTimeout(timeoutSeconds)  // SIGTERM 后等待时间
+            .exec();
+    log.info("容器 {} 已停止(等待 {} 秒)", containerId, timeoutSeconds);
+}
+```
+
+### killContainerCmd - 强制停止容器
+
+与 `stopContainerCmd` 不同,`killContainerCmd` 直接发送 `SIGKILL` 信号,进程立即终止,不给优雅退出的机会:
+
+```java
+/**
+ * 强制停止容器(立即发送 SIGKILL)
+ * @param containerId 容器 ID
+ */
+public void killContainer(String containerId) {
+    dockerClient.killContainerCmd(containerId).exec();
+    log.info("容器 {} 已被强制停止", containerId);
+}
+```
+
+**思考:** 什么场景下应该用 stop,什么场景下应该用 kill?
+
+`stop` 会先给进程发 `SIGTERM`,让应用有机会保存数据、释放资源(比如数据库需要执行清理操作);`kill` 则是立即杀死进程,可能导致数据丢失。生产环境中应该优先使用 `stop`,只有在进程无法响应或严格限时的情况下才使用 `kill`。
+
+### removeContainerCmd - 删除容器
+
+删除容器时需要考虑两个选项:
+
+```java
+/**
+ * 删除容器
+ * @param containerId 容器 ID
+ * @param force 是否强制删除(容器运行中也删除)
+ * @param removeVolumes 是否同时删除关联的卷
+ */
+public void removeContainer(String containerId, boolean force, boolean removeVolumes) {
+    dockerClient.removeContainerCmd(containerId)
+            .withForce(force)            // 强制删除运行中的容器
+            .withRemoveVolumes(removeVolumes)  // 删除关联数据卷
+            .exec();
+    log.info("容器 {} 已删除", containerId);
+}
+```
+
+### listContainersCmd - 列出容器
+
+列出容器对应 `docker ps` 命令。默认只显示运行中的容器,`withShowAll(true)` 则显示所有容器(包括已停止的):
+
+```java
+/**
+ * 列出所有容器(包括停止的)
+ * @return 容器列表
+ */
+public List<Container> listContainers() {
+    // withShowAll(true) 展示所有容器(不只运行中的)
+    List<Container> containers = dockerClient.listContainersCmd()
+            .withShowAll(true)
+            .exec();
+    log.info("本机共有 {} 个容器", containers.size());
+    return containers;
+}
+```
+
+`Container` 对象包含的信息:
+
+| 字段 | 说明 |
+|------|------|
+| `getId()` | 容器 ID |
+| `getNames()` | 容器名称列表(以 `/` 开头) |
+| `getImage()` | 使用的镜像名称 |
+| `getState()` | 容器状态(running / exited 等) |
+| `getStatus()` | 状态描述(如 "Up 2 hours") |
+| `getPorts()` | 端口映射信息 |
+| `getLabels()` | 容器标签 |
+
+### inspectContainerCmd - 获取容器详细信息
+
+与镜像类似,容器也有详查方法。它返回容器的完整配置、网络、挂载信息:
+
+```java
+/**
+ * 获取容器详细信息
+ * @param containerId 容器 ID
+ * @return 容器详情
+ */
+public InspectContainerResponse inspectContainer(String containerId) {
+    InspectContainerResponse response = dockerClient.inspectContainerCmd(containerId).exec();
+    log.info("容器 {} 状态: {}, 名称: {}",
+            containerId, response.getState().getStatus(), response.getName());
+    return response;
+}
+```
+
+`InspectContainerResponse` 的关键信息:
+
+| 字段 | 说明 |
+|------|------|
+| `getId()` | 容器 ID |
+| `getName()` | 容器名称 |
+| `getState()` | 容器状态(含 Running、Status、StartedAt 等) |
+| `getConfig()` | 容器配置(镜像、环境变量、命令等) |
+| `getNetworkSettings()` | 网络配置(端口映射、IP 地址等) |
+| `getMounts()` | 挂载的卷 |
+| `getHostConfig()` | 宿主机相关配置 |
+| `getCreated()` | 创建时间 |
+
+### 完整生命周期示例
+
+```java
+public void containerLifecycleDemo(String imageName, String containerName) {
+    // 1. 创建容器
+    CreateContainerResponse container = createContainer(imageName, containerName);
+
+    // 2. 启动容器
+    startContainer(container.getId());
+
+    // 3. 查询状态
+    inspectContainer(container.getId());
+
+    // 4. 停止容器
+    stopContainer(container.getId(), 10);
+
+    // 5. 删除容器
+    removeContainer(container.getId(), false, false);
+    log.info("容器 {} 生命周期演示完成", containerName);
+}
+```
+
+**思考:** 查看 `Container` 的 `getNames()` 返回的数组——为什么是一个数组而不是单个字符串?
+
+Docker 的设计中,一个容器可以有多个名称(别名),通常是 `/容器名`。当使用 `--link` 方式连接容器时,会为被连接的容器创建额外的名称。虽然现代 Docker 更推荐使用自定义网络,但 `getNames()` 的设计保留了这种能力。
+
+***
+
+下一章,我们将讲解容器运维相关 API,包括查看日志、在容器中执行命令、文件复制等操作。

+ 186 - 0
src/main/java/space/anyi/docker/ContainerManageAPI.java

@@ -0,0 +1,186 @@
+package space.anyi.docker;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.github.dockerjava.api.model.Container;
+import com.github.dockerjava.api.model.ExposedPort;
+import com.github.dockerjava.api.model.Ports;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * Docker 容器管理 API 示例
+ * 演示容器的创建、启动、停止、删除、列出、查看等操作
+ */
+public class ContainerManageAPI {
+    private static final Logger log = LoggerFactory.getLogger(ContainerManageAPI.class);
+
+    private final DockerClient dockerClient;
+
+    public ContainerManageAPI() {
+        this.dockerClient = DockerClientFactory.createDockerClient();
+    }
+
+    /**
+     * 创建容器(不启动)
+     * @param imageName 镜像名称
+     * @param containerName 容器名称
+     * @return 创建的容器响应(包含容器 ID)
+     */
+    public CreateContainerResponse createContainer(String imageName, String containerName) {
+        // 创建容器:分配资源配置,但不会真正运行
+        // withName 指定容器名称(可选,不指定时 Docker 自动生成)
+        CreateContainerResponse response = dockerClient.createContainerCmd(imageName)
+                .withName(containerName)
+                .exec();
+        log.info("容器创建成功,ID: {}, 名称: {}", response.getId(), containerName);
+        return response;
+    }
+
+    /**
+     * 创建带环境变量的容器
+     * @param imageName 镜像名称
+     * @param containerName 容器名称
+     * @param envVars 环境变量,如 "KEY=VALUE"
+     * @return 容器响应
+     */
+    public CreateContainerResponse createContainerWithEnv(String imageName, String containerName, String... envVars) {
+        CreateContainerResponse response = dockerClient.createContainerCmd(imageName)
+                .withName(containerName)
+                .withEnv(envVars)
+                .exec();
+        log.info("带环境变量的容器创建成功,ID: {}", response.getId());
+        return response;
+    }
+
+    /**
+     * 创建带端口映射的容器
+     * @param imageName 镜像名称
+     * @param containerName 容器名称
+     * @param hostPort 宿主机端口
+     * @param containerPort 容器内端口
+     * @return 容器响应
+     */
+    public CreateContainerResponse createContainerWithPort(String imageName, String containerName,
+                                                           Integer hostPort, Integer containerPort) {
+        // 声明容器内需要暴露的端口
+        ExposedPort exposedPort = ExposedPort.tcp(containerPort);
+        // 配置端口绑定:宿主机端口 -> 容器内端口
+        Ports portBindings = new Ports();
+        portBindings.bind(exposedPort, Ports.Binding.bindPort(hostPort));
+
+        CreateContainerResponse response = dockerClient.createContainerCmd(imageName)
+                .withName(containerName)
+                .withExposedPorts(exposedPort)
+                .withPortBindings(portBindings)
+                .exec();
+        log.info("带端口映射的容器创建成功,ID: {}, 映射: {} -> {}", response.getId(), hostPort, containerPort);
+        return response;
+    }
+
+    /**
+     * 启动容器
+     * @param containerId 容器 ID
+     */
+    public void startContainer(String containerId) {
+        dockerClient.startContainerCmd(containerId).exec();
+        log.info("容器 {} 启动成功", containerId);
+    }
+
+    /**
+     * 停止容器(等待容器优雅退出)
+     * @param containerId 容器 ID
+     * @param timeoutSeconds 等待时长(秒),超过则强制终止
+     */
+    public void stopContainer(String containerId, Integer timeoutSeconds) {
+        dockerClient.stopContainerCmd(containerId)
+                .withTimeout(timeoutSeconds)  // SIGTERM 后等待时间
+                .exec();
+        log.info("容器 {} 已停止(等待 {} 秒)", containerId, timeoutSeconds);
+    }
+
+    /**
+     * 强制停止容器(立即发送 SIGKILL)
+     * @param containerId 容器 ID
+     */
+    public void killContainer(String containerId) {
+        dockerClient.killContainerCmd(containerId).exec();
+        log.info("容器 {} 已被强制停止", containerId);
+    }
+
+    /**
+     * 删除容器
+     * @param containerId 容器 ID
+     * @param force 是否强制删除(容器运行中也删除)
+     * @param removeVolumes 是否同时删除关联的卷
+     */
+    public void removeContainer(String containerId, boolean force, boolean removeVolumes) {
+        dockerClient.removeContainerCmd(containerId)
+                .withForce(force)            // 强制删除运行中的容器
+                .withRemoveVolumes(removeVolumes)  // 删除关联数据卷
+                .exec();
+        log.info("容器 {} 已删除", containerId);
+    }
+
+    /**
+     * 列出所有容器(包括停止的)
+     * @return 容器列表
+     */
+    public List<Container> listContainers() {
+        // withShowAll(true) 展示所有容器(不只为运行中的)
+        List<Container> containers = dockerClient.listContainersCmd()
+                .withShowAll(true)
+                .exec();
+        log.info("本机共有 {} 个容器", containers.size());
+        return containers;
+    }
+
+    /**
+     * 只列出运行中的容器
+     * @return 运行中的容器列表
+     */
+    public List<Container> listRunningContainers() {
+        List<Container> containers = dockerClient.listContainersCmd().exec();
+        log.info("运行中的容器数量: {}", containers.size());
+        return containers;
+    }
+
+    /**
+     * 获取容器详细信息
+     * @param containerId 容器 ID
+     * @return 容器详情
+     */
+    public InspectContainerResponse inspectContainer(String containerId) {
+        InspectContainerResponse response = dockerClient.inspectContainerCmd(containerId).exec();
+        log.info("容器 {} 状态: {}, 名称: {}",
+                containerId, response.getState().getStatus(), response.getName());
+        return response;
+    }
+
+    /**
+     * 封装的完整生命周期演示方法
+     * 创建 -> 启动 -> 查询状态 -> 停止 -> 删除
+     * @param imageName 镜像名称
+     * @param containerName 容器名称
+     */
+    public void containerLifecycleDemo(String imageName, String containerName) {
+        // 1. 创建容器
+        CreateContainerResponse container = createContainer(imageName, containerName);
+
+        // 2. 启动容器
+        startContainer(container.getId());
+
+        // 3. 查询状态
+        inspectContainer(container.getId());
+
+        // 4. 停止容器
+        stopContainer(container.getId(), 10);
+
+        // 5. 删除容器
+        removeContainer(container.getId(), false, false);
+        log.info("容器 {} 生命周期演示完成", containerName);
+    }
+}

+ 123 - 0
src/test/java/space/anyi/docker/ContainerManageAPITest.java

@@ -0,0 +1,123 @@
+package space.anyi.docker;
+
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.github.dockerjava.api.model.Container;
+import com.github.dockerjava.api.model.ExposedPort;
+import com.github.dockerjava.api.model.Ports;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 容器管理 API 的测试类
+ * 注意:需要本机 Docker daemon 运行中
+ * 使用本地已有镜像,每个测试自行清理创建的容器
+ */
+class ContainerManageAPITest {
+
+    // 使用本地 amd64 镜像作为基础
+    private static final String LOCAL_IMAGE = "registry.fit2cloud.com/halo/halo-pro:2.24";
+
+    // 生成唯一的容器名称,避免测试之间相互干扰
+    private String uniqueName(String prefix) {
+        return prefix + "-" + UUID.randomUUID().toString().substring(0, 8);
+    }
+
+    /**
+     * 测试创建容器
+     */
+    @Test
+    void createContainer() {
+        ContainerManageAPI api = new ContainerManageAPI();
+        String name = uniqueName("test-create");
+        CreateContainerResponse container = api.createContainer(LOCAL_IMAGE, name);
+        assertNotNull(container.getId(), "创建的容器应返回 ID");
+
+        // 清理
+        api.removeContainer(container.getId(), true, false);
+    }
+
+    /**
+     * 测试创建带环境变量的容器
+     */
+    @Test
+    void createContainerWithEnv() {
+        ContainerManageAPI api = new ContainerManageAPI();
+        String name = uniqueName("test-env");
+        CreateContainerResponse container = api.createContainerWithEnv(LOCAL_IMAGE, name, "MY_ENV=hello", "TEST=true");
+        assertNotNull(container.getId());
+
+        // 验证环境变量已生效
+        InspectContainerResponse inspect = api.inspectContainer(container.getId());
+        String[] env = inspect.getConfig().getEnv();
+        assertNotNull(env);
+        assertTrue(java.util.Arrays.asList(env).contains("MY_ENV=hello"),
+                "环境变量 MY_ENV=hello 应已注入");
+
+        // 清理
+        api.removeContainer(container.getId(), true, false);
+    }
+
+    /**
+     * 测试创建带端口映射的容器
+     */
+    @Test
+    void createContainerWithPort() {
+        ContainerManageAPI api = new ContainerManageAPI();
+        String name = uniqueName("test-port");
+        CreateContainerResponse container = api.createContainerWithPort(LOCAL_IMAGE, name, 18090, 8080);
+        assertNotNull(container.getId());
+
+        // 验证端口映射配置存在
+        InspectContainerResponse inspect = api.inspectContainer(container.getId());
+        assertNotNull(inspect.getNetworkSettings().getPorts());
+
+        // 清理
+        api.removeContainer(container.getId(), true, false);
+    }
+
+    /**
+     * 测试容器的完整生命周期:创建->启动->查询->停止->删除
+     */
+    @Test
+    void containerLifecycleDemo() {
+        ContainerManageAPI api = new ContainerManageAPI();
+        String name = uniqueName("test-lifecycle");
+
+        // 1. 创建并启动
+        CreateContainerResponse container = api.createContainer(LOCAL_IMAGE, name);
+        api.startContainer(container.getId());
+
+        // 2. 验证处于运行状态
+        InspectContainerResponse running = api.inspectContainer(container.getId());
+        assertTrue(running.getState().getRunning(), "启动后的容器应处于运行状态");
+
+        // 3. 停止容器
+        api.stopContainer(container.getId(), 10);
+        InspectContainerResponse stopped = api.inspectContainer(container.getId());
+        assertFalse(stopped.getState().getRunning(), "停止后的容器不应处于运行状态");
+
+        // 4. 删除容器
+        api.removeContainer(container.getId(), false, false);
+    }
+
+    /**
+     * 测试列出容器
+     */
+    @Test
+    void listContainers() {
+        ContainerManageAPI api = new ContainerManageAPI();
+        List<Container> all = api.listContainers();
+        assertNotNull(all, "容器列表不应为 null");
+
+        List<Container> running = api.listRunningContainers();
+        assertNotNull(running);
+        // 运行中的容器数应小于等于全部容器数
+        assertTrue(running.size() <= all.size(),
+                "运行中的容器数不应超过容器总数");
+    }
+}