Bazel 教學課程:建構 Java 專案

回報問題 查看來源 Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

本教學課程將介紹使用 Bazel 建構 Java 應用程式的基本概念。您將設定工作區,並建構簡單的 Java 專案,說明目標和 BUILD 檔案等重要 Bazel 概念。

預計完成時間:30 分鐘。

課程內容

在本教學課程中,您將瞭解如何:

  • 建立目標
  • 將專案的依附元件視覺化
  • 將專案分割為多個目標和套件
  • 控管套件的目標瀏覽權限
  • 透過標籤參照目標
  • 部署目標

事前準備

安裝 Bazel

如要準備進行教學課程,請先安裝 Bazel (如果尚未安裝)。

安裝 JDK

  1. 安裝 Java JDK (建議使用 11 版,但支援 8 到 15 版)。

  2. 將 JAVA_HOME 環境變數設為指向 JDK。

    • Linux/macOS:

      export JAVA_HOME="$(dirname $(dirname $(realpath $(which javac))))"
      
    • Windows:

      1. 開啟控制台。
      2. 依序前往「系統與安全性」>「系統」>「進階系統設定」>「進階」分頁 >「環境變數...」。
      3. 在「使用者變數」清單 (頂端清單) 下方,按一下「新增...」。
      4. 在「變數名稱」欄位中輸入 JAVA_HOME
      5. 按一下「Browse Directory...」(瀏覽目錄...)。
      6. 前往 JDK 目錄 (例如 C:\Program Files\Java\jdk1.8.0_152)。
      7. 按一下所有對話方塊視窗中的「確定」。

取得範例專案

從 Bazel 的 GitHub 存放區擷取範例專案:

git clone https://github.com/bazelbuild/examples

本教學課程的範例專案位於 examples/java-tutorial 目錄,結構如下:

java-tutorial
├── BUILD
├── src
   └── main
       └── java
           └── com
               └── example
                   ├── cmdline
                      ├── BUILD
                      └── Runner.java
                   ├── Greeting.java
                   └── ProjectRunner.java
└── WORKSPACE

使用 Bazel 建構

設定工作區

您必須先設定專案的工作區,才能建構專案。工作區是保存專案來源檔案和 Bazel 建構輸出內容的目錄。這個檔案也包含 Bazel 視為特殊的檔案:

  • WORKSPACE 檔案,可將目錄及其內容識別為 Bazel 工作區,並位於專案目錄結構的根目錄。

  • 一或多個 BUILD 檔案,用於告知 Bazel 如何建構專案的不同部分。(工作區中包含 BUILD 檔案的目錄是套件。本教學課程稍後會介紹套件。)

如要將目錄指定為 Bazel 工作區,請在該目錄中建立名為 WORKSPACE 的空白檔案。

當 Bazel 建構專案時,所有輸入內容和依附元件都必須位於同一個工作區。除非連結,否則不同工作區中的檔案彼此獨立,這不在本教學課程的討論範圍內。

瞭解 BUILD 檔案

BUILD 檔案包含 Bazel 的多種不同類型指令。最重要的類型是「建構規則」,可告知 Bazel 如何建構所需的輸出內容,例如可執行的二進位檔或程式庫。BUILD 檔案中的每個建構規則例項都稱為「目標」,並指向一組特定的來源檔案和依附元件。目標也可以指向其他目標。

查看 java-tutorial/BUILD 檔案:

java_binary(
    name = "ProjectRunner",
    srcs = glob(["src/main/java/com/example/*.java"]),
)

在本範例中,ProjectRunner 目標會例項化 Bazel 的內建 java_binary 規則。這項規則會指示 Bazel 建構 .jar 檔案和包裝函式 Shell 指令碼 (兩者都以目標命名)。

目標中的屬性會明確指出其依附元件和選項。 name 屬性為必填,但許多屬性為選填。舉例來說,在 ProjectRunner 規則目標中,name 是目標名稱,srcs 則指定 Bazel 用於建構目標的來源檔案,而 main_class 則指定包含主要方法的類別。(您可能已經發現,我們的範例使用 glob 將一組來源檔案傳遞至 Bazel,而不是逐一列出檔案。)

建構專案

如要建構範例專案,請前往 java-tutorial 目錄並執行下列指令:

bazel build //:ProjectRunner

在目標標籤中,// 部分是 BUILD 檔案相對於工作區根目錄的位置 (在本例中為根目錄本身),而 ProjectRunnerBUILD 檔案中的目標名稱。(本教學課程結尾會詳細說明目標標籤)。

Bazel 會產生類似以下的輸出內容:

   INFO: Found 1 target...
   Target //:ProjectRunner up-to-date:
      bazel-bin/ProjectRunner.jar
      bazel-bin/ProjectRunner
   INFO: Elapsed time: 1.021s, Critical Path: 0.83s

恭喜,您剛建構了第一個 Bazel 目標!Bazel 會將建構輸出內容放在工作區根目錄的 bazel-bin 目錄中。瀏覽內容,瞭解 Bazel 的輸出結構。

現在測試剛建構的二進位檔:

bazel-bin/ProjectRunner

查看依附元件圖表

Bazel 需要在 BUILD 檔案中明確宣告建構依附元件。Bazel 會使用這些陳述式建立專案的依附元件圖,以便準確地進行增量建構。

如要將範例專案的依附元件視覺化,請在工作區根目錄執行下列指令,產生依附元件圖表的文字表示法:

bazel query  --notool_deps --noimplicit_deps "deps(//:ProjectRunner)" --output graph

上述指令會告知 Bazel 尋找目標 //:ProjectRunner 的所有依附元件 (不含主機和隱含依附元件),並將輸出內容格式化為圖表。

然後將文字貼到 GraphViz

如您所見,專案只有一個目標,可建構兩個來源檔案,且沒有其他依附元件:

目標「ProjectRunner」的依附元件圖

設定工作區、建構專案並檢查依附元件後,即可新增一些複雜度。

調整 Bazel 建構作業

雖然小型專案只要一個目標就夠了,但您可能想將大型專案分割成多個目標和套件,以便快速進行增量建構 (也就是只重建變更的內容),並同時建構專案的多個部分,加快建構速度。

指定多個建構目標

您可以將範例專案建構作業分割為兩個目標。將 java-tutorial/BUILD 檔案的內容替換成以下內容:

java_binary(
    name = "ProjectRunner",
    srcs = ["src/main/java/com/example/ProjectRunner.java"],
    main_class = "com.example.ProjectRunner",
    deps = [":greeter"],
)

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
)

採用這項設定時,Bazel 會先建構 greeter 程式庫,然後建構 ProjectRunner 二進位檔。java_binary 中的 deps 屬性會告知 Bazel,建構 ProjectRunner 二進位檔時需要 greeter 程式庫。

如要建構這個新版專案,請執行下列指令:

bazel build //:ProjectRunner

Bazel 會產生類似以下的輸出內容:

INFO: Found 1 target...
Target //:ProjectRunner up-to-date:
  bazel-bin/ProjectRunner.jar
  bazel-bin/ProjectRunner
INFO: Elapsed time: 2.454s, Critical Path: 1.58s

現在測試剛建構的二進位檔:

bazel-bin/ProjectRunner

如果您現在修改 ProjectRunner.java 並重建專案,Bazel 只會重新編譯該檔案。

查看依附元件圖表,您會發現 ProjectRunner 依附於與先前相同的輸入內容,但建構結構不同:

新增依附元件後,目標「ProjectRunner」的依附元件圖表

您現在已建構專案,並包含兩個目標。ProjectRunner 目標會建構兩個來源檔案,並依附於另一個目標 (:greeter),該目標會建構一個額外的來源檔案。

使用多個套件

現在,我們將專案分割為多個套件。查看 src/main/java/com/example/cmdline 目錄時,您會發現其中也包含 BUILD 檔案和一些來源檔案。因此對 Bazel 而言,工作區現在包含兩個套件://src/main/java/com/example/cmdline// (因為工作區根目錄中有 BUILD 檔案)。

查看 src/main/java/com/example/cmdline/BUILD 檔案:

java_binary(
    name = "runner",
    srcs = ["Runner.java"],
    main_class = "com.example.cmdline.Runner",
    deps = ["//:greeter"],
)

runner 目標依附於 // 套件中的 greeter 目標 (因此為目標標籤 //:greeter) - Bazel 會透過 deps 屬性瞭解這一點。查看依附元件關係圖:

目標「runner」的依附元件圖

不過,如要成功建構,您必須使用 visibility 屬性,明確將 runner 目標的 //src/main/java/com/example/cmdline/BUILD 可見度提供給 //BUILD 中的目標。這是因為根據預設,只有相同 BUILD 檔案中的其他目標才能看到目標。(Bazel 會使用目標可見度來避免問題,例如含有實作詳細資料的程式庫洩漏到公用 API 中)。

如要這麼做,請在 java-tutorial/BUILD 中將 visibility 屬性新增至 greeter 目標,如下所示:

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
    visibility = ["//src/main/java/com/example/cmdline:__pkg__"],
)

現在,您可以在工作區的根目錄執行下列指令,建構新套件:

bazel build //src/main/java/com/example/cmdline:runner

Bazel 會產生類似以下的輸出內容:

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner.jar
  bazel-bin/src/main/java/com/example/cmdline/runner
  INFO: Elapsed time: 1.576s, Critical Path: 0.81s

現在測試剛建構的二進位檔:

./bazel-bin/src/main/java/com/example/cmdline/runner

您已修改專案,現在會建構為兩個套件,每個套件都包含一個目標,並瞭解兩者之間的依附元件。

使用標籤參照目標

BUILD 檔案和指令列中,Bazel 會使用目標標籤參照目標,例如 //:ProjectRunner//src/main/java/com/example/cmdline:runner。語法如下:

//path/to/package:target-name

如果目標是規則目標,則 path/to/package 是包含 BUILD 檔案的目錄路徑,而 target-name 是您在 BUILD 檔案中為目標命名的名稱 (name 屬性)。如果目標是檔案目標,則 path/to/package 是套件根目錄的路徑,而 target-name 是目標檔案的名稱,包括完整路徑。

在存放區根層級參照目標時,套件路徑為空白,只要使用 //:target-name 即可。在同一個 BUILD 檔案中參照目標時,您甚至可以略過 // 工作區根 ID,只使用 :target-name

舉例來說,在 java-tutorial/BUILD 檔案中,您不必為目標指定套件路徑,因為工作區根目錄本身就是套件 (//),而您的兩個目標標籤只是 //:ProjectRunner//:greeter

不過,對於 //src/main/java/com/example/cmdline/BUILD 檔案中的目標,您必須指定 //src/main/java/com/example/cmdline 的完整套件路徑,且目標標籤為 //src/main/java/com/example/cmdline:runner

將 Java 目標封裝為部署作業

現在,請建構包含所有執行階段依附元件的二進位檔,將 Java 目標封裝以供部署。這樣一來,您就能在開發環境以外執行二進位檔。

如您所知,java_binary 建構規則會產生 .jar 和包裝函式殼層指令碼。使用下列指令查看 runner.jar 的內容:

jar tf bazel-bin/src/main/java/com/example/cmdline/runner.jar

內容如下:

META-INF/
META-INF/MANIFEST.MF
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class

如您所見,runner.jar 包含 Runner.class,但不包含其依附元件 Greeting.class。Bazel 產生的 runner 指令碼會將 greeter.jar 新增至類別路徑,因此如果保留這個設定,指令碼會在本地執行,但無法在其他電腦上獨立執行。幸好,java_binary 規則可讓您建構可部署的獨立二進位檔。如要建構,請將 _deploy.jar 附加至目標名稱:

bazel build //src/main/java/com/example/cmdline:runner_deploy.jar

Bazel 會產生類似以下的輸出內容:

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner_deploy.jar up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar
INFO: Elapsed time: 1.700s, Critical Path: 0.23s

您剛建構了 runner_deploy.jar,由於其中包含必要的執行階段依附元件,因此可以獨立執行,不必在開發環境中執行。使用與先前相同的指令,查看這個獨立 JAR 的內容:

jar tf bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar

內容包括執行時所需的所有類別:

META-INF/
META-INF/MANIFEST.MF
build-data.properties
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class
com/example/Greeting.class

延伸閱讀

相關詳情請參閱:

祝您建構愉快!