# Docker Java SDK 使用教程 > Solomon Hykes — Docker 创始人 > > Docker 提供了一个核心的抽象层,让你可以在任何基础设施上运行任何应用。这种抽象层的本质是:将应用的运行环境打包成一个标准化的单元,这个单元可以在任何地方运行。 那么,从这章开始,就让我们来感受一下,Docker Java SDK 为我们带来了什么。 *** ## 前置Docker配置 在使用 Docker Java SDK 之前,我们需要确保本机的 Docker 环境已经正确配置。SDK 通过 Unix socket 或 TCP 协议与 Docker daemon 通信,因此需要根据实际情况选择合适的连接方式并完成相应配置。 ### 用户组配置 默认情况下,Docker daemon 的 Unix socket 文件 `/var/run/docker.sock` 的所有者是 `root` 用户,所属组是 `docker`。如果当前用户没有 `docker` 用户组的权限,在连接 Docker 时会遇到权限拒绝的问题: ``` permission denied while trying to connect to the Docker daemon socket ``` 解决方法是将当前用户添加到 `docker` 用户组中: ```bash # 将当前用户添加到 docker 用户组 sudo usermod -aG docker $USER # 查看当前用户所属 groups groups # 如果 usermod 不生效,可以尝试 newgrp 刷新组信息 newgrp docker ``` 添加完成后,需要**重新登录系统**或**重启系统**让修改生效。我们可以通过以下命令验证是否配置成功: ```bash # 查看 docker.sock 文件的权限 ls -la /var/run/docker.sock # 输出示例:srw-rw---- 1 root docker 0 Sep 15 10:00 /var/run/docker.sock # 注意权限中的 's' 表示 socket 文件,所属组为 docker ``` **思考:** 为什么 Docker 选择使用 Unix socket 而不是普通的文件来进行通信? Unix socket 是一种进程间通信(IPC)机制,相比 TCP 连接,它在同一台机器上的通信效率更高,且不需要经过网络协议栈,安全性也更好。因此 Docker daemon 默认监听 Unix socket 作为主要的通信方式。 ### 网络协议配置 Docker Java SDK 支持两种连接协议:**Unix socket** 和 **TCP**。 #### Unix socket 协议 Unix socket 是默认的连接方式,适用于 Docker daemon 运行在本机的场景。SDK 通过以下地址连接: ``` unix:///var/run/docker.sock ``` 这种方式的优点是无需额外配置,安全性高(通过文件权限控制访问),但缺点是只能连接本机的 Docker daemon。 #### TCP 协议 当需要远程连接 Docker daemon 时,需要使用 TCP 协议。这要求 Docker daemon 启动时开启 TCP 端口监听。 **方法一:修改 Docker daemon 启动参数** 在 `/etc/docker/daemon.json` 中添加配置: ```json { "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"] } ``` **方法二:修改 systemd service 配置** 创建或编辑 systemd 配置文件: ```bash sudo mkdir -p /etc/systemd/system/docker.service.d sudo vi /etc/systemd/system/docker.service.d/override.conf ``` 添加以下内容: ```ini [Service] ExecStart= ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock ``` 然后重启 Docker 服务: ```bash sudo systemctl daemon-reload sudo systemctl restart docker ``` 验证 TCP 监听是否生效: ```bash # 查看 Docker daemon 监听的端口 ss -tlnp | grep 2375 # 或使用 curl 测试 curl http://localhost:2375/version ``` **注意:** 开启 TCP 监听后,任何能够访问该端口的客户端都可以操作 Docker daemon,存在严重的安全风险。在生产环境中,建议配合 TLS 证书进行加密通信。 #### TLS 安全通信配置 在生产环境中使用 TCP 协议时,应该启用 TLS 加密: ```bash # 创建 TLS 证书(示例) mkdir -p /etc/docker/certs openssl req -x509 -newkey rsa:4096 -sha256 -days 365 \ -keyout /etc/docker/certs/server-key.pem \ -out /etc/docker/certs/server-cert.pem \ -nodes -subj "/CN=localhost" # Docker daemon 启动参数 ExecStart=/usr/bin/dockerd \ --tlsverify \ --tlscacert=/etc/docker/ca.pem \ --tlscert=/etc/docker/certs/server-cert.pem \ --tlskey=/etc/docker/certs/server-key.pem \ -H tcp://0.0.0.0:2376 \ -H unix://var/run/docker.sock ``` ### 连接方式对比 | 特性 | Unix socket | TCP | TCP + TLS | |------|-------------|-----|-----------| | 适用场景 | 本机连接 | 远程连接 | 远程连接(生产) | | 安全性 | 高(文件权限) | 低(明文传输) | 高(证书加密) | | 性能 | 高 | 中等 | 中等(加密开销) | | 配置复杂度 | 低 | 中 | 高 | | 默认端口 | 无 | 2375 | 2376 | **思考:** 在微服务架构中,如果需要让应用通过 Docker Java SDK 管理容器化的服务,你会选择哪种连接方式?为什么? *** ## 快速入门 前面我们完成了 Docker 环境的配置,现在让我们正式开始使用 Docker Java SDK。本章将介绍如何创建 DockerClient、连接到 Docker daemon、拉取镜像,以及创建并启动一个容器。 ### Maven 依赖 首先,我们需要在项目中引入 Docker Java SDK 的依赖。在 `pom.xml` 中添加以下配置: ```xml com.github.docker-java docker-java 3.7.1 com.github.docker-java docker-java-transport-httpclient5 3.7.1 ch.qos.logback logback-classic 1.5.38 ``` 这里我们需要两个核心依赖:`docker-java` 是 SDK 的核心库,定义了所有 API 接口和模型;`docker-java-transport-httpclient5` 是传输层实现,负责与 Docker daemon 进行 HTTP 通信。 ### 创建 DockerClient DockerClient 是 Docker Java SDK 的核心入口类,所有的 Docker 操作都通过它来执行。SDK 提供了两种创建方式:使用默认配置和自定义配置。 #### 使用默认配置 最简单的方式是使用 `DockerClientBuilder` 的默认配置,它会自动检测本机的 Docker 环境: ```java package space.anyi.docker; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.core.DockerClientBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Docker Java SDK 快速入门示例 * 演示如何使用默认配置创建 DockerClient */ public class QuickStart { private static final Logger log = LoggerFactory.getLogger(QuickStart.class); /** * 使用默认配置连接本地 Docker * 默认配置会自动使用 Unix socket 连接本机 Docker daemon */ public void quickStart() { // 使用默认配置创建 DockerClient 实例 // 内部会自动检测 Docker 环境并使用 unix:///var/run/docker.sock 连接 DockerClient dockerClient = DockerClientBuilder.getInstance().build(); // 使用 ping 命令测试连接是否成功 // 等价命令: docker version(连接自检,底层请求 HTTP GET /_ping) dockerClient.pingCmd().exec(); log.info("Docker client ping 成功!"); // 列出所有本地镜像,验证连接正常 // 等价命令: docker images dockerClient.listImagesCmd().exec().forEach(image -> { log.info("Docker 镜像信息: {}", image); }); } } ``` 这段代码展示了最基本的使用方式:通过 `DockerClientBuilder.getInstance().build()` 创建客户端,然后使用 `pingCmd()` 测试连接,最后列出所有镜像验证功能正常。 **思考:** `DockerClientBuilder.getInstance()` 内部做了什么?它是如何知道 Docker daemon 的地址的? 实际上,`DockerClientBuilder.getInstance()` 会读取环境变量 `DOCKER_HOST` 来确定 Docker daemon 的地址。如果该环境变量未设置,则默认使用 `unix:///var/run/docker.sock`。 #### 使用自定义配置 在实际项目中,我们通常需要自定义连接参数,比如指定 Docker daemon 地址、设置超时时间等: ```java package space.anyi.docker; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.core.DockerClientImpl; import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientConfig; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; /** * Docker Java SDK 自定义配置示例 * 演示如何使用自定义参数创建 DockerClient */ public class QuickStart { private static final Logger log = LoggerFactory.getLogger(QuickStart.class); // 连接地址配置 private static final String UNIX_HOST = "unix://var/run/docker.sock"; private static final String TCP_HOST = "tcp://0.0.0.0:2375"; /** * 使用自定义配置连接 Docker * 可以指定连接地址、超时时间、最大连接数等参数 */ public void customStart() { // 步骤1:构建 DockerClientConfig 配置对象 DockerClientConfig dockerClientConfig = DefaultDockerClientConfig .createDefaultConfigBuilder() .withDockerHost(UNIX_HOST) // 指定 Docker daemon 地址 // 以下为可选的 TLS 配置 //.withDockerTlsVerify(true) //.withDockerCertPath("/home/user/.docker") // 以下为可选的 Registry 认证配置 //.withRegistryUsername(registryUser) //.withRegistryPassword(registryPass) //.withRegistryEmail(registryMail) //.withRegistryUrl(registryUrl) .build(); log.info("Docker 客户端配置: {}", dockerClientConfig); // 步骤2:构建 HTTP 传输层实例 DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() .dockerHost(dockerClientConfig.getDockerHost()) // 使用配置中的地址 //.sslConfig(dockerClientConfig.getSSLConfig()) // SSL 配置 .maxConnections(100) // 最大连接数 .connectionTimeout(Duration.ofSeconds(30)) // 连接超时时间 .responseTimeout(Duration.ofSeconds(45)) // 响应超时时间 .build(); log.info("HTTP 客户端配置: {}", httpClient); // 步骤3:根据自定义配置创建 DockerClient DockerClient dockerClient = DockerClientImpl.getInstance(dockerClientConfig, httpClient); // 测试连接 // 等价命令: docker version(连接自检) dockerClient.pingCmd().exec(); log.info("自定义配置连接成功!"); // 列出所有镜像 // 等价命令: docker images dockerClient.listImagesCmd().exec().forEach(image -> { log.info("Docker 镜像信息: {}", image); }); } } ``` 自定义配置的创建分为三个步骤: 1. 构建 `DockerClientConfig`:配置 Docker daemon 地址、TLS、Registry 认证等 2. 构建 `DockerHttpClient`:配置传输层参数,如连接数、超时时间等 3. 使用 `DockerClientImpl.getInstance()` 创建客户端实例 ### 连接测试 创建 DockerClient 后,我们可以使用 `pingCmd()` 方法测试连接是否正常。如果连接失败,SDK 会抛出 `DockerException` 异常: ```java try { // 等价命令: docker version(连接自检) dockerClient.pingCmd().exec(); log.info("Docker 连接成功!"); } catch (Exception e) { log.error("Docker 连接失败: {}", e.getMessage()); } ``` 我们还可以获取 Docker daemon 的版本信息,进一步验证连接: ```java // 获取 Docker 版本信息 // 等价命令: docker version String version = dockerClient.versionCmd().exec().getVersion(); log.info("Docker 版本: {}", version); // 获取 Docker 系统信息 // 等价命令: docker info Info info = dockerClient.infoCmd().exec(); log.info("Docker 系统信息: {}", info); ``` ### 拉取镜像 拉取镜像是使用 Docker 的基本操作之一。SDK 提供了 `pullImageCmd()` 方法来拉取镜像: ```java import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.command.PullImageResultCallback; import com.github.dockerjava.api.model.PullResponseItem; /** * 拉取 Docker 镜像 * @param imageName 镜像名称,如 "hello-world" 或 "nginx:latest" */ public void pullImage(String imageName) { try { // 构建并执行拉取命令 // 等价命令: docker pull nginx:latest // exec() 方法会阻塞直到镜像拉取完成 dockerClient.pullImageCmd(imageName) .exec(new PullImageResultCallback()) .awaitCompletion(); log.info("镜像 {} 拉取成功!", imageName); } catch (InterruptedException e) { log.error("镜像拉取被中断: {}", e.getMessage()); Thread.currentThread().interrupt(); } } ``` **注意:** `PullImageResultCallback` 是一个异步回调类,我们需要调用 `awaitCompletion()` 方法来等待拉取完成。如果不调用此方法,拉取操作会在后台异步执行,主线程可能无法获取到结果。 ### 创建容器并启动 拉取镜像后,我们可以使用该镜像创建并启动一个容器。这个过程分为两步:先创建容器(create),再启动容器(start): ```java import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.Ports; /** * 创建并启动一个容器 * @param imageName 镜像名称 * @param containerName 容器名称 */ public void createAndStartContainer(String imageName, String containerName) { // 步骤1:创建容器 // 创建容器只是分配资源和配置,并不会真正运行 // 等价命令: docker create --name demo-container hello-world CreateContainerResponse container = dockerClient.createContainerCmd(imageName) .withName(containerName) // 设置容器名称 .exec(); log.info("容器创建成功,ID: {}", container.getId()); // 步骤2:启动容器 // 等价命令: docker start demo-container // 上面两步合起来相当于: docker run --name demo-container hello-world dockerClient.startContainerCmd(container.getId()).exec(); log.info("容器 {} 启动成功!", containerName); } ``` 创建容器时,我们可以通过链式调用设置各种参数: ```java // 创建一个带有端口映射和环境变量的容器 CreateContainerResponse container = dockerClient.createContainerCmd("nginx:latest") .withName("my-nginx") .withEnv("NGINX_HOST=localhost", "NGINX_PORT=80") // 设置环境变量 .withExposedPorts(ExposedPort.tcp(80)) // 暴露端口 .withPortBindings(new Ports.Binding(8080, 80)) // 端口映射 .exec(); ``` ### 完整示例 下面是一个完整的示例,展示了从创建 DockerClient 到运行容器的整个流程: ```java package space.anyi.docker; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientImpl; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; /** * Docker Java SDK 快速入门完整示例 */ public class QuickStart { private static final Logger log = LoggerFactory.getLogger(QuickStart.class); private final DockerClient dockerClient; public QuickStart() { // 创建 DockerClient 实例 DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() .dockerHost(config.getDockerHost()) .maxConnections(100) .connectionTimeout(Duration.ofSeconds(30)) .responseTimeout(Duration.ofSeconds(45)) .build(); this.dockerClient = DockerClientImpl.getInstance(config, httpClient); } /** * 完整流程演示:连接 -> 拉取镜像 -> 创建容器 -> 启动容器 */ public void runDemo() { // 1. 测试连接 // 等价命令: docker version dockerClient.pingCmd().exec(); log.info("1. Docker 连接成功"); // 2. 拉取 hello-world 镜像 try { // 等价命令: docker pull nginx:latest dockerClient.pullImageCmd("hello-world") .exec(new com.github.dockerjava.api.command.PullImageResultCallback()) .awaitCompletion(); log.info("2. 镜像拉取成功"); } catch (InterruptedException e) { log.error("镜像拉取被中断"); Thread.currentThread().interrupt(); return; } // 3. 创建并启动容器 // 等价命令: docker create --name demo-container hello-world CreateContainerResponse container = dockerClient.createContainerCmd("hello-world") .withName("demo-container") .exec(); log.info("3. 容器创建成功,ID: {}", container.getId()); // 等价命令: docker start demo-container // 上面两步合起来相当于: docker run --name demo-container hello-world dockerClient.startContainerCmd(container.getId()).exec(); log.info("4. 容器启动成功"); } public static void main(String[] args) { new QuickStart().runDemo(); } } ``` 运行这段代码,你会看到 Docker 客户端成功连接、拉取镜像、创建并启动容器的完整过程。 **思考:** 为什么创建容器和启动容器要分成两步?直接一步完成不是更简单吗? 将创建和启动分开的设计允许我们在容器启动前进行更多配置,比如设置网络、挂载卷、配置资源限制等。这种设计也符合 Docker 的命令行操作习惯(`docker create` + `docker start`)。 *** ## 镜像管理相关API 镜像(Image)是 Docker 的核心概念之一,它是一个只读的模板,包含了运行应用所需的所有内容:代码、运行时、系统工具、库和配置。Docker Java SDK 提供了一系列镜像管理 API,本章我们来逐一学习。 ### listImagesCmd - 列出所有镜像 `listImagesCmd()` 方法用于列出 Docker daemon 上的镜像列表,对应 Docker CLI 的 `docker images` 命令。 ```java /** * 列出自定义节点上所有存在的镜像 * @return 镜像列表 */ public List listImages() { // 等价命令: docker images -a(列出所有镜像,包括中间层缩影) List images = dockerClient.listImagesCmd() .withShowAll(true) // 展示所有镜像(包括中间层) .exec(); log.info("本机共有 {} 个镜像", images.size()); return images; } ``` `Image` 对象包含了镜像的核心信息: | 字段 | 说明 | |------|------| | `getId()` | 镜像的完整 ID(sha256 哈希值) | | `getRepoTags()` | 镜像的仓库和标签列表,如 `["nginx:latest", "nginx:1.25"]` | | `getRepoDigests()` | 镜像摘要 | | `getSize()` | 镜像大小(字节) | | `getCreated()` | 创建时间戳 | | `getLabels()` | 镜像标签(Label) | | `getContainers()` | 使用该镜像的容器数量 | **思考:** 为什么 `listImagesCmd()` 默认不包含中间层镜像?`withShowAll(true)` 开启了什么? Docker 的镜像采用分层存储架构,一个镜像由多个 Layer 叠加而成。中间层镜像(dangling image)通常没有标签,它们只是构建过程中的临时产物。`withShowAll(true)` 会将这些中间层镜像也展示出来,方便排查磁盘占用问题。 ### inspectImageCmd - 获取镜像详细信息 当我们需要深入了解一个镜像的完整信息时,可以使用 `inspectImageCmd()` 方法: ```java /** * 获取镜像的详细信息 * 包括镜像的 ID、仓库、标签、创建时间、大小、Layer 等 * @param imageId 镜像 ID 或名称 * @return 镜像详情 */ public InspectImageResponse inspectImage(String imageId) { // 等价命令: docker inspect InspectImageResponse response = dockerClient.inspectImageCmd(imageId).exec(); log.info("镜像 {} 详情: {}", imageId, response); return response; } ``` `InspectImageResponse` 返回的信息比 `Image` 更为详细,主要包括: | 字段 | 说明 | |------|------| | `getId()` | 镜像 ID | | `getArch()` | 支持的 CPU 架构 | | `getOs()` | 操作系统类型 | | `getConfig()` | 镜像的配置信息(包括环境变量、入口命令等) | | `getContainerConfig()` | 构建镜像时的容器配置 | | `getRootFs()` | 镜像的文件系统层次 | | `getDockerVersion()` | 构建镜像时使用的 Docker 版本 | **思考:** `inspectImageCmd()` 和 `listImagesCmd()` 的返回信息有什么区别?什么场景下需要用 `inspect`? `listImagesCmd()` 返回的是简要信息,适合批量展示;`inspectImageCmd()` 返回的是完整 JSON 详情,适合获取镜像的配置细节,比如环境变量、入口命令、暴露端口等。在需要根据镜像配置来决定如何启动容器时,`inspect` 非常有用。 ### pullImageCmd - 拉取镜像(异步回调机制) `pullImageCmd()` 用于从远程仓库拉取镜像,对应 `docker pull` 命令。与前面几个方法不同,拉取镜像是一个**异步操作**,需要借助回调机制来处理结果: ```java /** * 拉取指定名称的镜像 * @param imageName 镜像名称 * @return 拉取是否成功 */ public boolean pullImage(String imageName) { try { // 等价命令: docker pull ubuntu:latest // exec() 执行命令,返回异步回调 // awaitCompletion() 阻塞等待镜像拉取完成 dockerClient.pullImageCmd(imageName) .exec(new PullImageResultCallback()) .awaitCompletion(); log.info("镜像 {} 拉取成功!", imageName); return true; } catch (InterruptedException e) { log.error("镜像拉取被中断: {}", e.getMessage()); Thread.currentThread().interrupt(); return false; } } ``` `PullImageResultCallback` 继承自 `ResultCallback.Adapter`,它会持续接收 Docker daemon 推送过来的拉取进度。`awaitCompletion()` 方法会阻塞当前线程,直到拉取完成或发生异常。 **思考:** 为什么要设计成异步回调模式?如果直接返回结果不是更简单吗? 镜像拉取可能是一个漫长的过程(大型镜像可能需要数分钟),如果将这个方法设计为同步阻塞,那么调用线程会被长时间占用。异步回调模式允许我们在等待拉取的同时执行其他操作,比如拉取多个镜像时,可以并发执行。 ### removeImageCmd - 删除镜像 删除镜像使用 `removeImageCmd()` 方法,对应 `docker rmi` 命令: ```java /** * 删除本地镜像 * @param imageId 镜像 ID 或名称 * @param force 是否强制删除(即使镜像被容器使用) */ public void removeImage(String imageId, boolean force) { // 等价命令: docker rmi // 等价命令: docker rmi -f (force=true 时强制删除) dockerClient.removeImageCmd(imageId) .withForce(force) // 强制删除 .exec(); log.info("镜像 {} 已删除", imageId); } ``` **注意:** 如果镜像正在被某个容器使用,普通删除会失败。此时需要设置 `force=true` 强制删除,或者先删除容器再删除镜像。 ### tagImageCmd - 标记镜像 `tagImageCmd()` 用于给镜像添加标签,对应 `docker tag` 命令,常用于给镜像重命名或在本地建立镜像副本: ```java /** * 为镜像添加标签(相当于 docker tag) * @param imageId 源镜像 ID 或名称 * @param repository 新的仓库名 * @param tag 新的标签名 */ public void tagImage(String imageId, String repository, String tag) { // 等价命令: docker tag : dockerClient.tagImageCmd(imageId, repository, tag).exec(); log.info("镜像 {} 已标记为 {}:{}", imageId, repository, tag); } ``` 例如,当我们拉取了 `nginx:latest` 镜像后,想将其标记为 `my-nginx:v1.0`: ```java api.tagImage("nginx:latest", "my-nginx", "v1.0"); ``` ### saveImageCmd / loadImageCmd - 导入导出镜像 Docker 还支持将镜像导出为 tar 文件,或者从 tar 文件导入镜像。这在离线部署场景中非常有用: ```java // 将镜像导出为 tar 文件 public void saveImage(String imageId, String filePath) { try (OutputStream outputStream = new FileOutputStream(filePath)) { // 等价命令: docker save -o nginx-backup.tar dockerClient.saveImageCmd(imageId).exec(outputStream); log.info("镜像 {} 已导出到 {}", imageId, filePath); } catch (IOException e) { log.error("导出镜像失败: {}", e.getMessage()); } } // 从 tar 文件加载镜像 public void loadImage(String filePath) { try (InputStream inputStream = new FileInputStream(filePath)) { // 等价命令: docker load -i nginx-backup.tar dockerClient.loadImageCmd(inputStream) .exec(new LoadImageResultCallback()) .awaitCompletion(); log.info("镜像已从 {} 加载成功", filePath); } catch (IOException | InterruptedException e) { log.error("加载镜像失败: {}", e.getMessage()); } } ``` **思考:** 镜像导出和导入在什么场景下会用到?与直接在网络上拉取镜像相比有什么优势? 镜像导出/导入适用于**离线环境**或**内网环境**,比如生产环境无法直接访问 Docker Hub 时,可以在有网络的机器上导出镜像,然后通过 U 盘或内网传输到目标机器上加载。这种方式不依赖网络连接,但文件传输本身需要额外的存储空间。 ### 镜像管理完整示例 ```java // 列出所有镜像 List images = api.listImages(); images.forEach(img -> log.info("镜像: {}, 大小: {}MB", Arrays.toString(img.getRepoTags()), img.getSize() / 1024 / 1024)); // 检查镜像是否存在 boolean exists = api.checkImageExists("nginx:latest"); if (!exists) { api.pullImage("nginx:latest"); // 拉取 } // 查看镜像详情 InspectImageResponse info = api.inspectImage("nginx:latest"); log.info("镜像架构: {}, 操作系统: {}", info.getArch(), info.getOs()); // 给镜像打标签 api.tagImage("nginx:latest", "my-nginx", "v1.0"); // 删除镜像 api.removeImage("my-nginx:v1.0", false); ``` *** ## 镜像构建相关API 前面我们学习了镜像的基本管理操作,本章将介绍如何使用 Docker Java SDK **构建自定义镜像**。镜像构建是 Docker 最强大的功能之一,它允许我们将应用打包成标准化的镜像,实现"一次构建,到处运行"。 ### buildImageCmd - 从 Dockerfile 构建镜像 `buildImageCmd()` 对应 `docker build` 命令,它需要指定构建上下文(包含 Dockerfile 和构建所需的文件)。 ```java /** * 从 Dockerfile 构建镜像 * @param dockerfilePath Dockerfile 文件路径 * @param imageName 构建目标镜像名称(repository:tag) * @return 构建成功返回镜像 ID,失败返回 null */ public String buildImage(String dockerfilePath, String imageName) { File dockerfile = new File(dockerfilePath); if (!dockerfile.exists()) { log.error("Dockerfile 不存在: {}", dockerfilePath); return null; } try { // 等价命令: docker build -f Dockerfile -t sdk-demo/hello:latest . // withDockerfile 指定 Dockerfile 文件位置 // withBaseDirectory 设置构建上下文(当前目录) // withTag 指定构建后的镜像名称和标签 // exec() 返回异步回调,awaitImageId() 阻塞等待构建完成并返回镜像 ID String imageId = dockerClient.buildImageCmd() .withDockerfile(dockerfile) .withBaseDirectory(dockerfile.getParentFile()) .withTag(imageName) .exec(new BuildImageResultCallback()) .awaitImageId(); log.info("镜像 {} 构建成功,镜像 ID: {}", imageName, imageId); return imageId; } catch (Exception e) { log.error("镜像构建失败: {}", e.getMessage()); return null; } } ``` 构建过程的关键参数: | 方法 | 说明 | |------|------| | `withDockerfile(File)` | 指定 Dockerfile 文件路径 | | `withBaseDirectory(File)` | 指定构建上下文目录(Dockerfile 中 COPY 指令从此目录读取文件) | | `withTag(String)` | 指定构建出的镜像名称和标签(如 `sdk-demo/hello:latest`) | | `withRemove(boolean)` | 构建完成后是否移除中间层容器 | | `withForcerm(boolean)` | 是否强制移除中间层容器(当构建失败时) | | `withBuildArg(String, String)` | 传递构建参数(对应 ARG 指令) | `BuildImageResultCallback` 是构建过程的异步回调,各个方法的含义: | 方法 | 说明 | |------|------| | `onNext(BuildResponseItem)` | 每收到一个构建事件时回调(如缓存命中、步骤执行等) | | `onError(Throwable)` | 构建出错时回调 | | `awaitImageId()` | 阻塞等待构建完成,返回镜像 ID(超时则抛出异常) | | `awaitCompletion()` | 阻塞等待构建完成,无返回值 | **思考:** 构建上下文(BaseDirectory)的作用是什么?为什么需要单独指定 Dockerfile 的位置? 构建上下文是 Docker 构建时能"看到"的所有文件所在的目录。当 Dockerfile 中使用 `COPY app.jar /app/` 时,这个 `app.jar` 就是从构建上下文中寻找的。将 Dockerfile 放在构建上下文之外(通过 `withDockerfile` 指定)是允许的,但 COPY 指令仍然只能访问构建上下文内的文件。 ### 构建流程详解 构建一个镜像的过程大致如下: ``` Dockerfile(构建指令) | v 构建上下文 (BaseDirectory) ──► Docker daemon 执行每个指令 | | 1. FROM alist666/alist:latest ← 拉取/复用基础镜像 | 2. RUN echo "..." > /info.txt ← 执行命令,创建新 Layer | v 构建完成的镜像 (imageName:tag) ``` 每次 `RUN`、`COPY` 等指令都会创建一个新的镜像 Layer,所有 Layer 叠加在一起就是最终的镜像。这个分层机制使得镜像可以被高效地缓存和复用。 ### 示例:构建一个带标识文件的自定义镜像 假设我们有如下 Dockerfile: ```dockerfile FROM registry.fit2cloud.com/halo/halo-pro:2.24 # 构建时添加一个标识文件,演示 Dockerfile 的 RUN 指令 RUN echo "Built by Docker Java SDK" > /build-info.txt # 容器启动时输出构建信息 CMD ["sh", "-c", "cat /build-info.txt"] ``` 使用我们封装好的 `ImageBuildAPI` 构建并验证: ```java ImageBuildAPI api = new ImageBuildAPI(); String imageId = api.buildImage("src/test/resources/Dockerfile", "sdk-demo/hello:latest"); log.info("构建结果: {}", imageId); // 验证镜像已存在 List images = api.findImageByName("sdk-demo/hello:latest"); log.info("找到 {} 个匹配镜像", images.size()); ``` 运行测试可以看到构建的输出: ``` 镜像 sdk-demo/hello:latest 构建成功,镜像 ID: sha256:xxxx 镜像 sdk-demo/hello:latest 是否存在: true ``` **思考:** 为什么 `withRemove(true)` 和 `withForcerm(true)` 的组合在 CI/CD 流水线中很重要? 在自动化构建过程中,如果每次构建都留下大量的中间层容器,会逐渐耗尽磁盘空间。`withRemove(true)` 确保正常完成后清理中间层,`withForcerm(true)` 则在构建失败时也能强制清理,避免残留。 *** ## 容器管理相关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) { // 创建容器:分配资源配置,但不会真正运行 // 等价命令: docker create --name my-container nginx:latest // 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 // 创建带环境变量的容器 // 等价命令: docker create --name my-container -e MYSQL_ROOT_PASSWORD=123456 nginx:latest dockerClient.createContainerCmd("nginx:latest") .withName("my-nginx") .withEnv("NGINX_HOST=localhost", "NGINX_PORT=80") .exec(); // 创建带端口映射的容器 // 等价命令: docker create --name my-container -p 8080:80 nginx:latest 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) { // 等价命令: docker start 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) { // 等价命令: docker stop -t 10 (先发 SIGTERM,超时后 SIGKILL) 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) { // 等价命令: docker kill (立即发送 SIGKILL,不给优雅退出机会) 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) { // 等价命令: docker rm // 等价命令: docker rm -f -v (同时强制停止并删除数据卷) dockerClient.removeContainerCmd(containerId) .withForce(force) // 强制删除运行中的容器 .withRemoveVolumes(removeVolumes) // 删除关联数据卷 .exec(); log.info("容器 {} 已删除", containerId); } ``` ### listContainersCmd - 列出容器 列出容器对应 `docker ps` 命令。默认只显示运行中的容器,`withShowAll(true)` 则显示所有容器(包括已停止的): ```java /** * 列出所有容器(包括停止的) * @return 容器列表 */ public List listContainers() { // 等价命令: docker ps -a(列出所有容器,包括已停止的) // withShowAll(true) 展示所有容器(不只运行中的) List 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) { // 等价命令: docker inspect (查看容器完整 JSON 详情) 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 前面我们学习了容器的基础管理(创建、启动、停止、删除),本章将深入讲解容器的**运维操作**:查看日志、在容器中执行命令、文件复制、等待容器退出等。这些操作是日常运维中最常用的功能。 ### logsCmd - 获取容器日志 获取容器日志对应 `docker logs` 命令。日志以**帧(Frame)**的方式流式返回,每帧包含数据类型和载荷: ```java /** * 获取容器日志(同步方式) * @param containerId 容器 ID * @param tailLines 只显示末尾的行数 * @return 日志文本 */ public String getContainerLogs(String containerId, int tailLines) { StringBuilder logBuilder = new StringBuilder(); try { // 等价命令: docker logs --tail 10 // withTail 只获取末尾 N 行 LogContainerCmd cmd = dockerClient.logContainerCmd(containerId) .withStdOut(true) // 获取标准输出 .withStdErr(true) // 获取错误输出 .withTail(tailLines); // 只取末尾几行 cmd.exec(new ResultCallback.Adapter<>() { @Override public void onNext(Frame frame) { // 日志以帧(Frame)形式返回,每帧包含不同类型的数据 if (frame.getStreamType() == FrameType.STDOUT || frame.getStreamType() == FrameType.STDERR || frame.getStreamType() == FrameType.RAW) { logBuilder.append(new String(frame.getPayload())); } } }).awaitCompletion(); return logBuilder.toString(); } catch (InterruptedException e) { log.error("获取容器日志被中断: {}", e.getMessage()); Thread.currentThread().interrupt(); return logBuilder.toString(); } } ``` 常用的日志选项: | 方法 | 说明 | |------|------| | `withStdOut(true)` | 获取标准输出流 | | `withStdErr(true)` | 获取标准错误流 | | `withTail(int)` | 只获取末尾 N 行(类似 `tail -n`) | | `withSince(long)` | 获取某个时间戳之后的日志 | | `withTimestamps(true)` | 在日志前添加时间戳 | | `withFollow(boolean)` | 是否持续跟随(类似 `-f`) | **思考:** 为什么日志要使用 Frame(帧)而不是纯文本?FrameType 的作用是什么? Docker 容器的日志实际上包含两个数据流:标准输出(STDOUT)和标准错误(STDERR)。如果只用纯文本,无法区分一行日志到底来自哪个流。Frame 的 `streamType` 字段标记了数据来源,这样上层可以根据需求选择性处理(比如只收集错误日志用于告警)。 ### execCreateCmd + execStartCmd - 在容器中执行命令 在运行中的容器内部执行命令,对应 `docker exec` 命令。它分为**两步**: 1. `execCreateCmd`:创建 exec 实例,描述要执行什么命令 2. `execStartCmd`:真正执行命令并接收输出 ```java /** * 在容器中执行命令(同步方式) * @param containerId 容器 ID * @param command 要执行的命令参数,如 ["ls", "-la"] 或 ["echo", "hello"] * @return 命令输出的文本 */ public String execCommandInContainer(String containerId, String... command) { StringBuilder output = new StringBuilder(); try { // 等价命令: docker exec ls -la / // 1. 创建 exec 实例(描述要在容器中执行什么命令) ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId) .withCmd(command) // 要执行的命令 .withAttachStdout(true) // 挂接标准输出 .withAttachStderr(true) // 挂接错误输出 .exec(); // 2. 启动 exec(真正执行命令,并接收输出) dockerClient.execStartCmd(execCreateCmdResponse.getId()) .exec(new ResultCallback.Adapter<>() { @Override public void onNext(Frame frame) { if (frame.getStreamType() == FrameType.STDOUT || frame.getStreamType() == FrameType.STDERR) { output.append(new String(frame.getPayload())); } } }).awaitCompletion(); return output.toString(); } catch (InterruptedException e) { log.error("执行容器命令被中断: {}", e.getMessage()); Thread.currentThread().interrupt(); return output.toString(); } } ``` **思考:** 为什么 `exec` 要分为 create 和 start 两步?为什么不一步到位? 这个设计参考了 fork/exec 的 POSIX 模型:create 阶段创建新的进程上下文(包括环境变量、工作目录、附加流),start 阶段真正在目标容器内启动这个进程。分离的好处是可以在真正执行前检查命令是否合法、权限是否足够,失败时可以提前阻止而不产生任何影响。 ### waitContainerCmd - 等待容器退出 `waitContainerCmd` 用于阻塞当前线程,直到容器退出并返回退出码: ```java /** * 等待容器退出 * @param containerId 容器 ID * @param timeoutSeconds 超时时间(秒) * @return 容器退出码(-1 表示超时) */ public int waitContainer(String containerId, int timeoutSeconds) { try { // 等价命令: docker wait (阻塞直到容器退出,返回退出码) WaitContainerResultCallback callback = dockerClient.waitContainerCmd(containerId) .exec(new WaitContainerResultCallback()); // 阻塞等待容器退出,设置超时 int exitCode = callback.awaitStatusCode(timeoutSeconds, TimeUnit.SECONDS); log.info("容器 {} 已退出,退出码: {}", containerId, exitCode); return exitCode; } catch (InterruptedException e) { log.error("等待容器退出被中断: {}", e.getMessage()); Thread.currentThread().interrupt(); return -1; } } ``` 这个 API 在 CI/CD 场景中非常有用:我们可以启动一个容器执行测试任务,然后等待它退出并检查退出码来判断测试是否通过。 ### 运维完整示例 一个完整的运维操作流程如下: ```java public void opsDemo() { String containerName = "ops-demo-" + System.currentTimeMillis(); try { // 1. 创建一个执行 echo 命令的容器 String id = createTemporaryContainer(containerName, "sh", "-c", "echo HelloOps && sleep 999"); log.info("1. 临时容器已创建并启动: {}", id); // 2. 在容器中执行命令 String result = execCommandInContainer(id, "ls", "-la", "/"); log.info("2. 容器内 ls -la / 输出:\n{}", result); // 3. 获取容器日志 String logs = getContainerLogs(id, 10); log.info("3. 容器日志:\n{}", logs); // 4. 清理容器 // 等价命令: docker rm -f (测试清理用) dockerClient.removeContainerCmd(id).withForce(true).exec(); log.info("4. 临时容器已清理"); } catch (Exception e) { log.error("运维演示失败: {}", e.getMessage()); } } ``` **思考:** 结合前面学习的 exec 与 logs 场景,在"容器内执行命令"获取到的输出与"查看容器日志"获取到的是否相同?它们有什么区别? `exec` 获取的是**在运行中的容器里新执行命令**的输出,相当于 `docker exec `;而 `logs` 获取的是**容器主进程**(PID 1)启动以来产生的日志,相当于 `docker logs `。两者针对的是不同数据源:exec 是临时命令的输出,logs 是容器自身的运行日志。 *** ## DockerClient 核心对象及 API 对照表 经过前面章节的学习,我们已经掌握了 Docker Java SDK 的主要使用方法。本章将对 `DockerClient` 接口的全部方法进行归类整理,形成一份**速查表**,方便日常开发时快速查找。 ### DockerClient 核心架构 `DockerClient` 是整个 SDK 的统一入口,所有 Docker 操作都通过它来分发。它的方法返回值是各种 `*Cmd` 接口,通过链式调用设置参数后,调用 `.exec()` 真正执行。 ```java // DockerClient 方法的调用模式 dockerClient.<命令方法>(<参数>) .<参数方法>(<值>) // 链式调用 .<参数方法>(<值>) .exec(); // 真正执行,返回响应或回调 ``` ### API 全局概览 以下表格列出了 `DockerClient` 接口的全部方法,按功能分类: #### 一、Docker Daemon 基本信息 | 方法 | 对应命令 | 说明 | |------|----------|------| | `pingCmd()` | `docker ping` | 检测 Docker daemon 连接是否正常 | | `infoCmd()` | `docker info` | 获取 Docker daemon 系统信息 | | `versionCmd()` | `docker version` | 获取 Docker 版本信息 | | `authCmd()` | — | 进行 Registry 认证 | | `eventsCmd()` | `docker events` | 实时监听 Docker 事件 | #### 二、镜像管理 | 方法 | 对应命令 | 说明 | |------|----------|------| | `listImagesCmd()` | `docker images` | 列出本地所有镜像 | | `inspectImageCmd(id)` | `docker inspect` (镜像) | 获取镜像完整详情 | | `pullImageCmd(name)` | `docker pull` | 拉取镜像(异步回调) | | `pushImageCmd(name)` | `docker push` | 推送镜像到 Registry | | `removeImageCmd(id)` | `docker rmi` | 删除本地镜像 | | `tagImageCmd(id, repo, tag)` | `docker tag` | 为镜像添加标签 | | `saveImageCmd(id)` | `docker save` | 将镜像导出为 tar 流 | | `saveImagesCmd()` | `docker save` (多个) | 批量导出多个镜像 | | `loadImageCmd(inputStream)` | `docker load` | 从 tar 流加载镜像 | | `searchImagesCmd(keyword)` | `docker search` | 搜索 Registry 中的镜像 | | `imageHistoryCmd(id)` | `docker history` | 查看镜像构建历史 | | `createImageCmd(name, stream)` | `docker import` | 从 tar 流创建镜像 | #### 三、镜像构建 | 方法 | 对应命令 | 说明 | |------|----------|------| | `buildImageCmd()` | `docker build` | 从 Dockerfile 构建镜像 | | `buildImageCmd(file)` | `docker build` | 指定 Dockerfile 构建 | | `buildImageCmd(inputStream)` | `docker build` | 从 tar 包构建镜像 | #### 四、容器生命周期管理 | 方法 | 对应命令 | 说明 | |------|----------|------| | `createContainerCmd(image)` | `docker create` | 创建容器(不启动) | | `startContainerCmd(id)` | `docker start` | 启动容器 | | `stopContainerCmd(id)` | `docker stop` | 优雅停止容器(先 SIGTERM,再 SIGKILL) | | `killContainerCmd(id)` | `docker kill` | 强制杀死容器(立即 SIGKILL) | | `restartContainerCmd(id)` | `docker restart` | 重启容器 | | `pauseContainerCmd(id)` | `docker pause` | 暂停容器(使用 cgroup freezer) | | `unpauseContainerCmd(id)` | `docker unpause` | 恢复已暂停容器 | | `removeContainerCmd(id)` | `docker rm` | 删除容器 | | `waitContainerCmd(id)` | `docker wait` | 阻塞等待容器退出(返回退出码) | | `renameContainerCmd(id)` | `docker rename` | 重命名容器 | | `updateContainerCmd(id)` | `docker update` | 更新容器运行时配置 | | `commitCmd(id)` | `docker commit` | 将容器状态保存为镜像 | | `topContainerCmd(id)` | `docker top` | 查看容器内运行的进程 | | `exportContainerCmd(id)` | `docker export` | 将容器文件系统导出为 tar | #### 五、容器查询 | 方法 | 对应命令 | 说明 | |------|----------|------| | `listContainersCmd()` | `docker ps` | 列出容器 | | `inspectContainerCmd(id)` | `docker inspect` (容器) | 获取容器完整详情 | | `containerDiffCmd(id)` | `docker diff` | 查看容器文件系统变更 | | `statsCmd(id)` | `docker stats` | 获取容器资源统计 | #### 六、容器运维 | 方法 | 对应命令 | 说明 | |------|----------|------| | `logContainerCmd(id)` | `docker logs` | 获取容器日志 | | `attachContainerCmd(id)` | `docker attach` | 连接到容器的 I/O 流 | | `execCreateCmd(id)` | `docker exec` (创建) | 在容器内创建命令会话 | | `execStartCmd(id)` | `docker exec` (启动) | 执行创建好的命令会话 | | `inspectExecCmd(id)` | — | 获取 exec 会话详情 | | `resizeExecCmd(id)` | — | 调整 exec TTY 大小 | | `copyArchiveFromContainerCmd(id, path)` | `docker cp` (取出) | 从容器复制文件到本地 | | `copyArchiveToContainerCmd(id)` | `docker cp` (放入) | 将本地文件复制到容器 | | `copyFileFromContainerCmd(id, path)` | `docker cp` (取出) | 从容器复制单个文件 | | `resizeContainerCmd(id)` | — | 调整容器 TTY 大小 | #### 七、数据卷(Volume) | 方法 | 对应命令 | 说明 | |------|----------|------| | `listVolumesCmd()` | `docker volume ls` | 列出所有数据卷 | | `inspectVolumeCmd(name)` | `docker volume inspect` | 查看卷详情 | | `createVolumeCmd()` | `docker volume create` | 创建数据卷 | | `removeVolumeCmd(name)` | `docker volume rm` | 删除数据卷 | #### 八、网络(Network) | 方法 | 对应命令 | 说明 | |------|----------|------| | `listNetworksCmd()` | `docker network ls` | 列出所有网络 | | `inspectNetworkCmd()` | `docker network inspect` | 查看网络详情 | | `createNetworkCmd()` | `docker network create` | 创建自定义网络 | | `removeNetworkCmd(id)` | `docker network rm` | 删除网络 | | `connectToNetworkCmd()` | `docker network connect` | 将容器接入网络 | | `disconnectFromNetworkCmd()` | `docker network disconnect` | 将容器移出网络 | ### 核心模型类对照 SDK 中的模型类与 Docker JSON API 的返回数据一一对应。以下是常用的模型类: #### Image 相关 | 模型类 | 用途 | 常用字段 | |--------|------|----------| | `Image` | 镜像简要信息 | `id`, `repoTags`, `size`, `created`, `labels` | | `InspectImageResponse` | 镜像完整详情 | `id`, `arch`, `os`, `config`, `rootFs` | | `ImageHistory` | 镜像构建历史 | `created`, `createdBy`, `size` | #### Container 相关 | 模型类 | 用途 | 常用字段 | |--------|------|----------| | `Container` | 容器简要信息 | `id`, `names`, `state`, `status`, `ports` | | `InspectContainerResponse` | 容器完整详情 | `id`, `name`, `state`, `config`, `networkSettings`, `mounts` | | `ContainerState` | 容器状态 | `running`, `status`, `exitCode`, `pid` | | `ContainerPort` | 端口映射 | `privatePort`, `publicPort`, `type` | | `Mount` | 挂载点 | `source`, `destination`, `mode`, `type` | #### 命令响应 | 模型类 | 用途 | 常用字段 | |--------|------|----------| | `CreateContainerResponse` | 创建容器响应 | `id`, `warnings` | | `ExecCreateCmdResponse` | 创建 exec 响应 | `id` | | `Version` | 版本信息 | `version`, `apiVersion`, `buildTime` | | `Info` | 系统信息 | `containers`, `images`, `serverVersion` | ### 回调类与异步机制 Docker SDK 中的 I/O 密集操作(拉取镜像、构建镜像、执行命令等)采用异步回调模式。核心回调类: | 回调类 | 用途 | 常用方法 | |--------|------|----------| | `ResultCallback.Adapter` | 通用异步回调适配器 | `onNext(T)`, `onComplete()`, `onError(Throwable)` | | `PullImageResultCallback` | 拉取镜像回调 | `awaitCompletion()` | | `BuildImageResultCallback` | 构建镜像回调 | `awaitImageId()` | | `WaitContainerResultCallback` | 等待容器回调 | `awaitStatusCode(timeout, unit)` | | `LogContainerResultCallback` | 获取日志回调 | 继承自 `ResultCallback`,逐帧回调 | | `ExecStartResultCallback` | 执行命令回调 | 继承自 `ResultCallback`,逐帧回调 | | `LoadImageResultCallback` | 加载镜像回调 | `awaitCompletion()` | ### 错误处理机制 SDK 中的所有 Docker 操作失败时,统一抛出 `DockerException` 的子类: | 异常类 | HTTP 状态码 | 说明 | |--------|-------------|------| | `NotFoundException` | 404 | 资源不存在(镜像/容器未找到) | | `ConflictException` | 409 | 资源冲突(如删除运行中的容器) | | `InternalServerErrorException` | 500 | Docker daemon 内部错误 | | `BadRequestException` | 400 | 请求参数不正确 | | `ForbiddenException` | 403 | 权限不足(Docker socket 权限或 Registry 认证失败) | 最佳实践:在调用 Docker API 时,始终用 try-catch 捕获异常并进行适当的错误处理: ```java try { dockerClient.pingCmd().exec(); log.info("Docker 连接正常"); } catch (DockerException e) { // 根据异常类型进行针对性处理 if (e instanceof NotFoundException) { log.error("请求的资源不存在"); } else if (e instanceof InternalServerErrorException) { log.error("Docker daemon 内部错误: {}", e.getMessage()); } else { log.error("Docker 操作失败: {}", e.getMessage()); } } catch (Exception e) { log.error("网络连接失败: {}", e.getMessage()); } ``` ### 本教程代码结构 | 类名 | 所在章节 | 说明 | |------|----------|------| | `QuickStart` | Chapter 2 | DockerClient 创建、连接、拉取镜像、创建启动容器 | | `DockerClientFactory` | Chapter 2 | DockerClient 工厂类,提供统一的实例创建方法 | | `ImageManageAPI` | Chapter 3 | 镜像管理(列出、查看详情、拉取、删除、标记、检查存在) | | `ImageBuildAPI` | Chapter 4 | 镜像构建(从 Dockerfile 构建、清理中间层) | | `ContainerManageAPI` | Chapter 5 | 容器管理(创建、启动、停止、删除、列出、查看详情) | | `ContainerOpsAPI` | Chapter 6 | 容器运维(日志、执行命令、复制文件、等待退出) | > Docker Java SDK 的 API 设计遵循"命令对象模式":每个操作对应一个 `*Cmd` 接口,通过 Builder 模式链式设置参数,最后通过 `.exec()` 统一执行。掌握这个模式后,即使遇到本教程未覆盖的 API,也能快速上手。