51、原生编译的 Java 微服务:从构建到测试的全面指南

原生编译的 Java 微服务:从构建到测试的全面指南

1. 构建 Docker 镜像

可以使用现有的 Gradle 任务 bootBuildImage 来创建 Docker 镜像。若在构建文件中声明了 GraalVM 的 Gradle 插件, bootBuildImage 任务将创建一个包含原生镜像的 Docker 镜像,而非使用应用 JAR 文件和 Java VM 的镜像。原生镜像将在 Docker 容器中构建,以确保其适用于 Linux 系统。使用 bootBuildImage 任务时,无需安装 GraalVM 的原生镜像编译器,该任务会使用 buildpacks 而非 Dockerfile 来创建 Docker 镜像。

Spring Boot 使用 Paketo 项目的 buildpacks 来创建 OCI 镜像。不过,在撰写本文时,Paketo 的 buildpacks 不支持 arm64 架构,包括 Apple silicon。虽然基于 amd64(Intel)的 buildpacks 技术上可以在配备 Apple silicon 的 MacBook 上运行,但速度极慢。可以使用非官方的 arm64 Docker 镜像作为临时解决方案,这些镜像可在 https://hub.docker.com/r/dashaun/builder-arm 找到。

创建本地操作系统的原生镜像比创建 Docker 镜像更快,因此在最初尝试成功构建原生镜像时,可以使用 nativeImage 任务进行快速反馈。但一旦成功构建,创建包含原生镜像的 Docker 镜像则是测试原生编译微服务的最佳选择,可以结合 Docker Compose 或 Kubernetes 使用。本文将使用上述非官方 Docker 镜像 dashaun/builder:tiny ,它同时提供了 arm64 和 amd64 架构的镜像。

2. 处理原生编译问题

原生编译 Spring Boot 应用目前尚未成为主流,因此在自行尝试时可能会遇到问题。以下是一些可用于处理这些问题的项目和工具:
- Spring AOT 冒烟测试 :该项目包含一套测试,用于验证各种 Spring 项目在原生编译时是否正常工作。当遇到 Spring 功能原生编译问题时,可在此项目中寻找解决方案。若要报告 Spring 项目原生编译问题,也可使用该项目的测试作为模板以可重现的方式展示问题。项目地址为 https://github.com/spring-projects/spring-aot-smoke-tests ,测试结果可在 Spring 的 CI 环境中查看,例如各种 Spring Cloud 项目的测试结果可在 https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-3.0.x?group=cloud-app-tests 找到。
- GraalVM 可达性元数据仓库 :该项目包含各种尚未支持原生编译的开源项目的可达性元数据。GraalVM 社区可提交可达性元数据,经项目团队审核后批准。GraalVM 的 Gradle 插件在原生编译时会自动查找并添加该项目中的可达性元数据。更多信息可查看 https://graalvm.github.io/native-build-tools/0.9.18/gradle-plugin.html#metadata-support
- 使用 Java VM 测试 AOT 生成的代码 :由于原生编译 Spring Boot 应用需要几分钟时间,一个有趣的替代方法是在 Java VM 上尝试 Spring AOT 引擎生成的初始化代码。通常在使用 Java VM 时会忽略 AOT 生成的代码,但可通过将系统属性 spring.aot.enabled 设置为 true 来改变这一点。这意味着应用的正常基于反射的初始化将被执行生成的初始化代码所取代,可用于快速验证生成的初始化代码是否按预期工作,还能使应用启动速度略有提升。
- 提供自定义提示 :若应用需要为 GraalVM 原生镜像编译器提供自定义可达性元数据以创建原生镜像,可按照 GraalVM 项目介绍部分的描述以 JSON 文件形式提供。Spring 提供了替代 JSON 文件的方法,可使用 @RegisterReflectionForBinding 注解或在类中实现 RuntimeHintsRegistrar 接口,并通过 @ImportRuntimeHints 注解激活。 RegisterReflectionForBinding 注解更易于使用,而实现 RuntimeHintsRegistrar 接口则能完全控制指定的提示。使用 Spring 的自定义提示比使用 GraalVM JSON 文件的一个重要优势是,自定义提示是类型安全的,并由编译器进行检查。若 GraalVM JSON 文件中引用的实体被重命名,但 JSON 文件未更新,该元数据将丢失,导致 GraalVM 原生镜像编译器无法创建原生镜像。而使用自定义提示时,源代码甚至无法编译,通常在实体重命名后,IDE 会立即提示自定义提示不再有效。
- 运行原生测试 :尽管使用 Java VM 测试 AOT 生成的代码可以快速判断原生编译是否可行,但仍需创建应用的原生镜像以进行全面测试。基于创建原生镜像、启动应用并手动运行测试的反馈循环速度慢且容易出错。一个更好的替代方法是运行原生测试,Spring 的 Gradle 插件会自动创建原生镜像,并使用该原生镜像运行应用项目中定义的 JUnit 测试。虽然由于原生编译仍需时间,但该过程完全自动化且可重复。确保原生测试按预期运行后,可将其放入 CI 构建管道进行自动执行。可使用以下 Gradle 命令启动原生测试:

gradle nativeTest
  • 使用 GraalVM 的跟踪代理 :若难以确定创建应用的有效原生镜像所需的可达性元数据和/或自定义提示,GraalVM 的跟踪代理可提供帮助。在 Java VM 中运行应用时启用跟踪代理,它可根据应用对反射、资源和代理的使用情况收集所需的可达性元数据。若与 JUnit 测试一起运行,收集所需的可达性元数据将是自动化且可重复的。
3. 源代码的更改

在将微服务中的 Java 源代码编译为原生可执行镜像之前,需要对源代码进行一些更新,以实现原生编译,具体更改如下:
- 更新 Gradle 构建文件 :在每个微服务项目的 build.gradle 文件中进行以下更改:
- 添加 GraalVM 插件以启用 Spring AOT 任务:

plugins {
    ...
    id 'org.graalvm.buildtools.native' version '0.9.18'
}
- 配置 `bootBuildImage` 任务以指定创建的 Docker 镜像名称,使用与之前相同的命名约定,但在镜像名称前加上 `native-` 以与现有 Docker 镜像区分,并指定支持 arm64 的构建器 Docker 镜像 `dashaun/builder:tiny`。以产品微服务为例,配置如下:
tasks.named('bootBuildImage') {
    imageName = "hands-on/native-product-service"
    builder = "dashaun/builder:tiny"
}
- 为解决原生编译中的一些问题,将 Spring Boot 从 v3.0.4 升级到 v3.0.5,将 `springdoc-openapi` 从 v2.0.2 升级到 v2.1.0。需要注意的是,不能将 `spring.aot.enabled` 指定为环境变量或在属性文件中设置,必须在 `java` 命令中作为系统属性设置。
- 由于 [https://github.com/spring-projects/spring-boot/issues/33238](https://github.com/spring-projects/spring-boot/issues/33238) 中描述的问题,不再禁用 `jar` 任务。
  • 添加可达性元数据和自定义提示 :在源代码中,GraalVM 原生编译器在某些情况下需要帮助才能正确编译源代码。例如,微服务使用的基于 JSON 的 API 和消息,JSON 解析器 Jackson 必须能够根据微服务接收到的 JSON 文档创建 Java 对象,Jackson 使用反射来执行此工作,因此需要告知原生编译器 Jackson 将应用反射的类。以 Product 类为例,原生提示如下:
@RegisterReflectionForBinding({ Event.class, ZonedDateTimeSerializer.class, Product.class})
public class ProductServiceApplication {

所有必要的自定义提示注解已添加到每个微服务的主类中。在撰写本文时,Resilience4J 注解在原生编译时无法正常工作,在 issue #1882 中提出了通过实现 RuntimeHintsRegistrar 接口来解决此问题的方案,并已应用于产品服务项目的 NativeHintsConfiguration 类中,该类的核心部分如下:

@Configuration
@ImportRuntimeHints(NativeHintsConfiguration.class)
public class NativeHintsConfiguration implements RuntimeHintsRegistrar {
  @Override
  public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
    hints.reflection().registerType(CircuitBreakerAspect.class,
      builder -> builder.withMembers(INVOKE_DECLARED_METHODS));
    hints.reflection().registerType(RetryAspect.class,
      builder -> builder.withMembers(INVOKE_DECLARED_METHODS));
    hints.reflection().registerType(TimeLimiterAspect.class,
      builder -> builder.withMembers(INVOKE_DECLARED_METHODS));
  }
}

从上述源代码可以看出,为产品组合微服务中使用的三个注解(断路器、重试和时间限制器)注册了提示,该类还通过 ImportRuntimeHints 注解导入自身来提供必要的配置。最后,还需要为 Resilience4J 在重试机制声明中使用的反射提供可达性元数据,配置如下:

resilience4j.retry:
  instances:
    product:
      maxAttempts: 3
      waitDuration: 1000
      retryExceptions:
      - org.springframework.web.reactive.function.client.WebClientResponseException$InternalServerError

此配置将使重试机制能够重试 InternalServerError 类型的错误。为了让 GraalVM 原生镜像编译器知道必须为该类启用反射,使用了第三种提供可达性元数据的方式:通过提供 GraalVM 配置文件,可在产品组合项目的 src/main/resources/META-INF/Native Image/reflect-config.json 中找到。

4. 启用 Spring Bean

由于静态分析在构建时使用的封闭世界假设,运行时所需的所有 Spring Bean 必须在构建时可达,否则无法在运行时激活。若使用仅在设置某些配置文件或满足某些条件(使用 @Profile @ConditionalOnProperty 注解)时才创建的 Spring Bean,必须确保在构建时满足这些配置文件和条件。

例如,使用原生编译的微服务时,若要在运行时指定单独的管理端口,必须在构建时将管理端口设置为随机端口(与标准端口不同)。因此,每个微服务在其 src/main/resources 文件夹中都有一个 application.yml 文件,指定如下:

# Required to make the Spring AOT engine generate the appropriate 
# infrastructure for a separate management port at build time
management.server.port: 9009

在构建时指定此设置后,创建原生镜像时,可使用 config-repo 文件夹中的属性文件在运行时将管理端口设置为任何值。以下是在 application.yml 文件中构建时设置的所有属性列表,以避免四个微服务使用的各种 Spring Bean 出现此类问题:

# Required to make Springdoc handling forward headers correctly when natively 
# compiled
server.forward-headers-strategy: framework
# Required to make the Spring AOT engine generate the appropriate 
# infrastructure for a separate management port, Prometheus, and K8S probes at 
# build time
management.server.port: 9009
management.endpoint.health.probes.enabled: true
management.endpoints.web.exposure.include: 
health,info,circuitbreakerevents,prometheus
# Required to make the Spring AOT engine generate a ReactiveJwtDecoder for the 
# OIDC issuer
spring.security.oauth2.resourceserver.jwt.issuer-uri: http://someissuer
# See https://github.com/springdoc/springdoc-openapi/
# issues/1284#issuecomment-1279854219
springdoc.enable-native-support: true
# Native Compile: Point out that RabbitMQ is to be used when performing the 
# native compilation
spring.cloud.stream.defaultBinder: rabbit
# Native Compile: Required to disable the health check of RabbitMQ when using 
# Kafka
# management.health.rabbit.enabled: false
# Native Compile: Required to disable the health check of Kafka when using 
# RabbitMQ
management.health.kafka.enabled: false
# Native Compile: Required to get the circuit breaker's health check to work 
# properly
management.health.circuitbreakers.enabled: true
5. 更新运行时属性

在使用原生编译的镜像时,有一个运行时属性需要更新,即评论微服务使用的 MySQL 数据库的连接字符串。由于并非所有字符集都默认包含在原生镜像中,必须指定一个在原生镜像中可用的字符集,这里使用 UTF - 8 字符集。在评论配置文件 config-repo/review.yml 中对所有 MySQL 连接属性进行如下设置:

spring.datasource.url: jdbc:mysql://localhost/review-db?useUnicode=true&connectionCollation=utf8_general_ci&characterSetResults=utf8&characterEncoding=utf-8
6. 配置 GraalVM 原生镜像跟踪代理

若难以确定创建应用的有效原生镜像所需的可达性元数据和/或自定义提示,可使用 GraalVM 原生镜像跟踪代理。在 Java VM 中运行应用时启用跟踪代理,它可根据应用对反射、资源和代理的使用情况收集所需的可达性元数据。

为使跟踪代理能够观察 JUnit 测试的执行,可在 build.gradle 文件的测试部分添加以下 jvmArgs

tasks.named('test') {
    useJUnitPlatform()
    jvmArgs "-agentlib:Native Image-agent=access-filter-file=src/test/resources/access-filter.json,config-output-dir=src/main/resources/META-INF/Native Image"
}

由于跟踪代理并非使微服务原生编译工作所必需,此配置在构建文件中被注释掉。 Native Image-agent=access-filter-file 参数指定一个文件,列出跟踪代理应排除的 Java 包和类,通常是运行时无用的测试相关类。另外,有报告称在原生编译时,Swagger UI 不显示 Authorize 按钮,详情见 https://github.com/springdoc/springdoc-openapi/issues/2255

综上所述,原生编译 Java 微服务虽然面临一些挑战,但通过合理使用上述工具和方法,对源代码进行相应的更改和配置,可以顺利实现微服务的原生编译和测试,提高应用的性能和部署效率。在实际应用中,可根据具体情况灵活运用这些技术,逐步优化微服务的原生编译过程。

原生编译的 Java 微服务:从构建到测试的全面指南

7. 总结与操作步骤梳理

为了更清晰地展示原生编译 Java 微服务的整个流程,下面将关键步骤进行梳理:

7.1 构建 Docker 镜像
  • 使用 Gradle 任务 :使用 bootBuildImage 任务创建 Docker 镜像,前提是在构建文件中声明 GraalVM 的 Gradle 插件。
  • 处理架构问题 :若使用 arm64 架构(如 Apple silicon),可使用非官方 Docker 镜像 dashaun/builder:tiny 作为临时解决方案。
  • 快速反馈 :初始构建时,可使用 nativeImage 任务进行快速反馈,成功后再使用 Docker 镜像进行测试。
7.2 处理原生编译问题
工具/项目 作用 操作方法
Spring AOT 冒烟测试 验证 Spring 项目原生编译是否正常 访问 项目地址 查找解决方案,测试结果可在 CI 环境 查看
GraalVM 可达性元数据仓库 提供开源项目的可达性元数据 GraalVM 的 Gradle 插件会自动查找并添加,更多信息见 文档
使用 Java VM 测试 AOT 生成的代码 快速验证初始化代码 设置系统属性 spring.aot.enabled=true
提供自定义提示 为 GraalVM 原生镜像编译器提供元数据 使用 @RegisterReflectionForBinding 注解或实现 RuntimeHintsRegistrar 接口
运行原生测试 全面测试原生编译 使用 gradle nativeTest 命令
使用 GraalVM 的跟踪代理 收集可达性元数据 build.gradle 文件的测试部分添加 jvmArgs
7.3 源代码更改
  • 更新 Gradle 构建文件
    1. 添加 GraalVM 插件。
    2. 配置 bootBuildImage 任务。
    3. 升级 Spring Boot 和 springdoc-openapi 版本。
    4. 不再禁用 jar 任务。
  • 添加可达性元数据和自定义提示
    1. 在主类添加自定义提示注解。
    2. 处理 Resilience4J 注解问题,实现 RuntimeHintsRegistrar 接口。
    3. 为 Resilience4J 重试机制提供可达性元数据。
  • 启用 Spring Bean :在 application.yml 文件中设置属性,确保运行时所需的 Spring Bean 在构建时可达。
  • 更新运行时属性 :在 config-repo/review.yml 文件中更新 MySQL 数据库连接字符串,指定 UTF - 8 字符集。
  • 配置 GraalVM 原生镜像跟踪代理 :在 build.gradle 文件的测试部分添加 jvmArgs
8. 流程图展示

下面是原生编译 Java 微服务的主要流程 mermaid 流程图:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([开始]):::startend --> B(更新 Gradle 构建文件):::process
    B --> C(添加可达性元数据和自定义提示):::process
    C --> D(启用 Spring Bean):::process
    D --> E(更新运行时属性):::process
    E --> F(配置 GraalVM 跟踪代理):::process
    F --> G{选择构建方式}:::decision
    G -->|nativeImage 任务| H(快速反馈构建):::process
    G -->|bootBuildImage 任务| I(创建 Docker 镜像):::process
    H --> J(运行原生测试):::process
    I --> J
    J --> K(处理编译问题):::process
    K --> L([结束]):::startend
9. 注意事项
  • 系统属性设置 spring.aot.enabled 必须作为系统属性在 java 命令中设置,不能作为环境变量或在属性文件中设置。
  • 字符集问题 :使用原生编译镜像时,要确保数据库连接字符串指定可用的字符集,如 UTF - 8。
  • 跟踪代理配置 :跟踪代理配置在构建文件中默认注释掉,若需要使用,需取消注释。
10. 实际应用建议
  • 逐步推进 :在实际项目中,可先在开发环境进行小范围测试,逐步验证各个步骤的可行性,再推广到生产环境。
  • 持续优化 :根据测试结果和实际运行情况,不断调整配置和代码,优化原生编译过程。
  • 团队协作 :涉及到代码更改和配置调整,团队成员之间要保持良好的沟通,确保各个环节的一致性。

通过以上步骤和方法,我们可以更系统地进行 Java 微服务的原生编译,提高开发效率和应用性能。在实际操作中,要根据具体情况灵活运用各种工具和技术,不断探索和优化,以实现最佳的编译效果。

【EI复现】基于深度强化学习的微能源网能量管理与优化策略研究(Python代码实现)内容概要:本文围绕“基于深度强化学习的微能源网能量管理与优化策略”展开研究,重点利用深度Q网络(DQN)等深度强化学习算法对微能源网中的能量调度进行建模与优化,旨在应对可再生能源出力波动、负荷变化及运行成本等问题。文中结合Python代码实现,构建了包含光伏、储能、负荷等元素的微能源网模型,通过强化学习智能体动态决策能量分配策略,实现经济性、稳定性和能效的多重优化目标,并可能与其他优化算法进行对比分析以验证有效性。研究属于电力系统与人工智能交叉领域,具有较强的工程应用背景和学术参考价值。; 适合人群:具备一定Python编程基础和机器学习基础知识,从事电力系统、能源互联网、智能优化等相关方向的研究生、科研人员及工程技术人员。; 使用场景及目标:①学习如何将深度强化学习应用于微能源网的能量管理;②掌握DQN等算法在实际能源系统调度中的建模与实现方法;③为相关课题研究或项目开发提供代码参考和技术思路。; 阅读建议:建议读者结合提供的Python代码进行实践操作,理解环境建模、状态空间、动作空间及奖励函数的设计逻辑,同时可扩展学习其他强化学习算法在能源系统中的应用。
皮肤烧伤识别作为医学与智能技术交叉的前沿课题,近年来在深度学习方法推动下取得了显著进展。该技术体系借助卷积神经网络等先进模型,实现了对烧伤区域特征的高效提取与分类判别,为临床诊疗决策提供了重要参考依据。本研究项目系统整合了算法设计、数据处理及模型部署等关键环节,形成了一套完整的可操作性方案。 在技术实现层面,首先需要构建具有代表性的烧伤图像数据库,涵盖不同损伤程度及愈合阶段的临床样本。通过对原始图像进行标准化校正、对比度增强等预处理操作,有效提升后续特征学习的稳定性。网络架构设计需充分考虑皮肤病变的区域特性,通过多层卷积与池化操作的组合,逐步抽象出具有判别力的烧伤特征表示。 模型优化过程中采用自适应学习率调整策略,结合交叉熵损失函数与梯度下降算法,确保参数收敛的稳定性。为防止过拟合现象,引入数据扩增技术与正则化约束,增强模型的泛化能力。性能验证阶段采用精确率、召回率等多维度指标,在独立测试集上全面评估模型对不同烧伤类型的识别效能。 经过充分验证的识别系统可集成至医疗诊断平台,通过规范化接口实现与现有医疗设备的无缝对接。实际部署前需进行多中心临床验证,确保系统在不同操作环境下的稳定表现。该技术方案的实施将显著缩短烧伤评估时间,为临床医师提供客观量化的辅助诊断依据,进而优化治疗方案制定流程。 本项目的突出特点在于将理论研究与工程实践有机结合,既包含前沿的深度学习算法探索,又提供了完整的产业化实施路径。通过模块化的设计思路,使得医疗专业人员能够快速掌握核心技术方法,推动智能诊断技术在烧伤外科领域的实际应用。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值