排解 Java 適用的 Cloud 用戶端程式庫問題

本文概述如何記錄及排解 Java 適用的 Cloud 用戶端程式庫常見問題。

記錄

啟用用戶端程式庫記錄功能的方法有兩種:使用 java.util.logging,或使用 Client Library Debug Logging with SLF4J。

使用java.util.logging

Java 適用的 Cloud 用戶端程式庫使用 Java 記錄 API (java.util.logging) 套件。設定記錄層級會顯示有助於排解問題的資訊,包括:

  • 基礎用戶端與伺服器通訊的時間。
  • 要求和回應訊息標頭。
  • 基礎依附元件程式庫中的詳細訊息。

如要快速啟用 Java 適用的 Cloud 用戶端程式庫詳細記錄,請建立 logging.properties 檔案,並加入下列內容:

# run java program pointing to this properties file with the java arg
#   -Djava.util.logging.config.file=path/to/logging.properties
handlers=java.util.logging.ConsoleHandler
java.util.logging.SimpleFormatter.format=%1$tF %1$tT,%1$tL %4$-8s %3$-50s - %5$s %6$s%n

# --- ConsoleHandler ---
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
.level=INFO

# --- Specify logging level for certain packages ---
# com.google.api is for HTTP 1.1 layer
com.google.api.level=ALL
# io.grpc is for gRPC + Netty layer
io.grpc.level=FINE
# com.google.auth is for authentication
com.google.auth.level=FINE

# Example when you specify the storage library's level. This works when
# the target Cloud library uses the logging API.
com.google.cloud.storage.level=INFO

接下來,請使用 -Djava.util.logging.config.file=path/to/logging.properties 做為 VM argument (而非 Program argument) 執行應用程式。

如果您使用 IntelliJ,請在「Run/Debug Configuration」(執行/偵錯設定) 中指定 VM 引數:

IntelliJ 執行/偵錯設定,顯示指定 VM 引數的位置

如果程式的 JVM 執行時設定正確,您會在控制台中看到 FINE 層級的記錄。

輸出內容範例:

2023-04-05 13:03:01,761 FINE     com.google.auth.oauth2.DefaultCredentialsProvider  - Attempting to load credentials from well known file: /usr/local/google/home/suztomo/.config/gcloud/application_default_credentials.json
2023-04-05 13:03:01,847 FINE     io.grpc.ManagedChannelRegistry                     - Unable to find OkHttpChannelProvider
java.lang.ClassNotFoundException: io.grpc.okhttp.OkHttpChannelProvider
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
    at java.base/java.lang.Class.forName0(Native Method)
    at java.base/java.lang.Class.forName(Class.java:315)
...

這是設定記錄層級的方式之一。如要進一步瞭解 Java Logging API 的用法,請參閱「Java Logging 總覽」。

用戶端程式庫偵錯記錄 (SLF4J)

Cloud 用戶端程式庫隨附可選擇加入的偵錯記錄功能,可協助您排解應用程式與 API 整合時的問題。啟用記錄功能後,系統會將要求和回應等重要事件,連同資料酬載和中繼資料 (例如標頭),記錄到標準錯誤串流。

警告:用戶端程式庫偵錯記錄會以純文字形式納入資料酬載,其中可能包含您或客戶的個人識別資訊、私密金鑰,或其他可能因外洩而遭盜用的安全性資料。請務必對應用程式記錄進行良好的資料健全度做法,並遵循最低存取權原則。Google 也建議您只在進行偵錯時暫時啟用用戶端程式庫偵錯記錄,不要在實際工作環境中永久啟用。

修課條件

我們的程式庫支援使用 SLF4J 介面進行偵錯記錄。

用戶端程式庫和程式庫 BOM 不包含 slf4j-api 依附元件。您必須先設定 SLF4J 和相應的記錄實作項目與設定等記錄依附元件,才能啟用用戶端程式庫的偵錯記錄。

NOTE:您必須在類別路徑中加入 SLF4J 和對應的記錄供應商 (例如 logback、log4j2 等),才能使用用戶端程式庫的偵錯記錄功能;否則即使使用環境變數啟用偵錯記錄,也不會產生任何記錄。

使用環境變數啟用記錄功能

偵錯記錄功能預設為關閉。即使符合必要條件,系統也不會明確開啟偵錯記錄。

如要啟用用戶端程式庫偵錯記錄,請將環境變數 GOOGLE_SDK_JAVA_LOGGING 設為 true (不分大小寫)。如果不設定這個欄位或設為任何其他值,用戶端程式庫偵錯記錄就會停用。

記錄設定範例

這份文件提供使用 Logback 進行偵錯記錄的範例。

將 Logback 依附元件新增至應用程式,這會以遞移方式帶入 slf4j 依附元件。使用者應參閱 Logback 設定,瞭解最新版本。如果應用程式的類別路徑中已有 logback 依附元件,則可略過這個步驟。

如果您使用 Maven:

<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>LATEST_VERSION</version>
</dependency>

如要查看 DEBUG 層級的記錄,請將內容新增至 logback 設定檔。如果只需要 INFO 記錄,可以略過這個步驟。 詳情請參閱「Logback 設定」。

<!-- set Client Library log level to DEBUG -->
<logger name="com.google.api"  level="DEBUG"/>
<!-- set Auth Library log level to DEBUG -->
<logger name="com.google.auth" level="DEBUG"/>

ALPN 設定有誤

如果看到與 ALPN is not configured properly 相關的例外狀況,例如:

Caused by: java.lang.IllegalArgumentException: ALPN is not configured properly. See https://github.com/grpc/grpc-java/blob/master/SECURITY.md#troubleshooting for more information.

使用相容性檢查工具,確認您的環境是否與以 gRPC 為基礎的用戶端相容。

不相容可能代表下列其中一種情況:

排解「ClassNotFoundException」、「NoSuchMethodError」和「NoClassDefFoundError」問題

這些錯誤通常是因為類別路徑中有多個版本或衝突版本的相同依附元件所致。這些依附元件衝突通常發生在 guavaprotobuf-java

多個來源可能會導致類別路徑衝突:

  • 依附元件樹狀結構中,同一個遞移依附元件有多個版本。
  • 執行階段類別路徑的依附元件版本與您在建構中指定的版本不同。

舉例來說,如果您直接或遞移地依附於 Guava 19.0 版,而 google-cloud-java 使用 Guava 30.0 版,則 google-cloud-java 可能會使用 Guava 19.0 版中不存在的 Guava 方法,並導致 NoSuchMethodError

同樣地,如果類別路徑有舊版 protobuf-java,但 google-cloud-java 需要較新版本,您可能會看到 NoClassDefFoundError 無法初始化 google-cloud-java 類別。

例如:

java.lang.NoClassDefFoundError: Could not initialize class com.google.pubsub.v1.PubsubMessage$AttributesDefaultEntryHolder

驗證衝突

檢查依附元件樹狀結構,確認您是否有多個相同依附元件的版本:

$ mvn dependency:tree

尋找可能衝突的依附元件版本,例如 guavaprotobuf-java

如果只有在執行階段發生錯誤,則執行階段環境可能會將衝突的 JAR 導入執行階段類別路徑。一般來說,應用程式執行的 Hadoop、Spark 或其他伺服器軟體,在類別路徑中會有衝突的版本 nettyguavaprotobuf-java JAR。

在建構期間偵測衝突

如要在編譯時偵測依附元件連結錯誤,請在 pom.xml 中新增連結檢查器強制執行規則

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-enforcer-plugin</artifactId>
        <version>3.0.0-M3</version>
        <dependencies>
          <dependency>
            <groupId>com.google.cloud.tools</groupId>
            <artifactId>linkage-checker-enforcer-rules</artifactId>
            <version>1.5.7</version>
          </dependency>
        </dependencies>
        <executions>
          <execution>
            <id>enforce-linkage-checker</id>
            <!-- Important! Should run after compile -->
            <phase>verify</phase>
            <goals>
              <goal>enforce</goal>
            </goals>
            <configuration>
              <rules>
                <LinkageCheckerRule
                    implementation="com.google.cloud.tools.dependencies.enforcer.LinkageCheckerRule"/>
              </rules>
            </configuration>
          </execution>
        </executions>
      </plugin>

不過,您無法偵測執行階段類別路徑衝突。您必須充分瞭解執行階段類路徑中包含哪些 JAR/類別,因為每個伺服器環境都不盡相同。

解決衝突

解決衝突的策略有很多種,但您必須瞭解衝突的根本原因:

  • 如果您控管依附元件樹狀結構,請升級衝突的依附元件 (例如升級 Guava)。這個方法最穩健,但可能需要投入大量心力,並發布多個程式庫版本,才能確保相容性。
  • 如果無法修改及推送新的依附元件版本,請匯入 com.google.cloud:libraries-bom:25.1.0 (或更新近的版本),選取一致的依附元件版本。這個方法可簡化依附元件管理作業。舉例來說,您可以透過這種方式,依附於 Guava 和 com.google.cloud:google-cloud-storage 的一致版本,而不必明確設定任一項目的版本:
  ...
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <version>25.1.0</version>
        <type>pom</type>
        <scope>import</scope>
       </dependency>
     </dependencies>
  </dependencyManagement>
  ...
  <dependencies>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-storage</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
    </dependency>
    ...
  </dependencies>
  ...
  • 程式庫 BOM 版本資訊會顯示相容的依附元件程式庫。舉例來說,https://github.com/googleapis/java-cloud-bom/releases/tag/v26.31.0 會顯示:

    These client libraries are built with the following Java libraries:
    
    - Guava: 32.1.3-jre
    - Protobuf Java: 3.25.2
    - Google Auth Library: 1.22.0
    - Google API Client: 2.2.0
    - gRPC: 1.61.0
    - GAX: 2.41.0
    - Google Cloud Core: 2.31.0
    

    檢查專案的依附元件圖表 (mvn dependency:tree -Dverbosegradle dependenciessbt dependencyTree) 時,您可能會發現某些依附元件的版本不符預期,這可能會導致依附元件衝突。

  • 如果變更依附元件版本會導致其他失敗,請考慮遮蔽與 Java 程式庫衝突的依附元件。 Google Cloud

    舉例來說,如要為 guavaprotobuf-java 加上陰影,請執行下列操作:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>...</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <keepDependenciesWithProvidedScope>false</keepDependenciesWithProvidedScope>
              <relocations>
                <!-- move protobuf to a shaded package -->
                <relocation>
                  <pattern>com.google.protobuf</pattern>
                  <shadedPattern>myapp.shaded.com.google.protobuf</shadedPattern>
                </relocation>
                <!-- move Guava to a shaded package -->
                <relocation>
                  <pattern>com.google.common</pattern>
                  <shadedPattern>myapp.shaded.com.google.common</shadedPattern>
                </relocation>
              </relocations>
            </configuration>
          </execution>
        </executions>
      </plugin>