Java 应用程序的容器简介

容器为跨开发、测试和生产阶段的 Java 应用程序提供一致的可移植环境。 本文介绍 Java 应用程序的容器化概念,并指导你创建、调试、优化和将容器化 Java 应用程序部署到 Azure 容器应用。

在本文中,你将了解 Java 开发人员的基本容器化概念以及以下技能:

  • 为容器化 Java 应用程序设置开发环境。
  • 创建针对 Java 工作负载优化的 Dockerfiles。
  • 使用容器配置本地开发工作流。
  • 调试容器化 Java 应用程序。
  • 优化用于生产的 Java 容器。
  • 将容器化 Java 应用程序部署到 Azure 容器应用。

通过容器化 Java 应用程序,可以获得一致的环境、简化的部署、高效的资源利用率和改进的可伸缩性。

Java 应用程序的容器

容器使用其依赖项打包应用程序,确保环境之间的一致性。 对于 Java 开发人员,这意味着将应用程序、其依赖项、Java 运行时环境/Java 开发工具包(JRE/JDK)和配置文件捆绑到单个可移植单元中。

容器化在虚拟化上具有关键优势,因此非常适合云开发。 与虚拟机相比,容器在服务器的主机 OS 内核上运行。 这有利于已在 Java 虚拟机(JVM)中运行的 Java 应用程序。 容器化 Java 应用程序可增加最少的开销,并提供显著的部署优势。

容器生态系统包括以下关键组件:

  • 图像 - 蓝图。
  • 容器 - 运行实例。
  • 注册表 - 存储映像的位置。
  • 容器编排器 - 大规模管理容器的系统。

Docker 是最受欢迎的容器化平台,它通过 Azure 容器应用在 Azure 生态系统中得到很好的支持。

设置开发环境

本部分将指导你安装必要的工具和配置开发环境,以生成、运行和调试容器化 Java 应用程序。

安装所需的工具

若要容器化 Java 应用程序,需要在开发计算机上安装以下工具:

使用以下命令验证安装:

docker --version
docker compose version

配置 Visual Studio Code 进行容器开发

要进行容器中的 Java 开发,请通过安装 Java 扩展包和配置 JDK 来设置 Visual Studio Code。 使用开发容器扩展可以打开容器中的任何文件夹,并使用 Visual Studio Code 在该容器内的完整功能集。

若要使 Visual Studio Code 能够自动生成并连接到开发容器,请在项目中创建 .devcontainer/devcontainer.json 文件。

例如,以下示例配置定义了 Java 生成:

{
    "name": "Java Development",
    "image": "mcr.microsoft.com/devcontainers/java:21",
    "customizations": {
        "vscode": {
            "extensions": [
                "vscjava.vscode-java-pack",
                "ms-azuretools.vscode-docker"
            ]
        }
    },
    "forwardPorts": [8080, 5005],
    "remoteUser": "vscode"
}

此配置使用 Azure 的 Java 开发容器映像,添加基本扩展,并转发应用程序端口和 8080调试端口 5005

创建 Dockerfile

Dockerfile 包含有关生成 Docker 映像的说明。 对于 Java 应用程序, Dockerfile 通常包含以下组件:

  • 具有 JDK 或 JRE 的基本映像。
  • 复制应用程序文件的说明。
  • 用于设置环境变量的命令。
  • 入口点的配置。

选择基础映像

选择正确的基础映像至关重要。 请考虑以下选项:

DESCRIPTION 名称 注解
Azure Java 开发映像 mcr.microsoft.com/java/jdk:21-zulu-ubuntu 完整 JDK 并针对 Azure 进行优化
Azure Java 生产环境映像 mcr.microsoft.com/java/jre:21-zulu-ubuntu 仅限运行时并针对 Azure 进行优化
官方 OpenJDK 开发映像 openjdk:21-jdk 完整 JDK(Java 开发工具包)
官方 OpenJDK 生产映像 openjdk:21-jre 仅限运行时

对于开发环境,请使用完整的 JDK 映像。 在生产环境中,使用 JRE 或无操作系统镜像来最大程度地减少应用程序的大小和攻击面。

Azure Java 镜像内置了特定于 Azure 的优化,并定期更新安全补丁,使它们成为针对 Azure 容器应用的应用程序的理想选择。

基本 Dockerfile 示例

以下示例展示了一个用于 Java 应用程序的简单 Dockerfile

FROM mcr.microsoft.com/java/jdk:21-zulu-ubuntu
WORKDIR /app
COPY target/myapp.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

对于 Spring Boot 应用程序,可以将 Dockerfile 设置为以下基础:

FROM mcr.microsoft.com/java/jdk:21-zulu-ubuntu
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Dspring.profiles.active=docker", "-jar", "app.jar"]

对于生产部署,请使用以下示例中所示的 JRE 映像来减小大小并最大程度地减少应用程序的攻击面:

FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENV JAVA_OPTS="-Dserver.port=8080"
ENTRYPOINT ["java", ${JAVA_OPTS}, "-jar", "app.jar"]

使用容器进行本地开发

容器旨在在不同上下文中执行。 在本部分中,你将了解用于容器的本地开发流。

将 Docker Compose 用于多容器应用程序

大多数 Java 应用程序与数据库、缓存或其他服务进行交互。 Docker Compose 可帮助你使用简单的 YAML 配置文件定义和协调多容器应用程序。

什么是 Docker Compose?

Docker Compose 是一种工具,可用于执行以下任务:

  • 在单个文件中定义多容器应用程序。
  • 管理应用程序生命周期,包括启动、停止和重新生成。
  • 维护隔离的环境。
  • 创建用于服务通信的网络。
  • 使用卷持久化地保存数据。

示例:使用数据库的 Java 应用程序

以下 compose.yml 文件使用 PostgreSQL 数据库配置 Java 应用程序:

version: '3.8'
services:
  app:
    build: .                              # Build from Dockerfile in current directory
    ports:
      - "8080:8080"                       # Map HTTP port
      - "5005:5005"                       # Map debug port
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/myapp
    volumes:
      - ./target:/app/target              # Mount target directory for hot reloads
    depends_on:
      - db                                # Ensure database starts first

  db:
    image: postgres:13
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=myapp
    ports:
      - "5432:5432"                       # Expose PostgreSQL port
    volumes:
      - postgres-data:/var/lib/postgresql/data  # Persist database data

volumes:
  postgres-data:                          # Named volume for database persistence

此文件具有以下特征:

  • 服务可以按名称相互引用 - 例如, db 在 JDBC URL 中。
  • Docker Compose 会自动为服务创建网络。
  • Java 应用程序等待数据库启动,因为 depends_on
  • 使用命名卷在重启后,数据库数据会持续保留。

常见 Docker Compose 命令

创建 compose.yml 文件后,使用以下命令管理应用程序:

# Build images without starting containers
docker compose build

# Start all services defined in compose.yml
docker compose up

# Start in detached mode (run in background)
docker compose up -d

# View running containers managed by compose
docker compose ps

# View logs from all containers
docker compose logs

# View logs from a specific service
docker compose logs app

# Stop all services
docker compose down

# Stop and remove volumes (useful for database resets)
docker compose down -v

开发工作流

使用 Docker Compose 的典型 Java 开发工作流包含以下步骤:

  1. 创建 compose.yml 文件和 Dockerfile
  2. 运行 docker compose up 以启动所有服务。
  3. 对 Java 代码进行更改。
  4. 重新生成应用程序。 根据配置,可能需要重启容器。
  5. 测试容器化环境中的更改。
  6. 完成后,运行 docker compose down

使用 Docker 运行单个容器

对于不需要多个互连服务的更简单方案,可以使用 docker run 命令启动单个容器。

以下 Docker 命令适用于 Java 应用程序:

# Run a Java application JAR directly
docker run -p 8080:8080 myapp:latest

# Run with environment variables
docker run -p 8080:8080 -e "SPRING_PROFILES_ACTIVE=prod" myapp:latest

# Run in detached mode (background)
docker run -d -p 8080:8080 myapp:latest

# Run with a name for easy reference
docker run -d -p 8080:8080 --name my-java-app myapp:latest

# Run with volume mount for persistent data
docker run -p 8080:8080 -v ./data:/app/data myapp:latest

调试容器化应用程序

调试容器化 Java 应用程序有时很有挑战性,因为代码在容器内的隔离环境中运行。

标准调试方法并不总是直接应用,但配置正确,可以建立与应用程序的远程调试连接。 本部分介绍如何配置容器进行调试,将开发工具连接到正在运行的容器,以及如何排查与容器相关的常见问题。

设置远程调试

调试容器化 Java 应用程序需要公开调试端口并配置 IDE 以连接到它。 可以使用以下步骤完成这些任务:

  1. 若要启用调试,请修改 Dockerfile ,使其包含以下内容:

    注释

    相反,您可以修改容器的启动命令。

    FROM mcr.microsoft.com/java/jdk:21-zulu-ubuntu
    WORKDIR /app
    COPY target/*.jar app.jar
    EXPOSE 8080 5005
    ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-jar", "app.jar"]
    
  2. 配置 Visual Studio Code 的 launch.json 文件以连接到调试端口,如以下示例所示:

    {
      "version": "0.2.0",
      "configurations": [
        {
          "type": "java",
          "name": "Debug in Container",
          "request": "attach",
          "hostName": "localhost",
          "port": 5005
        }
      ]
    }
    
  3. 使用映射到主机的端口 5005 启动容器,然后在 Visual Studio Code 中启动调试器。

排查容器问题

当容器不按预期运行时,可以检查应用的日志以调查问题。

使用以下命令对应用程序进行故障排除。 在运行这些命令之前,请确保将占位符 (<...>) 替换为自己的值。

# View logs
docker logs <CONTAINER_ID>

# Follow logs in real-time
docker logs -f <CONTAINER_ID>

# Inspect container details
docker inspect <CONTAINER_ID>

# Get a shell in the container
docker exec -it <CONTAINER_ID> bash

对于特定于 Java 的问题,请启用 JVM 标志以便进行更好的诊断,如以下示例所示:

ENTRYPOINT ["java", "-XX:+PrintFlagsFinal", "-XX:+PrintGCDetails", "-jar", "app.jar"]

下表列出了常见问题和相应的解决方案:

错误 可能的解决方法
内存不足 增加容器内存限制
连接超时 检查网络配置是否存在错误。 验证端口和路由规则。
权限问题 验证文件系统权限。
Classpath 问题 检查 JAR 结构和依赖项。

优化 Java 容器

容器中的 Java 应用程序需要特别考虑以获得最佳性能。 JVM 是在容器通用之前设计的。 如果未正确配置容器,则使用容器可能会导致资源分配问题。

通过微调内存设置、优化映像大小和配置垃圾回收,可以显著提高容器化 Java 应用程序的性能和效率。 本部分介绍 Java 容器的基本优化,重点介绍内存管理、启动时间和资源利用率。

容器中的 JVM 内存配置

JVM 不会自动检测 Java 8 中的容器内存限制。 对于 Java 9+,默认情况下会启用容器感知。

将 JVM 配置为遵循容器限制,如以下示例所示:

FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

以下 JVM 标志对于容器化应用程序非常重要:

  • -XX:MaxRAMPercentage=75.0。 将最大堆的大小设置为可用内存的百分比。
  • -XX:InitialRAMPercentage=50.0。 设置初始堆大小。
  • -Xmx-Xms。 这些标志也可用,但它们需要固定值。

准备生产部署

将容器化 Java 应用程序移动到生产环境需要除基本功能之外考虑事项。

生产环境需要可靠的安全性、可靠的监视、适当的资源分配和配置灵活性。

本部分介绍准备 Java 容器以供生产使用所需的基本做法和配置。 本部分重点介绍安全、运行状况检查和配置管理,以确保应用程序在生产环境中可靠运行。

安全最佳做法

使用以下做法保护容器化 Java 应用程序:

  • 默认安全上下文。 以非根用户身份运行应用程序,如以下示例所示:

    FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu
    WORKDIR /app
    COPY target/*.jar app.jar
    RUN addgroup --system javauser && adduser --system --ingroup javauser javauser
    USER javauser
    ENTRYPOINT ["java", "-jar", "app.jar"]
    
  • 主动查找问题。 使用以下命令定期扫描容器映像中是否存在漏洞:

    docker scan myapp:latest
    
  • 基本图像新鲜度。 使基础映像保持最新状态。

  • 机密管理。 实现适当的机密管理。 例如,不要将敏感数据硬编码到应用程序中,并尽可能使用密钥保管库。

  • 受限的安全上下文。 将最小特权原则应用于所有安全上下文。

  • 文件系统访问。 尽可能使用只读文件系统。

健康检查和监控

使用 探测 检查应用程序运行状况,以确保应用程序正常运行。

对于 Spring Boot 应用程序,添加 Actuator 依赖项以提供全面的健康检查端点,如以下示例所示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

将应用程序配置为以适合容器环境(如 JSON)的格式输出日志。

部署到 Azure 容器应用

本部分将指导你为 Azure 容器应用部署准备 Java 容器,并重点介绍关键配置注意事项。

为 Azure 准备容器

  • 端口配置。 确保您的容器监听 Azure 平台提供的端口,如以下示例所示:

    FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu
    WORKDIR /app
    COPY target/*.jar app.jar
    ENV PORT=8080
    EXPOSE ${PORT}
    CMD java -jar app.jar --server.port=${PORT}
    
  • 探测健康状况。 为 Azure 的运行情况和就绪情况检查实现 运行状况探测

  • 日志配置。 将 日志记录 配置为输出到 stdout/stderr

  • 为意外情况做好准备。 使用超时配置设置适当的正常关闭处理。 有关详细信息,请参阅 Azure 容器应用中的应用程序生命周期管理