使用容器进行 Java 开发

前提条件

按照 容器化你的应用 中的步骤完成应用容器化。

概述

在本节中,你将逐步为上一节中已容器化的应用设置本地开发环境。这包括:

  • 添加本地数据库并持久化数据
  • 创建用于连接调试器的开发容器
  • 配置 Compose 以在你编辑和保存代码时自动更新正在运行的 Compose 服务

添加本地数据库并持久化数据

你可以使用容器来设置本地服务,例如数据库。在本节中,你将更新 docker-compose.yaml 文件来定义一个数据库服务和一个用于持久化数据的卷(volume)。此外,这个特定的应用使用一个系统属性来定义数据库类型,因此你还需要更新 Dockerfile,在启动应用时传入该系统属性。

在克隆的仓库目录下,用 IDE 或文本编辑器打开 docker-compose.yaml 文件。你的 Compose 文件有一个示例数据库服务,但你需要对其进行一些修改以适配你的特定应用。

docker-compose.yaml 文件中,你需要执行以下操作:

  • 取消注释所有数据库相关的指令。现在你将使用数据库服务而不是本地存储来存放数据。
  • 移除顶层 secrets 元素以及 db 服务内部的该元素。本示例使用环境变量来设置密码,而不是 secrets。
  • 移除 db 服务中的 user 元素。本示例在环境变量中指定了用户。
  • 更新数据库环境变量。这些变量由 Postgres 镜像定义。有关更多详情,请参阅 Postgres 官方 Docker 镜像
  • 更新 db 服务的健康检查(healthcheck)测试并指定用户。默认情况下,健康检查使用 root 用户而不是你定义的 petclinic 用户。
  • server 服务中将数据库 URL 作为环境变量添加。这将覆盖 spring-petclinic/src/main/resources/application-postgres.properties 文件中定义的默认值。

以下是更新后的 docker-compose.yaml 文件。所有注释均已移除。

services:
  server:
    build:
      context: .
    ports:
      - 8080:8080
    depends_on:
      db:
        condition: service_healthy
    environment:
      - POSTGRES_URL=jdbc:postgresql://db:5432/petclinic
  db:
    image: postgres
    restart: always
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=petclinic
      - POSTGRES_USER=petclinic
      - POSTGRES_PASSWORD=petclinic
    ports:
      - 5432:5432
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "petclinic"]
      interval: 10s
      timeout: 5s
      retries: 5
volumes:
  db-data:

用 IDE 或文本编辑器打开 Dockerfile 文件。在 ENTRYPOINT 指令中,更新该指令,以便传入 spring-petclinic/src/resources/db/postgres/petclinic_db_setup_postgres.txt 文件中指定的系统属性。

- ENTRYPOINT [ "java", "org.springframework.boot.loader.launch.JarLauncher" ]
+ ENTRYPOINT [ "java", "-Dspring.profiles.active=postgres", "org.springframework.boot.loader.launch.JarLauncher" ]

保存并关闭所有文件。

现在,运行以下 docker compose up 命令来启动你的应用。

$ docker compose up --build

打开浏览器并在 http://localhost:8080 查看应用。你应该会看到一个简单的宠物诊所应用。浏览一下应用。导航到 Veterinarians 页面,通过查看兽医列表来验证应用是否已连接到数据库。

在终端中,按 ctrl+c 停止应用。

开发用 Dockerfile

你当前的 Dockerfile 非常适合构建一个小型、安全的生产镜像,该镜像只包含运行应用所需的组件。进行开发时,你可能需要一个具有不同环境的镜像。

例如,在开发镜像中,你可能希望将镜像配置为启动应用,以便可以将调试器连接到正在运行的 Java 进程。

你无需管理多个 Dockerfile,而可以添加一个新的 stage(阶段)。这样,你的 Dockerfile 既可以生成用于生产环境的最终镜像,也可以生成用于开发环境的镜像。

将你的 Dockerfile 内容替换为以下内容。

# syntax=docker/dockerfile:1

FROM eclipse-temurin:21-jdk-jammy as deps
WORKDIR /build
COPY --chmod=0755 mvnw mvnw
COPY .mvn/ .mvn/
RUN --mount=type=bind,source=pom.xml,target=pom.xml \
    --mount=type=cache,target=/root/.m2 ./mvnw dependency:go-offline -DskipTests

FROM deps as package
WORKDIR /build
COPY ./src src/
RUN --mount=type=bind,source=pom.xml,target=pom.xml \
    --mount=type=cache,target=/root/.m2 \
    ./mvnw package -DskipTests && \
    mv target/$(./mvnw help:evaluate -Dexpression=project.artifactId -q -DforceStdout)-$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout).jar target/app.jar

FROM package as extract
WORKDIR /build
RUN java -Djarmode=layertools -jar target/app.jar extract --destination target/extracted

FROM extract as development
WORKDIR /build
RUN cp -r /build/target/extracted/dependencies/. ./
RUN cp -r /build/target/extracted/spring-boot-loader/. ./
RUN cp -r /build/target/extracted/snapshot-dependencies/. ./
RUN cp -r /build/target/extracted/application/. ./
ENV JAVA_TOOL_OPTIONS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000
CMD [ "java", "-Dspring.profiles.active=postgres", "org.springframework.boot.loader.launch.JarLauncher" ]

FROM eclipse-temurin:21-jre-jammy AS final
ARG UID=10001
RUN adduser \
    --disabled-password \
    --gecos "" \
    --home "/nonexistent" \
    --shell "/sbin/nologin" \
    --no-create-home \
    --uid "${UID}" \
    appuser
USER appuser
COPY --from=extract build/target/extracted/dependencies/ ./
COPY --from=extract build/target/extracted/spring-boot-loader/ ./
COPY --from=extract build/target/extracted/snapshot-dependencies/ ./
COPY --from=extract build/target/extracted/application/ ./
EXPOSE 8080
ENTRYPOINT [ "java", "-Dspring.profiles.active=postgres", "org.springframework.boot.loader.launch.JarLauncher" ]

保存并关闭 Dockerfile

Dockerfile 中,你添加了一个基于 extract stage 的新 stage,标记为 development。在此 stage 中,你将提取的文件复制到一个通用目录,然后运行命令来启动应用。在该命令中,你暴露了 8000 端口并声明了 JVM 的调试配置,以便你可以附加调试器。

使用 Compose 进行本地开发

当前的 Compose 文件不会启动你的开发容器。要实现这一点,你必须更新 Compose 文件以指定 development stage。此外,还要更新 server 服务的端口映射,以便为调试器提供访问权限。

打开 docker-compose.yaml 文件,并添加以下指令。

services:
  server:
    build:
      context: .
      target: development
    ports:
      - 8080:8080
      - 8000:8000
    depends_on:
      db:
        condition: service_healthy
    environment:
      - POSTGRES_URL=jdbc:postgresql://db:5432/petclinic
  db:
    image: postgres
    restart: always
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=petclinic
      - POSTGRES_USER=petclinic
      - POSTGRES_PASSWORD=petclinic
    ports:
      - 5432:5432
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "petclinic"]
      interval: 10s
      timeout: 5s
      retries: 5
volumes:
  db-data:

现在,启动你的应用并确认其正在运行。

$ docker compose up --build

最后,测试你的 API 端点。运行以下 curl 命令:

$ curl  --request GET \
  --url http://localhost:8080/vets \
  --header 'content-type: application/json'

你应该收到以下响应:

{
  "vetList": [
    {
      "id": 1,
      "firstName": "James",
      "lastName": "Carter",
      "specialties": [],
      "nrOfSpecialties": 0,
      "new": false
    },
    {
      "id": 2,
      "firstName": "Helen",
      "lastName": "Leary",
      "specialties": [{ "id": 1, "name": "radiology", "new": false }],
      "nrOfSpecialties": 1,
      "new": false
    },
    {
      "id": 3,
      "firstName": "Linda",
      "lastName": "Douglas",
      "specialties": [
        { "id": 3, "name": "dentistry", "new": false },
        { "id": 2, "name": "surgery", "new": false }
      ],
      "nrOfSpecialties": 2,
      "new": false
    },
    {
      "id": 4,
      "firstName": "Rafael",
      "lastName": "Ortega",
      "specialties": [{ "id": 2, "name": "surgery", "new": false }],
      "nrOfSpecialties": 1,
      "new": false
    },
    {
      "id": 5,
      "firstName": "Henry",
      "lastName": "Stevens",
      "specialties": [{ "id": 1, "name": "radiology", "new": false }],
      "nrOfSpecialties": 1,
      "new": false
    },
    {
      "id": 6,
      "firstName": "Sharon",
      "lastName": "Jenkins",
      "specialties": [],
      "nrOfSpecialties": 0,
      "new": false
    }
  ]
}

连接调试器

你将使用 IntelliJ IDEA 自带的调试器。你可以使用此 IDE 的社区版。在 IntelliJ IDEA 中打开你的项目,转到 Run 菜单,然后选择 Edit Configuration。添加一个新的类似如下的 Remote JVM Debug 配置:

Java Connect a Debugger

设置一个断点。

打开 src/main/java/org/springframework/samples/petclinic/vet/VetController.java 文件,并在 showResourcesVetList 函数内部添加一个断点。

要开始调试会话,选择 Run 菜单,然后选择 Debug NameOfYourConfiguration

Debug menu

现在,你应该能在 Compose 应用的日志中看到连接信息。

Compose log file

现在你可以调用 server 端点了。

$ curl --request GET --url http://localhost:8080/vets

你应该已经看到代码在标记的行中断下,现在你可以像往常一样使用调试器了。你还可以检查和监视变量、设置条件断点、查看堆栈跟踪等等。

Debugger code breakpoint

在终端中按 ctrl+c 停止应用。

自动更新服务

使用 Compose Watch 在你编辑和保存代码时自动更新正在运行的 Compose 服务。有关 Compose Watch 的更多详情,请参阅 使用 Compose Watch

用 IDE 或文本编辑器打开 docker-compose.yaml 文件,然后添加 Compose Watch 指令。以下是更新后的 docker-compose.yaml 文件。

services:
  server:
    build:
      context: .
      target: development
    ports:
      - 8080:8080
      - 8000:8000
    depends_on:
      db:
        condition: service_healthy
    environment:
      - POSTGRES_URL=jdbc:postgresql://db:5432/petclinic
    develop:
      watch:
        - action: rebuild
          path: .
  db:
    image: postgres
    restart: always
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=petclinic
      - POSTGRES_USER=petclinic
      - POSTGRES_PASSWORD=petclinic
    ports:
      - 5432:5432
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "petclinic"]
      interval: 10s
      timeout: 5s
      retries: 5
volumes:
  db-data:

运行以下命令,使用 Compose Watch 运行你的应用。

$ docker compose watch

打开 Web 浏览器并在 http://localhost:8080 查看应用。你应该会看到 Spring Pet Clinic 的主页。

你对本地机器上应用源代码文件的任何更改现在都将自动反映在正在运行的容器中。

用 IDE 或文本编辑器打开 spring-petclinic/src/main/resources/templates/fragments/layout.html 文件,通过添加感叹号来更新 Home 导航字符串。

-   <li th:replace="~{::menuItem ('/','home','home page','home','Home')}">
+   <li th:replace="~{::menuItem ('/','home','home page','home','Home!')}">

保存对 layout.html 的更改,然后你可以继续开发,同时容器会自动重建。

容器重建并运行后,刷新 http://localhost:8080,然后验证菜单中是否显示了 Home!

在终端中按 ctrl+c 停止 Compose Watch。

总结

在本节中,我们探讨了如何在本地运行数据库并持久化数据。你还创建了一个包含 JDK 并允许你附加调试器的开发镜像。最后,你设置了 Compose 文件来暴露调试端口,并配置了 Compose Watch 以实现代码更改的实时重载。

相关信息

下一步

在下一节中,你将了解如何在 Docker 中运行单元测试。

页面选项