Hướng dẫn dành cho Bazel: Xây dựng dự án Java

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.
Báo cáo vấn đề Xem nguồn

Hướng dẫn này trình bày các thông tin 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:

  • Xây dựng mục tiêu
  • Hình ảnh hóa 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
  • Nhắm mục tiê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, trước tiên, hãy Install Bazel nếu bạn chưa cài đặt ứng dụng này.

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. Thiết lập 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 mục "Hệ thống và bảo mật" > "Hệ thống" > "Cài đặt hệ thống nâng cao" > thẻ "Nâng cao" > "Biến môi trường..." .
      3. Trong danh sách "Biến người dùng" (danh sách ở trên cùng), hãy 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 "Duyệt 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á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

Xây dựng bằng Bazel

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

Trước khi có thể 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 chứa các tệp nguồn của dự án và kết quả 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 ra là đặc biệt:

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

  • Một hoặc nhiều tệp BUILD sẽ hướng dẫn Bazel 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 có chứa tệp BUILD là một gói. Bạn sẽ tìm hiểu về các gói 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 là WORKSPACE trong thư mục đó.

Khi Bazel xây dựng dự án, tất cả các đầu vào và phần phụ thuộc phải ở trong cùng một không gian làm việc. Các tệp nằm trong không gian làm việc khác nhau sẽ độc lập với nhau, trừ trường hợp được liên kết. Điều này 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 nhiều loại hướng dẫn khác nhau cho Bazel. Loại quan trọng nhất là quy tắc xây dựng cho Bazel biết cách tạo đầ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. Mỗi thực thể của quy tắc bản dựng trong tệp BUILD được gọi là một mục tiêu (target) và trỏ đến một 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 các mục tiêu khác.

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 ta, mục tiêu ProjectRunner sẽ tạo bản sao Quy tắc java_binary tích hợp của Bazel. Quy tắc này yêu cầu Bazel tạo tệp .jar và tập lệnh bao bọc môi trường shell (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à tùy chọn của mục tiêu đó. Mặc dù thuộc tính name là bắt buộc, nhưng nhiều thuộc tính 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 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 rằng ví dụ của chúng tôi sử dụng glob để chuyển một tập hợp các tệp nguồn đến Bazel thay vì liệt kê từng tệp một.)

Xây dựng dự án

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

bazel build //:ProjectRunner

Trong nhãn mục tiêu, phần // là vị trí của tệp BUILD tương ứng với thư mục gốc của không gian làm việc (trong trường hợp này là thư mục gốc) và ProjectRunner là tên mục tiêu trong tệp BUILD. (Bạn sẽ 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 xây dựng mục tiêu Bazel đầu tiên của mình! Bazel đặt đầu ra bản dựng trong thư mục bazel-bin ở gốc của không gian làm việc. Duyệt xem nội dung để hiểu rõ cấu trúc đầu ra của Bazel.

Bây giờ, hãy kiểm tra tệp nhị phân mới tạo của bạn:

bazel-bin/ProjectRunner

Xem lại biểu đồ phần phụ thuộc

Bazel yêu cầu khai báo các phần phụ thuộc của bản dựng một cách rõ ràng trong các 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 tăng dần 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 nội dung biểu diễn văn bản dưới dạng 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 //:ProjectRunner mục tiêu (không bao gồm các phần phụ thuộc của máy chủ và ngầm ẩn) và định dạng kết quả ở 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ó thêm phần phụ thuộc nào:

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

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, thì bạn có thể thêm một số điểm phức tạp.

Tinh chỉnh bản dựng Bazel của bạn

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

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

Bạn có thể tách 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 đó tạo tệp nhị phân ProjectRunner. Thuộc tính deps trong java_binary cho Bazel biết rằng bạn cần có thư viện greeter để tạo tệp nhị phân ProjectRunner.

Để tạo phiên bản mới của dự án này, 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 tra tệp nhị phân mới tạo của bạn:

bazel-bin/ProjectRunner

Nếu bạn hiện sửa đổi ProjectRunner.java và tạo lại dự án, Bazel sẽ chỉ 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 ProjectRunner phụ thuộc vào các dữ liệu đầu vào giống như trước đây, nhưng cấu trúc của bản dựng thì khác:

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

Bạn đã xây dựng dự án với 2 mục tiêu. Mục tiêu ProjectRunner tạo 2 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 tệp BUILD và một số tệp nguồn. Do đó, đối với Bazel, không gian làm việc hiện chứa hai gói, //src/main/java/com/example/cmdline// (vì 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 đó có nhãn mục tiêu //:greeter) – Bazel biết điều này thông qua thuộc tính deps. Xem biểu đồ phần phụ thuộc:

Biểu đồ phần phụ thuộc của "trình chạy" mục tiêu

Tuy nhiên, để xây dựng thành công, bạn phải thể hiện rõ mục tiêu runner trong chế độ hiển thị //src/main/java/com/example/cmdline/BUILD cho các mục tiêu trong //BUILD bằng cách sử dụng thuộc tính visibility. Lý do là theo mặc định, các mục tiêu chỉ hiển thị với các mục tiêu khác trong cùng tệp BUILD. (Bazel sử dụng chế độ hiển thị mục tiêu để ngăn các vấn đề như các thư viện chứa thông tin triển khai bị rò rỉ vào các 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ư đoạn mã bên dưới:

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 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 tra tệp nhị phân mới tạo của bạn:

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

Giờ đây, bạn đã sửa đổi dự án để tạo thành hai gói, mỗi gói chứa một mục tiêu và hiểu các phần phụ thuộc giữa các gói đó.

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 mục tiêu để tham chiếu các mục tiêu – ví dụ: //:ProjectRunner hoặc //src/main/java/com/example/cmdline:runner. Cú pháp của họ 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 bạn đã đặt tên trong tệp BUILD (thuộc tính name). Nếu mục tiêu là mục tiêu tệp, thì path/to/package là đường dẫn đến gốc của gói và target-name là tên của tệp mục tiêu, bao gồm cả đường dẫn đầy đủ của tệp.

Khi tham chiếu các mục tiêu ở gốc kho lưu trữ, đường dẫn gói 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 tệp 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 phải chỉ định đường dẫn gói, vì bản thân không gian làm việc là một gói (//) và hai nhãn mục tiêu chỉ đơ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ột 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 có tất cả phần phụ thuộc thời gian chạy. Đ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 đã biết, quy tắc xây dựng java_binary sẽ tạo ra một .jar và một tập lệnh 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

Có những nội dung sau:

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 Greeting.class. Tập lệnh runner mà Bazel tạo sẽ thêm greeter.jar vào classpath, vì vậy nếu bạn để như vậy, tập lệnh này sẽ chạy cục bộ nhưng sẽ không chạy độc lập trên một máy khác. May mắn thay, quy tắc java_binary cho phép bạn tạo một tệp nhị phân độc lập, có thể triển khai. Để tạo tên, hãy thêm _deploy.jar vào 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 mà có thể chạy độc lập với môi trường phát triển của mình vì môi trường 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 lệnh 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

Đọc thêm

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

Chúc bạn xây dựng vui vẻ!