从 Java 8 迁移到最新的 Java 运行时

本页面介绍了从第一代迁移到第二代 Java 运行时的说明。如需升级第二代应用以使用最新支持的 Java 版本,请参阅升级现有应用

Java 8 已于 2024 年 1 月 31 日达到支持终止期限。现有的 Java 8 应用将继续运行并接收流量。但是,App Engine 可能会阻止重新部署使用支持终止日期之后的运行时的应用。我们建议您按照本页面中的指导迁移到最新支持的 Java 版本。

迁移到第二代 Java 运行时后,您可以通过惯用代码来利用最新的语言功能并构建可移植性更强的应用。

了解迁移选项

为了减少运行时迁移工作量和复杂性,App Engine 标准环境可让您在第二代 Java 运行时中访问许多旧版捆绑服务和 API,例如 Memcache。Java 应用可以通过 App Engine API JAR 调用捆绑服务 API,并访问与 Java 8 运行时相同的大多数功能。

您还可以选择使用 Google Cloud 产品,其可提供与旧版捆绑服务类似的功能。这些 Google Cloud 产品提供惯用的 Java 版 Cloud 客户端库。对于在 Google Cloud 中未作为单独产品提供的捆绑式服务(如图片处理、搜索和消息功能),您可以使用第三方提供商或其他解决方法。

如需详细了解如何迁移到未捆绑服务,请参阅从捆绑服务迁移

根据您是否选择使用旧版捆绑服务,执行运行时迁移的方式存在以下差异:

迁移到具有捆绑服务的第二代 Java 运行时 迁移到无捆绑服务的第二代 Java 运行时
使用 App Engine API JAR 访问捆绑服务 (可选)使用推荐的 Google Cloud 产品或第三方服务

使用 appengine-web.xmlweb.xml 进行应用配置。

您可能还需要配置其他 YAML 文件,具体取决于应用使用的功能。

使用 app.yaml 进行应用配置。

您可能还需要配置其他 YAML 文件,具体取决于应用使用的功能。

应用通过 Jetty 部署。使用 WAR 格式打包应用。 使用您自己的服务器部署应用。使用 JAR 格式打包您的应用。如需详细了解如何将现有 WAR 文件转换为可执行 JAR 文件,请参阅重新封装 WAR 文件

迁移过程概览

下面列出了您可能需要对现有 App Engine Java 8 应用和部署过程进行的一些更改,以便使用第二代 Java 运行时:

Java 8 与第二代 Java 运行时之间的主要区别

下面总结了 App Engine 标准环境中的 Java 8 与第二代 Java 运行时之间的区别:

Java 8 运行时 第二代 Java 运行时
服务器部署 使用 Jetty 为您部署的服务器 如果您的应用未使用旧版捆绑服务,则您必须自行部署服务器。1
App Engine 旧版捆绑服务 已提供 已提供
能够使用 Java 版 Cloud 客户端库
语言扩展和系统库支持
外部网络访问
文件系统访问权限 拥有对 /tmp 的读/写访问权限 拥有对 /tmp 的读/写访问权限
语言运行时 针对 App Engine 进行了修改 未修改的开源运行时
隔离机制 基于 gVisor 的容器沙盒 基于 gVisor 的容器沙盒
使用本地开发服务器进行测试 支持 支持
线程安全配置 可以在 appengine-web.xml 文件中指定。 无法在配置文件中指定。假定所有应用的线程安全。3
日志记录 使用 java.util.logging.
ConsoleHandler,该变量会向
stderr 写入数据,并在每条记录后刷新数据流
标准 Cloud Logging 2
DataNucleus 插件 2.x 支持 支持 不支持 4

注意:

  1. 如果您的应用未使用旧版捆绑服务,则只要您打包了配置为响应 PORT 环境变量指定的端口(推荐)或端口 8080 上的 HTTP 请求的 Web 服务器,第二代 Java 运行时就可以执行任何 Java 框架。例如,第二代 Java 运行时可以按原样运行 Spring Boot Uber JAR。如需查看更多示例,请参阅框架灵活性部分。

    如果您的应用使用旧版捆绑服务,App Engine 将使用 Jetty 以与 Java 8 运行时中相同的方式部署它。

  2. 第二代 Java 运行时中的日志记录遵循 Cloud Logging 中的日志记录标准。在第二代 Java 运行时中,应用日志不再与请求日志捆绑,而是分隔在不同的记录中。如需详细了解如何在第二代 Java 运行时中读取和写入日志,请参阅日志记录指南

  3. 如需在第二代 Java 运行时中配置非线程安全应用(类似于在 Java 8 中设置 <threadsafe>false</threadsafe>),请在使用旧版捆绑服务时在 app.yaml 文件或 appengine-web.xml 文件中将最大并发数设置为 1。

  4. Google 不支持第二代运行时中的 DataNucleus 库。新版本的 DataNucleus 向后不兼容 Java 8 中使用的版本。如需访问 Datastore,我们建议您使用 Datastore 模式客户端库或 Java Objectify(版本 6 或更高版本)解决方案。Objectify 是 Datastore 的开源 API,可提供更高级别的抽象。

内存用量差异

与第一代运行时相比,第二代运行时的内存用量基准更高。这是由多种因素造成的,例如基础映像版本不同,以及两代在计算内存用量的方式上存在差异。

第二代运行时在计算实例内存用量时,会将应用进程的内存用量与内存中动态缓存的应用文件数量相加。为避免内存密集型应用因超出内存限制而关停实例,请升级到具有更多内存的更大实例类

CPU 使用率差异

第二代运行时在实例冷启动时会看到更高的 CPU 使用率基准。根据应用的扩缩配置,这可能会导致意外的副作用,例如,如果应用配置为根据 CPU 利用率进行扩缩,则实例数会高于预期。为避免此问题,请查看并测试应用扩缩配置,以确保实例数在可接受的范围内。

请求标头差异

第一代运行时允许将带有下划线(例如 X-Test-Foo_bar)的请求标头转发给应用。第二代运行时将 Nginx 引入了主机架构。由于此更改,第二代运行时配置为自动移除带有下划线 (_) 的标头。为防止出现应用问题,请避免在应用请求标头中使用下划线。

框架灵活性

除非您使用的是旧版捆绑服务,否则第二代 Java 运行时不包含任何 Web 服务框架。这意味着您可以使用不是基于 servlet 的框架。如果您使用的是旧版捆绑服务,则第二代 Java 运行时会提供 Jetty Web 服务框架。

Google Cloud GitHub 代码库中有一些使用常见 Java Web 框架的 hello world 示例:

将 XML 迁移到 YAML 文件格式

gcloud CLI 不支持以下文件格式:

  • cron.xml
  • datastore-index.xml
  • dispatch.xml
  • queue.xml

以下示例演示了如何将 xml 文件迁移到 yaml 文件。

自动迁移文件

要自动迁移 xml 文件,请执行以下操作:

  1. 您必须具备 gcloud CLI 226.0.0 或更高版本。要更新到最新版本,请运行以下命令:

    gcloud components update
    
  2. 对于您要迁移的每个文件,请指定以下某个子命令(cron-xml-to-yamldatastore-indexes-xml-to-yamldispatch-xml-to-yamlqueue-xml-to-yaml)和文件名:

    gcloud beta app migrate-config queue-xml-to-yaml MY-QUEUE-XML-FILE.xml
    
  3. 在部署到生产环境之前手动仔细检查转换后的文件。

    如需查看成功将 xml 转换成 yaml 文件的示例,请参阅手动迁移文件标签页。

手动迁移文件

要手动将 xml 文件迁移到 yaml 文件,请执行以下操作:

cron.yaml

使用包含对象列表的 cron 对象创建 cron.yaml 文件,列表中的每个对象都包含与 cron.xml 文件中的每个 <cron> 标记特性相对应的字段,如下所示。

已转换的 cron.yaml 文件:

cron:
- url: '/recache'
  schedule: 'every 2 minutes'
  description: 'Repopulate the cache every 2 minutes'
- url: '/weeklyreport'
  schedule: 'every monday 08:30'
  target: 'version-2'
  timezone: 'America/New_York'
  description: 'Mail out a weekly report'

原始的 cron.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<cronentries>
  <cron>
    <url>/recache</url>
    <description>Repopulate the cache every 2 minutes</description>
    <schedule>every 2 minutes</schedule>
  </cron>
  <cron>
    <url>/weeklyreport</url>
    <description>Mail out a weekly report</description>
    <schedule>every monday 08:30</schedule>
    <timezone>America/New_York</timezone>
    <target>version-2</target>
  </cron>
</cronentries>

如需了解详情,请参阅 cron.yaml 参考文档。

dispatch.yaml

使用包含对象列表的 dispatch 对象创建 dispatch.yaml 文件,列表中的每个对象都包含与 dispatch.xml 文件中的每个 <dispatch> 标记特性相对应的字段,如下所示。

已转换的 dispatch.yaml 文件:

dispatch:
- url: '*/favicon.ico'
  module: default
- url: 'simple-sample.uc.r.appspot.com/'
  module: default
- url: '*/mobile/*'
  module: mobile-frontend

原始的 dispatch.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<dispatch-entries>
  <dispatch>
      <url>*/favicon.ico</url>
      <module>default</module>
  </dispatch>
  <dispatch>
      <url>simple-sample.uc.r.appspot.com/</url>
      <module>default</module>
  </dispatch>
  <dispatch>
      <url>*/mobile/*</url>
      <module>mobile-frontend</module>
  </dispatch>
</dispatch-entries>

如需了解详情,请参阅 dispatch.yaml 参考文档

index.yaml

使用包含对象列表的 indexes 对象创建 index.yaml 文件,列表中的每个对象都包含与 datastore-indexes.xml 文件中的每个 <datastore-index> 标记特性相对应的字段,如下所示。

已转换的 index.yaml 文件:

indexes:
- ancestor: false
  kind: Employee
  properties:
  - direction: asc
    name: lastName
  - direction: desc
    name: hireDate
- ancestor: false
  kind: Project
  properties:
  - direction: asc
    name: dueDate
  - direction: desc
    name: cost

原始的 datastore-index.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes
 autoGenerate="true">
   <datastore-index kind="Employee" ancestor="false">
       <property name="lastName" direction="asc" />
       <property name="hireDate" direction="desc" />
   </datastore-index>
   <datastore-index kind="Project" ancestor="false">
       <property name="dueDate" direction="asc" />
       <property name="cost" direction="desc" />
   </datastore-index>
</datastore-indexes>

如需了解详情,请参阅 index.yaml 参考文档。

queue.yaml

使用包含对象列表的 queue 对象创建 queue.yaml 文件,列表中的每个对象都包含与 queue.xml 文件中的每个 <queue> 标记特性相对应的字段,如下所示。

已转换的 queue.yaml 文件:

queue:
- name: fooqueue
  mode: push
  rate: 1/s
  retry_parameters:
    task_retry_limit: 7
    task_age_limit: 2d
- name: barqueue
  mode: push
  rate: 1/s
  retry_parameters:
    min_backoff_seconds: 10
    max_backoff_seconds: 200
    max_doublings: 0

原始的 queue.xml 文件:

<queue-entries>
  <queue>
    <name>fooqueue</name>
    <rate>1/s</rate>
    <retry-parameters>
      <task-retry-limit>7</task-retry-limit>
      <task-age-limit>2d</task-age-limit>
    </retry-parameters>
  </queue>
  <queue>
    <name>barqueue</name>
    <rate>1/s</rate>
    <retry-parameters>
      <min-backoff-seconds>10</min-backoff-seconds>
      <max-backoff-seconds>200</max-backoff-seconds>
      <max-doublings>0</max-doublings>
    </retry-parameters>
  </queue>
<queue-entries>