從 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.xml web.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 要求,第二代 Java 執行階段就能執行任何 Java 架構。舉例來說,第二代 Java 執行階段可以原封不動地執行 Spring Boot Uber JAR。如需更多範例,請參閱「架構彈性」一節。

    如果應用程式使用舊版內含服務,App Engine 會使用 Jetty 部署應用程式,這與 Java 8 執行階段相同。

  2. 第二代 Java 執行階段的記錄功能會遵循 Cloud Logging 的記錄標準。在第二代 Java 執行階段中,應用程式記錄不再與要求記錄綁在一起,而是分開儲存在不同的記錄中。如要進一步瞭解如何在第二代 Java 執行階段中讀取及寫入記錄,請參閱記錄指南

  3. 如要在第二代 Java 執行階段中設定非執行緒安全的應用程式,請在 app.yaml 檔案中,或在使用舊版內含服務的情況下,在 appengine-web.xml 檔案中,將並行作業的最大值設為 1。<threadsafe>false</threadsafe>

  4. Google 不支援第二代執行階段中的 DataNucleus 程式庫。較新的 DataNucleus 版本與 Java 8 中使用的版本不相容。如要存取 Datastore,建議您使用 Datastore 模式用戶端程式庫或 Java Objectify (第 6 版或更新版本) 解決方案。Objectify 是 Datastore 的開放原始碼 API,可提供更高層級的抽象。

記憶體用量差異

與第一代執行階段相比,第二代執行階段的記憶體使用量基準較高。這可能是由多種因素造成,例如不同的基礎映像檔版本,以及兩個世代計算記憶體用量的方式不同。

第二代執行階段會將執行個體記憶體用量計算為應用程式程序使用的用量總和,以及記憶體中動態快取的應用程式檔案數量。為避免記憶體密集型應用程式因超出記憶體限制而導致執行個體關閉,請升級至記憶體較多的執行個體類別

CPU 用量差異

在執行個體冷啟動時,第二代執行階段可以看到較高的 CPU 使用率基準。視應用程式的縮放設定而定,這可能會產生非預期的副作用,例如,如果應用程式已設為根據 CPU 使用率進行縮放,則執行個體數量可能會高於預期。為避免發生這個問題,請查看並測試應用程式縮放設定,確保可用的執行個體數量。

要求標頭差異

第一代執行階段允許將含有底線的請求標頭 (例如 X-Test-Foo_bar) 轉送至應用程式。第二代執行階段會將 Nginx 導入主機架構。因此,第二代執行階段會自動移除含有底線 (_) 的標頭。為避免應用程式發生問題,請避免在應用程式要求標頭中使用底線。

架構彈性

除非您使用舊版套裝服務,否則第二代 Java 執行階段不會包含任何網路服務架構。也就是說,您可以使用以 Servlet 為基礎的架構以外的架構。如果您使用舊版的內含服務,第二代 Java 執行階段會提供 Jetty 網路服務架構。

Google Cloud GitHub 存放區提供使用熱門 Java 網路架構的 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. 在部署到實際工作環境之前,先手動再次檢查已轉換的檔案。

    如需 xmlyaml 的檔案成功轉換範例,請參閱「手動遷移檔案」分頁。

手動遷移檔案

如何手動將 xml 檔案轉換成 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 物件的 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 參考說明文件

使用包含物件清單的 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 物件的 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>