Hướng dẫn về Bazel: Xây dựng dự án Java

Báo cáo vấn đề Xem nguồn Hằng đêm · 7,4 của Google. 7.3 · 7,2 · 7.1 · 7 · 6,5

Hướng dẫn này trình bày các kiến thức cơ bản về cách tạo ứng dụng Java bằng Bazel. Bạn sẽ thiết lập không gian làm việc và tạo một dự án Java đơn giản minh hoạ các khái niệm chính của Bazel, chẳng hạn như mục tiêu và tệp BUILD.

Thời gian hoàn thành ước tính: 30 phút.

Kiến thức bạn sẽ học được

Trong hướng dẫn này, bạn sẽ tìm hiểu cách:

  • Đặt mục tiêu
  • Hình ảnh hoá các phần phụ thuộc của dự án
  • Chia dự án thành nhiều mục tiêu và gói
  • Kiểm soát chế độ hiển thị mục tiêu trên các gói
  • Mục tiêu tham chiếu thông qua nhãn
  • Triển khai mục tiêu

Trước khi bắt đầu

Cài đặt Bazel

Để chuẩn bị cho hướng dẫn này, trước tiên, hãy Cài đặt Bazel nếu bạn chưa cài đặt.

Cài đặt JDK

  1. Cài đặt Java JDK (phiên bản ưu tiên là 11, tuy nhiên, các phiên bản từ 8 đến 15 được hỗ trợ).

  2. Đặt biến môi trường JAVA_HOME để trỏ đến JDK.

    • Trên Linux/macOS:

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

      1. Mở Control Panel (Bảng điều khiển).
      2. Chuyển đến phần "Hệ thống và bảo mật" > "Hệ thống" > "Cài đặt hệ thống nâng cao" > "Nâng cao" tab > "Biến môi trường..." .
      3. Trong mục "Biến người dùng" (danh sách ở trên cùng), nhấp vào "Mới...".
      4. Trong trường "Tên biến", hãy nhập JAVA_HOME.
      5. Nhấp vào "Browse Directory..." (Duyệt qua thư mục...).
      6. Chuyển đến thư mục JDK (ví dụ: C:\Program Files\Java\jdk1.8.0_152).
      7. Nhấp vào "OK" trên tất cả cửa sổ hộp thoại.

Tải dự án mẫu

Truy xuất dự án mẫu từ kho lưu trữ GitHub của Bazel:

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

Dự án mẫu cho hướng dẫn này nằm trong thư mục examples/java-tutorial và có cấu trúc như sau:

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

Tạo bằng Bazel

Thiết lập không gian làm việc

Trước khi tạo dự án, bạn cần thiết lập không gian làm việc của dự án đó. Không gian làm việc là một thư mục lưu giữ các tệp nguồn của dự án và dữ liệu đầu ra của bản dựng của Bazel. Tệp này cũng chứa các tệp mà Bazel nhận dạng là đặc biệt:

  • Tệp WORKSPACE xác định thư mục và nội dung của thư mục đó là một không gian làm việc Bazel và nằm ở thư mục gốc của cấu trúc thư mục dự án,

  • Một hoặc nhiều tệp BUILD cho Bazel biết cách tạo các phần khác nhau của dự án. (Một thư mục trong không gian làm việc chứa tệp BUILD là một gói. Bạn sẽ tìm hiểu về các gói ở phần sau trong hướng dẫn này).

Để chỉ định một thư mục làm không gian làm việc Bazel, hãy tạo một tệp trống có tên WORKSPACE trong thư mục đó.

Khi Bazel tạo dự án, tất cả dữ liệu đầu vào và phần phụ thuộc phải nằm trong cùng một không gian làm việc. Các tệp nằm trong các không gian làm việc khác nhau độc lập với nhau trừ khi được liên kết. Việc liên kết tệp nằm ngoài phạm vi của hướng dẫn này.

Tìm hiểu về tệp BUILD

Tệp BUILD chứa một số loại hướng dẫn khác nhau cho Bazel. Loại quan trọng nhất là quy tắc bản dựng. Quy tắc này cho Bazel biết cách tạo các đầu ra mong muốn, chẳng hạn như các tệp nhị phân hoặc thư viện có thể thực thi. Từng phiên bản của quy tắc bản dựng trong tệp BUILD được gọi là mục tiêu và trỏ đến tập hợp tệp nguồn và phần phụ thuộc cụ thể. Một mục tiêu cũng có thể trỏ đến mục tiêu.

Hãy xem tệp java-tutorial/BUILD:

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

Trong ví dụ của chúng tôi, mục tiêu ProjectRunner tạo bản sao cho quy tắc java_binary tích hợp của Bazel. Quy tắc này yêu cầu Bazel tạo một tệp .jar và một tập lệnh shell trình bao bọc (cả hai đều được đặt tên theo mục tiêu).

Các thuộc tính trong mục tiêu nêu rõ các phần phụ thuộc và tuỳ chọn của nó. Mặc dù thuộc tính name là bắt buộc, nhưng nhiều thuộc tính khác là không bắt buộc. Ví dụ: trong Mục tiêu quy tắc ProjectRunner, name là tên của mục tiêu, srcs chỉ định các tệp nguồn mà Bazel sử dụng để tạo mục tiêu và main_class chỉ định lớp chứa phương thức chính. (Bạn có thể nhận thấy ví dụ của chúng tôi sử dụng glob để truyền một tập hợp tệp nguồn đến Bazel thay vì liệt kê từng tệp một.)

Tạo dự án

Để tạo dự án mẫu, hãy chuyển đến thư mục java-tutorial và chạy:

bazel build //:ProjectRunner

Trong nhãn đích, phần // là vị trí của tệp BUILD so với gốc của không gian làm việc (trong trường hợp này là chính gốc), và ProjectRunner là tên đích trong tệp BUILD. (Bạn sẽ hãy tìm hiểu chi tiết hơn về nhãn mục tiêu ở cuối hướng dẫn này).

Bazel tạo ra kết quả tương tự như sau:

   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

Xin chúc mừng! Bạn vừa tạo mục tiêu Bazel đầu tiên! Xây dựng địa điểm trong khu Bazel kết quả trong thư mục bazel-bin ở gốc không gian làm việc. Duyệt qua nội dung của tệp này để biết ý tưởng về cấu trúc đầu ra của Bazel.

Bây giờ, hãy kiểm thử tệp nhị phân mới tạo:

bazel-bin/ProjectRunner

Xem xét biểu đồ phần phụ thuộc

Bazel yêu cầu các phần phụ thuộc bản dựng phải được khai báo rõ ràng trong tệp BUILD. Bazel sử dụng các câu lệnh đó để tạo biểu đồ phần phụ thuộc của dự án, cho phép tạo các bản dựng gia tăng chính xác.

Để trực quan hoá các phần phụ thuộc của dự án mẫu, bạn có thể tạo một bản trình bày văn bản của biểu đồ phần phụ thuộc bằng cách chạy lệnh sau tại thư mục gốc của không gian làm việc:

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

Lệnh trên yêu cầu Bazel tìm tất cả các phần phụ thuộc cho mục tiêu //:ProjectRunner (ngoại trừ phần phụ thuộc ngầm ẩn và máy chủ lưu trữ) và định dạng đầu ra dưới dạng biểu đồ.

Sau đó, dán văn bản vào GraphViz.

Như bạn có thể thấy, dự án có một mục tiêu duy nhất để tạo hai tệp nguồn mà không có phần phụ thuộc bổ sung nào:

Biểu đồ phần phụ thuộc của mục tiêu "ProjectRunner"

Sau khi thiết lập không gian làm việc, tạo dự án và kiểm tra các phần phụ thuộc của dự án, bạn có thể thêm một số độ phức tạp.

Tinh chỉnh bản dựng Bazel

Mặc dù một mục tiêu duy nhất là đủ cho các dự án nhỏ, nhưng bạn có thể muốn tách các dự án lớn hơn thành nhiều mục tiêu và gói để có thể gia tăng nhanh chóng bản dựng (tức là chỉ xây dựng lại những nội dung thay đổi) và tăng tốc các bản dựng bằng cách xây dựng nhiều phần của một dự án cùng một lúc.

Chỉ định nhiều mục tiêu bản dựng

Bạn có thể chia bản dựng dự án mẫu thành hai mục tiêu. Thay thế nội dung của tệp java-tutorial/BUILD bằng nội dung sau:

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"],
)

Với cấu hình này, trước tiên, Bazel sẽ tạo thư viện greeter, sau đó là tệp nhị phân ProjectRunner. Thuộc tính deps trong java_binary cho Bazel biết rằng thư viện greeter là bắt buộc để tạo tệp nhị phân ProjectRunner.

Để tạo phiên bản mới này của dự án, hãy chạy lệnh sau:

bazel build //:ProjectRunner

Bazel tạo ra kết quả tương tự như sau:

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

Bây giờ, hãy kiểm thử tệp nhị phân mới tạo:

bazel-bin/ProjectRunner

Nếu bây giờ bạn sửa đổi ProjectRunner.java và tạo lại dự án, thì chỉ Bazel biên dịch lại tệp đó.

Nhìn vào biểu đồ phần phụ thuộc, bạn có thể thấy rằng ProjectRunner phụ thuộc vào phần phụ thuộc đầu vào tương tự như trước đây, nhưng cấu trúc của bản dựng lại khác:

Biểu đồ phần phụ thuộc của mục tiêu "ProjectRunner" sau khi thêm phần phụ thuộc

Lúc này, bạn đã xây dựng dự án với hai mục tiêu. Bản dựng mục tiêu ProjectRunner hai tệp nguồn và phụ thuộc vào một mục tiêu khác (:greeter), mục tiêu này tạo một tệp nguồn bổ sung.

Sử dụng nhiều gói

Bây giờ, hãy chia dự án thành nhiều gói. Nếu xem thư mục src/main/java/com/example/cmdline, bạn có thể thấy thư mục này cũng chứa một tệp BUILD cùng với một số tệp nguồn. Do đó, đối với Bazel, không gian làm việc hiện tại chứa hai gói, //src/main/java/com/example/cmdline// (kể từ có một tệp BUILD ở gốc không gian làm việc).

Hãy xem tệp src/main/java/com/example/cmdline/BUILD:

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

Mục tiêu runner phụ thuộc vào mục tiêu greeter trong gói // (do đó nhãn mục tiêu //:greeter) – Bazel biết điều này thông qua thuộc tính deps. Hãy xem biểu đồ phần phụ thuộc:

Biểu đồ phần phụ thuộc của "runner" mục tiêu

Tuy nhiên, để quá trình tạo bản dựng thành công, bạn phải cung cấp mục tiêu runner một cách rõ ràng trong //src/main/java/com/example/cmdline/BUILD khả năng hiển thị để nhắm mục tiêu trong //BUILD bằng thuộc tính visibility. Điều này là do các mục tiêu mặc định chỉ hiển thị với các mục tiêu khác trong cùng một tệp BUILD. (Bazel sử dụng mục tiêu chế độ hiển thị để ngăn chặn các vấn đề như thư viện chứa thông tin triển khai rò rỉ vào API công khai.)

Để thực hiện việc này, hãy thêm thuộc tính visibility vào mục tiêu greeter trong java-tutorial/BUILD như minh hoạ dưới đây:

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

Bây giờ, bạn có thể tạo gói mới bằng cách chạy lệnh sau tại thư mục gốc của không gian làm việc:

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

Bazel sẽ tạo ra kết quả tương tự như sau:

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

Bây giờ, hãy kiểm thử tệp nhị phân mới tạo:

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

Bạn hiện đã sửa đổi dự án để xây dựng dưới dạng hai gói, mỗi gói chứa một nhắm mục tiêu và hiểu được các phần phụ thuộc giữa chúng.

Sử dụng nhãn để tham chiếu mục tiêu

Trong các tệp BUILD và tại dòng lệnh, Bazel sử dụng nhãn đích để tham chiếu mục tiêu – ví dụ: //:ProjectRunner hoặc //src/main/java/com/example/cmdline:runner. Cú pháp của các hàm này như sau:

//path/to/package:target-name

Nếu mục tiêu là mục tiêu quy tắc, thì path/to/package là đường dẫn đến thư mục chứa tệp BUILDtarget-name là tên mà bạn đã đặt tên cho đích trong tệp BUILD (thuộc tính name). Nếu mục tiêu là một tệp target, thì path/to/package là đường dẫn đến thư mục gốc của gói và target-name là tên của tệp đích, bao gồm cả đường dẫn đầy đủ của tệp đó.

Khi tham chiếu các mục tiêu ở thư mục gốc của kho lưu trữ, đường dẫn gói sẽ trống, chỉ cần sử dụng //:target-name. Khi tham chiếu các mục tiêu trong cùng một BUILD bạn thậm chí có thể bỏ qua mã nhận dạng gốc của không gian làm việc // và chỉ sử dụng :target-name.

Ví dụ: đối với các mục tiêu trong tệp java-tutorial/BUILD, bạn không cần phải chỉ định đường dẫn gói, vì gốc không gian làm việc chính là một gói (//) và hai nhãn mục tiêu đơn giản là //:ProjectRunner//:greeter.

Tuy nhiên, đối với các mục tiêu trong tệp //src/main/java/com/example/cmdline/BUILD, bạn phải chỉ định đường dẫn gói đầy đủ của //src/main/java/com/example/cmdline và nhãn mục tiêu là //src/main/java/com/example/cmdline:runner.

Đóng gói mục tiêu Java để triển khai

Bây giờ, hãy đóng gói một mục tiêu Java để triển khai bằng cách tạo tệp nhị phân với tất cả các phần phụ thuộc thời gian chạy của mục tiêu đó. Điều này cho phép bạn chạy tệp nhị phân bên ngoài môi trường phát triển.

Như bạn đã nhớ, quy tắc bản dựng java_binary tạo một .jar và một tập lệnh shell trình bao bọc. Hãy xem nội dung của runner.jar bằng lệnh sau:

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

Nội dung bao gồm:

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

Như bạn có thể thấy, runner.jar chứa Runner.class nhưng không chứa phần phụ thuộc của Greeting.class. Tập lệnh runner do Bazel tạo sẽ thêm greeter.jar cho classpath, vì vậy, nếu bạn để như thế này, trang sẽ chạy cục bộ, nhưng sẽ không chạy độc lập trên một máy khác. Rất may là quy tắc java_binary cho phép bạn tạo tệp nhị phân độc lập, có thể triển khai. Để tạo ứng dụng, hãy thêm _deploy.jar thành tên mục tiêu:

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

Bazel tạo ra kết quả tương tự như sau:

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

Bạn vừa tạo runner_deploy.jar, bạn có thể chạy độc lập khỏi môi trường phát triển vì tệp này chứa các phần phụ thuộc thời gian chạy bắt buộc. Hãy xem nội dung của tệp JAR độc lập này bằng cách sử dụng tương tự như trước:

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

Nội dung bao gồm tất cả các lớp cần thiết để chạy:

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

Tài liệu đọc thêm

Để biết thêm thông tin, hãy xem:

Chúc bạn tạo dựng thành công!