vlambda博客
学习文章列表

读书笔记《gradle-essentials》多项目构建

Chapter 5. Multiprojects Build

现在我们熟悉了构建脚本语法,我们准备处理更复杂的项目结构。在本章中,我们将重点关注跨多个项目的构建、它们的相互依赖关系以及介于两者之间的更多内容。

随着项目代码库的增长,很多时候,希望根据层、职责、生成的工件,有时甚至取决于开发团队,将其拆分为多个模块,以有效地分解工作。不管是什么原因,现实是大项目迟早会分解成更小的子项目。此外,像 Gradle 这样的构建工具完全能够处理复杂性。

The multiproject directory layout


multiproject(或多模块,有些人喜欢这样称呼它)是一组在逻辑上相互关联并且通常具有相同开发-构建的项目-发布周期。目录结构对于制定构建此类项目的策略很重要。通常,顶级根项目包含一个或多个子项目。根项目可能包含它自己的源集,可能只包含测试子项目集成的集成测试,或者甚至可以充当没有任何源和测试的主构建。 Gradle 支持所有这样的配置。

子项目相对于根项目的排列可能是扁平的,即所有子项目都是根项目的直接子项目(如示例 1 所示)或者是分层的,这样子项目也可能有嵌套的子项目(如示例 2) 或任何混合目录结构中显示。

让我们将以下目录结构称为示例 1:

sample1
├── repository
├── services
└── web-app

在示例 1 中,我们看到一个虚构的示例项目,其中所有子项目都是根项目的直接子项目,并且彼此是兄弟项目。只是为了这个例子,我们将我们的应用程序分成三个子项目,分别命名为 :repository:services:web-app。正如他们的名字所暗示的那样,存储库包含数据访问代码,而服务是以可消费API的形式封装业务规则的层。 web-app 仅包含特定于 Web 应用程序的代码,例如控制器和视图模板。但是,请注意 :web-app 项目可能依赖于 :services 项目,而 :services 项目又可能依赖于 < code class="literal">:repository 项目。我们很快就会看到这些依赖项是如何工作的。

Tip

不要将多项目结构与单个项目中的多个源目录混淆。

让我们看一个相对更复杂的结构,称之为样本 2:

sample2
├── core
│   ├── models
│   ├── repository
│   └── services
├── client
│   ├── client-api
│   ├── cli-client
│   └── desktop-client
└── web 
    ├── webservices
    └── webapp

我们的应用程序现在已经发展,为了满足更多需求,我们为其添加了更多功能。我们已经为我们的应用程序创建了更多子项目,例如桌面客户端和命令行界面。在示例 2 中,根项目分为三个项目(组),它们有自己的子项目。在此示例中,每个目录都可以视为一个项目。此示例的目的是仅显示一种可能的目录结构。 Gradle 不会将一个目录结构强加于另一个目录结构。

有人可能想知道,我们将所有的 build.gradle 文件放在哪里以及它们的内容是什么?这取决于我们的需求以及我们希望如何构建我们的构建。我们将在了解什么是 settings.gradle 后不久回答所有这些问题。

The settings.gradle file


初始化期间,Gradle 会读取 settings.gradle 文件以确定哪些项目将参与建造。 Gradle 创建一个 Setting 类型的对象。这甚至发生在任何 build.gradle 被解析之前。它通常与 build.gradle 平行放置在根项目中。建议将 setting.gradle 放在根项目中,否则我们必须通过命令行选项 -c。将这两个文件添加到示例 1 的目录结构中会得到如下信息:

sample1
├── repository
│   └── ...
├── services
│   └── ...
├── web-app
│   └── ...
├── build.gradle
└── settings.gradle

settings.gradle 最常见的用途是征用所有参与构建的子项目:

include ':repository', ':services', ':web-app'

此外,这就是告诉 Gradle 当前构建是多项目构建所需的全部内容。当然,这不是故事的结束,我们可以用多项目构建做更多的事情,但这是最低限度的,有时足以让多项目构建工作。

Settings 的方法和属性在 settings.gradle 文件中可用,并在 Settings 实例就像 Project API 的方法在 build.gradle 文件中可用,如我们在上一章看到了。

Note

您想知道为什么在上一节中的项目名称之前使用冒号(:)吗?它表示相对于根项目的项目路径。但是,include 方法允许 1 级子项目名称省略冒号。因此,include 调用可以重写如下:

include 'repository', 'services', 'web-app'

让我们通过从命令行调用任务 projects 来查询项目。 projects 任务 列出了 Gradle 构建中可用的所有项目:

$ gradle projects
:projects
------------------------------------------------------------
Root project
------------------------------------------------------------

Root project 'sample1'
+--- Project ':repository'
+--- Project ':services'
\--- Project ':web-app'

To see a list of the tasks of a project, run gradle <project-path>:tasks.
For example, try running gradle :repository:tasks.

BUILD SUCCESSFUL

Note

如果嵌套深度超过一层,例如示例 2,则所有项目都必须包含在根项目 settings.gradle 中,语法如下:

include 'core',
  'core:models', 'core:repository', 'core:services',
  'client' //... so on

我们可以在 Settings DSL 文档(http://www.gradle.org/docs/ current/dsl/org.gradle.api.initialization.Settings.html)和 Settings API 文档(http://www.gradle.org/docs /current/javadoc/org/gradle/api/initialization/Settings.html)。

Organizing build logic in multiproject builds


Gradle 让 我们可以灵活地为所有项目创建一个构建文件或为每个项目创建单独的构建文件;你也可以混搭。让我们从向根项目的 build.gradle 添加一个简单的任务开始:

task sayHello << {
    println "Hello from multi-project build"
}

我们正在创建 一个任务,其动作只是打印一条消息。现在,让我们检查一下我们的根项目中有哪些任务可用。从 root 目录中,我们将任务称为 tasks

$ gradle tasks
...

Other tasks
-----------
sayHello

....

难怪 sayHello 任务在根项目中可用。但是,如果我们只想查看子项目中可用的任务怎么办?假设 :repository。对于多项目构建,我们可以使用 gradle <project-path>:<task-name> 语法或进入子项目目录和执行 gradle <task-name>。所以现在,如果我们执行以下代码,我们将看不到 sayHello 任务:

$ gradle repository:tasks

这是因为 sayHello 只是为根项目定义的;因此,它在子项目中不可用。

Applying a build logic to all projects

有时我们希望所有项目(包括根项目)都可以使用相同的任务。例如,让我们想象一个只打印项目名称的任务。我们有四个项目,包括根项目,我们希望为每个项目定义相同的任务。如果我们必须编写四次相同的代码,每个项目一个,这不是矫枉过正吗?当然可以,这就是为什么 Gradle DSL 为在所有项目中声明通用构建元素提供了一流的支持。

看看下面的代码片段,我们将把它添加到根项目的 build.gradle 中:

allprojects {
    task whoami << {println "I am ${project.name}"}
}

在尝试理解代码片段之前,让我们再次运行熟悉的任务。首先,从根项目:

$ gradle tasks
...
Other tasks
-----------
sayHello
whoami
...

然后,从存储库项目:

$ gradle repository:tasks
...
Other tasks
-----------
whoami
...

我们在存储库项目中也看到 whoami 任务。让我们揭开使之成为可能的 allprojects 方法。

allprojects 方法接受一个闭包,并在构建文件的项目(对象)和当前项目的所有子项目上执行它。因此,如果在根项目中定义了 allproject,则该块将被一一应用于所有项目,每个项目对象一次作为隐式引用。

现在,让我们了解代码片段。我们在 allprojects 块中声明的任务(传递给 allprojects 的闭包,在技术上是正确的)被应用于所有的项目。该任务的操作使用 project 对象引用打印项目名称。请记住,project 对象将引用不同的项目,具体取决于调用任务的项目。发生这种情况是因为在配置阶段,一旦我们拥有该项目的 project 引用,就会为每个项目执行 allproject 块。

传递给 allproject 的闭包内的内容看起来与单个项目的 build.gradle 文件完全相同。我们甚至可以应用插件、声明存储库和依赖项等等。因此,本质上,我们可以编写所有项目通用的任何构建逻辑,然后将其应用于所有项目。 allprojects 方法也可用于查询当前构建中的项目对象。 allprojects 的详细信息请参阅项目的 API。

如果我们将 --all 标志传递给 tasks 任务,我们将看到 whoami 任务存在于所有子项目中,除了 root 项目:

$ gradle tasks --all
...
Other tasks
-----------
sayHello
whoami
repository:whoami
services:whoami
web-app:whoami
...

如果我们想只在特定项目上执行whoami,比如说:repository,就像下面的命令一样简单:

$ gradle -q repository:whoami
I am repository

当我们在没有任何项目路径的情况下执行 whoami 时:

$ gradle -q whoami
I am root
I am repository
I am services
I am web-app

哇,Gradle 加倍努力确保我们从父项目执行任务时,也执行同名的子项目任务。当我们考虑诸如 assemble 之类的任务时,这非常方便,我们实际上希望所有子项目都组装或测试,它测试根和子项目。

但是,如果只在根项目上执行任务呢?确实,一个有效的场景。记住绝对任务路径:

$ gradle -q :whoami
I am root

冒号使一切变得不同。这里,我们仅指 root 项目的 whoami。没有其他任务匹配相同的路径。例如,存储库的 whoami 有一个路径 repository:whoami

现在,cdrepository目录下,然后执行whoami

$ gradle –q whoami
I am repository

所以任务执行是上下文相关的。在这里,默认情况下,Gradle 假定只能在当前项目上调用任务。不错,不是吗?

让我们在现有的 build.gradle 文件中添加更多动态代码:

allprojects {
  task("describe${project.name.capitalize()}") << {
    println project.name
  }
}

在这里,根据项目名称,我们将任务名称设置为describe,前缀为项目名称。所以所有项目都有他们的任务,但名称不会相同。我们添加一个仅打印项目名称的操作。如果我们现在在我们的项目上执行 tasks,我们可以看到任务名称包括项目名称:

$ gradle tasks 
...
Other tasks
-----------
describeRepository
describeSample1
describeServices
describeWeb-app
sayHello
whoami
...

虽然这个例子很琐碎,但我们学到了一些东西。首先,allprojects 块与 Gradle 中的大多数其他方法一样是相加的。我们添加了第二个 allprojects 块并且都工作得很好。其次,可以动态分配任务名称,例如使用项目名称。

现在,我们可以从项目根目录调用任何 describe* 任务。此外,正如我们可能猜到的,任务名称是唯一的;我们不需要预先设置项目路径:

$ gradle -q describeServices 
services

让我们 cd 进入 repository 目录并列出任务:

$ gradle -q tasks  
...
Other tasks
-----------
describeRepository
whoami

我们只看到适用于当前项目的任务,即repository

Applying build logic to subprojects

让我们继续我们的示例。在这里,根项目将没有任何源集,因为所有 Java 代码都将位于三个子项目之一中。因此,将 java 插件仅应用于子项目不是明智的吗?这正是 subprojects 方法发挥作用的地方,也就是说,当我们只想在子项目上应用一些构建逻辑而不影响父项目时。它的用法类似于allprojects。让我们将 java 插件应用于所有子项目:

subprojects {
  apply plugin: 'java'
}

现在,运行 gradle tasks 应该也会向我们展示 java 插件添加的任务。尽管看起来这些任务在根项目中可用,但实际上并非如此。在这种情况下检查 gradle -q tasks --all 的输出。子项目中的任务可以从根项目中调用,但这并不意味着它们存在于根项目中。 java 插件添加的任务将仅在子项目中可用,而帮助任务等任务将在所有项目中可用。

Dependency on subprojects

这一章的开头,我们提到一个子项目可能依赖于另一个子项目,就像它依赖于外部库依赖一样。例如 services 项目的编译依赖于 repository 项目,这意味着我们需要从 repository 项目在 services 项目的编译类路径中可用。

为了实现这一点,我们当然可以在 services 项目中创建一个 build.gradle 文件,并将依赖声明放在那里.然而,为了展示另一种方式,我们将把这个声明放在 root 项目的 build.gradle 中。

allprojectssubprojects 不同,我们需要一种更精细的机制来仅从 配置单个项目root 项目的 build.gradle。事实证明,使用 project 方法非常容易。除了将应用闭包的项目名称之外,此方法还接受一个闭包,就像 allprojectssubprojects 方法一样。在 配置阶段,闭包在该项目的对象上执行。

因此,让我们将其添加到根项目的 build.gradle 中:

project(':services') {
  dependencies {
    compile project(':repository')
  }
}

在这里,我们只为 services 项目配置依赖项。在 dependencies 块中,我们声明 :repository 项目是 服务项目。这或多或少类似于外部库声明;而不是 group-id:artifact-id:version 表示法中的库名称,我们使用 project(:sub-project) 引用子项目。

我们还说过 web-app 项目依赖于 services 项目。所以这一次,让我们使用 web-app自己的build.gradle来声明这个依赖。我们将在 web-app 目录下创建一个 build.gradle 文件:

root
├── build.gradle
├── settings.gradle
├── repository
├── services
└── web-app
    └── build.gradle

由于这是一个特定于项目的构建文件,我们可以像在任何其他项目中一样添加 dependencies 块:

dependencies {
  compile project(':services')
}

现在,让我们使用 dependencies 任务可视化 Web 项目的依赖关系:

$ gradle -q web-app:dependencies

------------------------------------------------------------
Project :web-app
------------------------------------------------------------

archives - Configuration for archive artifacts.
No dependencies

compile - Compile classpath for source set 'main'.
\--- project :services
     \--- project :repository

default - Configuration for default artifacts.
\--- project :services
     \--- project :repository

runtime - Runtime classpath for source set 'main'.
\--- project :services
     \--- project :repository

testCompile - Compile classpath for source set 'test'.
\--- project :services
     \--- project :repository

testRuntime - Runtime classpath for source set 'test'.
\--- project :services
     \--- project :repository

Gradle向我们展示了web-app在各种配置下的依赖关系。此外,我们可以清楚地看到 Gradle 理解传递依赖;因此,它显示 web-app 通过 services 传递依赖于 repository。请注意,我们实际上并未在任何项目中声明任何外部依赖项(例如 servlet-api),否则它们也会出现在此处。

为了过滤和配置选定的项目,值得查看 project 对象上的 configure 方法的变体。关于 configure 方法的更多信息可以在 https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html

Summary


在这个简短的章节中,我们了解到 Gradle 支持复杂项目层次结构的灵活目录结构,并允许我们为构建选择正确的结构。然后,我们研究了 settings.gradle 在 mutliprojects 构建的上下文中的重要性。然后,我们看到了将构建逻辑应用于所有项目、子项目或仅单个项目的各种方法。最后,举一个项目间依赖的小例子。

就 Gradle 语法而言,这就是我们需要担心的全部。现在接下来的章节将主要关注各种插件添加到我们的构建中的功能以及我们如何配置它们。