Bazel 教程:构建 Java 项目

报告问题 查看来源 Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

本教程将介绍使用 Bazel 构建 Java 应用的基础知识。您将设置工作区并构建一个简单的 Java 项目,该项目将演示关键的 Bazel 概念,例如目标和 BUILD 文件。

预计完成时间: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. 点击“浏览目录…”。
      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 的 build 输出。它还包含 Bazel 识别为特殊的文件:

  • WORKSPACE 文件,用于将相应目录及其内容标识为 Bazel 工作区,并位于项目目录结构的根目录中,

  • 一个或多个 BUILD 文件,用于告知 Bazel 如何构建项目的不同部分。(工作区内包含 BUILD 文件的目录是一个软件包。您将在本教程的后面部分中了解软件包。)

如需将某个目录指定为 Bazel 工作区,请在该目录中创建一个名为 WORKSPACE 的空文件。

当 Bazel 构建项目时,所有输入和依赖项都必须位于同一工作区中。除非链接在一起,否则位于不同工作区中的文件彼此独立,这不在本教程的探讨范围内。

了解 BUILD 文件

BUILD 文件包含针对 Bazel 的多种不同类型的指令。最重要的类型是构建规则,它会告知 Bazel 如何构建所需的输出,例如可执行二进制文件或库。BUILD 文件中 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 指定包含 main 方法的类。(您可能已经注意到,我们的示例使用 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 会将 build 输出放置在工作区根目录的 bazel-bin 目录中。浏览其内容,了解 Bazel 的输出结构。

现在,测试您刚刚构建的二进制文件:

bazel-bin/ProjectRunner

查看依赖关系图

Bazel 要求在 BUILD 文件中明确声明 build 依赖项。Bazel 会使用这些语句来创建项目的依赖关系图,从而实现准确的增量构建。

如需直观呈现示例项目的依赖项,您可以在工作区根目录下运行以下命令,生成依赖关系图的文本表示形式:

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

上述命令会指示 Bazel 查找目标 //:ProjectRunner 的所有依赖项(不包括宿主依赖项和隐式依赖项),并将输出格式设置为图表。

然后,将文本粘贴到 GraphViz 中。

如您所见,该项目只有一个目标,用于构建两个没有其他依赖项的源文件:

目标“ProjectRunner”的依赖关系图

设置好工作区、构建项目并检查其依赖项后,您就可以添加一些复杂性了。

优化 Bazel build

虽然单个 build 目标足以满足小型项目的需求,但您可能需要将大型项目拆分为多个 build 目标和软件包,以便快速进行增量 build(即仅重新 build 更改的内容),并通过同时 build 项目的多个部分来加快 build 速度。

指定多个 build 目标

您可以将示例项目 build 拆分为两个目标。将 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 依赖的输入与之前相同,但 build 的结构不同:

添加依赖项后,目标“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”的依赖关系图

不过,为了使 build 成功,您必须使用 visibility 属性明确授予 //src/main/java/com/example/cmdline/BUILD 中的 runner 目标对 //BUILD 中目标的可见性。这是因为默认情况下,目标仅对同一 BUILD 文件中的其他目标可见。(Bazel 使用目标可见性来防止库包含实现细节等问题泄露到公共 API 中。)

为此,请将 visibility 属性添加到 java-tutorial/BUILD 中的 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 文件中的目标时,您甚至可以跳过 // 工作区根标识符,而只使用 :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 build 规则会生成 .jar 和封装容器 shell 脚本。使用以下命令查看 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

深入阅读

如需了解详情,请参阅以下文档:

祝您构建顺利!