读书笔记《gradle-essentials》多项目构建
multiproject(或多模块,有些人喜欢这样称呼它)是一组在逻辑上相互关联并且通常具有相同开发-构建的项目-发布周期。目录结构对于制定构建此类项目的策略很重要。通常,顶级根项目包含一个或多个子项目。根项目可能包含它自己的源集,可能只包含测试子项目集成的集成测试,或者甚至可以充当没有任何源和测试的主构建。 Gradle 支持所有这样的配置。
子项目相对于根项目的排列可能是扁平的,即所有子项目都是根项目的直接子项目(如示例 1 所示)或者是分层的,这样子项目也可能有嵌套的子项目(如示例 2) 或任何混合目录结构中显示。
让我们将以下目录结构称为示例 1:
在示例 1 中,我们看到一个虚构的示例项目,其中所有子项目都是根项目的直接子项目,并且彼此是兄弟项目。只是为了这个例子,我们将我们的应用程序分成三个子项目,分别命名为 :repository
、:services
和 :web-app
。正如他们的名字所暗示的那样,存储库包含数据访问代码,而服务是以可消费API的形式封装业务规则的层。 web-app
仅包含特定于 Web 应用程序的代码,例如控制器和视图模板。但是,请注意 :web-app
项目可能依赖于 :services
项目,而 :services
项目又可能依赖于 < code class="literal">:repository 项目。我们很快就会看到这些依赖项是如何工作的。
让我们看一个相对更复杂的结构,称之为样本 2:
我们的应用程序现在已经发展,为了满足更多需求,我们为其添加了更多功能。我们已经为我们的应用程序创建了更多子项目,例如桌面客户端和命令行界面。在示例 2 中,根项目分为三个项目(组),它们有自己的子项目。在此示例中,每个目录都可以视为一个项目。此示例的目的是仅显示一种可能的目录结构。 Gradle 不会将一个目录结构强加于另一个目录结构。
有人可能想知道,我们将所有的 build.gradle
文件放在哪里以及它们的内容是什么?这取决于我们的需求以及我们希望如何构建我们的构建。我们将在了解什么是 settings.gradle
后不久回答所有这些问题。
在 初始化期间,Gradle 会读取 settings.gradle
文件以确定哪些项目将参与建造。 Gradle 创建一个 Setting
类型的对象。这甚至发生在任何 build.gradle
被解析之前。它通常与 build.gradle
平行放置在根项目中。建议将 setting.gradle
放在根项目中,否则我们必须通过命令行选项 -c
。将这两个文件添加到示例 1 的目录结构中会得到如下信息:
settings.gradle
最常见的用途是征用所有参与构建的子项目:
此外,这就是告诉 Gradle 当前构建是多项目构建所需的全部内容。当然,这不是故事的结束,我们可以用多项目构建做更多的事情,但这是最低限度的,有时足以让多项目构建工作。
Settings
的方法和属性在 settings.gradle
文件中可用,并在 Settings
实例就像 Project
API 的方法在 build.gradle
文件中可用,如我们在上一章看到了。
让我们通过从命令行调用任务 projects
来查询项目。 projects
任务 列出了 Gradle 构建中可用的所有项目:
我们可以在 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)。
Gradle 让 我们可以灵活地为所有项目创建一个构建文件或为每个项目创建单独的构建文件;你也可以混搭。让我们从向根项目的 build.gradle
添加一个简单的任务开始:
我们正在创建 一个任务,其动作只是打印一条消息。现在,让我们检查一下我们的根项目中有哪些任务可用。从 root
目录中,我们将任务称为 tasks
:
难怪 sayHello
任务在根项目中可用。但是,如果我们只想查看子项目中可用的任务怎么办?假设 :repository
。对于多项目构建,我们可以使用 gradle <project-path>:<task-name>
语法或进入子项目目录和执行 gradle <task-name>
。所以现在,如果我们执行以下代码,我们将看不到 sayHello
任务:
这是因为 sayHello
只是为根项目定义的;因此,它在子项目中不可用。
有时我们希望所有项目(包括根项目)都可以使用相同的任务。例如,让我们想象一个只打印项目名称的任务。我们有四个项目,包括根项目,我们希望为每个项目定义相同的任务。如果我们必须编写四次相同的代码,每个项目一个,这不是矫枉过正吗?当然可以,这就是为什么 Gradle DSL 为在所有项目中声明通用构建元素提供了一流的支持。
看看下面的代码片段,我们将把它添加到根项目的 build.gradle
中:
在尝试理解代码片段之前,让我们再次运行熟悉的任务。首先,从根项目:
然后,从存储库项目:
我们在存储库项目中也看到 whoami
任务。让我们揭开使之成为可能的 allprojects
方法。
allprojects
方法接受一个闭包,并在构建文件的项目(对象)和当前项目的所有子项目上执行它。因此,如果在根项目中定义了 allproject
,则该块将被一一应用于所有项目,每个项目对象一次作为隐式引用。
现在,让我们了解代码片段。我们在 allprojects
块中声明的任务(传递给 allprojects
的闭包,在技术上是正确的)被应用于所有的项目。该任务的操作使用 project
对象引用打印项目名称。请记住,project
对象将引用不同的项目,具体取决于调用任务的项目。发生这种情况是因为在配置阶段,一旦我们拥有该项目的 project
引用,就会为每个项目执行 allproject
块。
传递给 allproject
的闭包内的内容看起来与单个项目的 build.gradle
文件完全相同。我们甚至可以应用插件、声明存储库和依赖项等等。因此,本质上,我们可以编写所有项目通用的任何构建逻辑,然后将其应用于所有项目。 allprojects
方法也可用于查询当前构建中的项目对象。 allprojects
的详细信息请参阅项目的 API。
如果我们将 --all
标志传递给 tasks
任务,我们将看到 whoami
任务存在于所有子项目中,除了 root
项目:
如果我们想只在特定项目上执行whoami
,比如说:repository
,就像下面的命令一样简单:
当我们在没有任何项目路径的情况下执行 whoami
时:
哇,Gradle 加倍努力确保我们从父项目执行任务时,也执行同名的子项目任务。当我们考虑诸如 assemble
之类的任务时,这非常方便,我们实际上希望所有子项目都组装或测试,它测试根和子项目。
但是,如果只在根项目上执行任务呢?确实,一个有效的场景。记住绝对任务路径:
冒号使一切变得不同。这里,我们仅指 root
项目的 whoami
。没有其他任务匹配相同的路径。例如,存储库的 whoami
有一个路径 repository:whoami
。
现在,cd
在repository
目录下,然后执行whoami
:
所以任务执行是上下文相关的。在这里,默认情况下,Gradle 假定只能在当前项目上调用任务。不错,不是吗?
让我们在现有的 build.gradle
文件中添加更多动态代码:
在这里,根据项目名称,我们将任务名称设置为describe
,前缀为项目名称。所以所有项目都有他们的任务,但名称不会相同。我们添加一个仅打印项目名称的操作。如果我们现在在我们的项目上执行 tasks
,我们可以看到任务名称包括项目名称:
虽然这个例子很琐碎,但我们学到了一些东西。首先,allprojects
块与 Gradle 中的大多数其他方法一样是相加的。我们添加了第二个 allprojects
块并且都工作得很好。其次,可以动态分配任务名称,例如使用项目名称。
现在,我们可以从项目根目录调用任何 describe*
任务。此外,正如我们可能猜到的,任务名称是唯一的;我们不需要预先设置项目路径:
让我们 cd
进入 repository
目录并列出任务:
让我们继续我们的示例。在这里,根项目将没有任何源集,因为所有 Java 代码都将位于三个子项目之一中。因此,将 java
插件仅应用于子项目不是明智的吗?这正是 subprojects
方法发挥作用的地方,也就是说,当我们只想在子项目上应用一些构建逻辑而不影响父项目时。它的用法类似于allprojects
。让我们将 java
插件应用于所有子项目:
现在,运行 gradle tasks
应该也会向我们展示 java
插件添加的任务。尽管看起来这些任务在根项目中可用,但实际上并非如此。在这种情况下检查 gradle -q tasks --all
的输出。子项目中的任务可以从根项目中调用,但这并不意味着它们存在于根项目中。 java
插件添加的任务将仅在子项目中可用,而帮助任务等任务将在所有项目中可用。
在这一章的开头,我们提到一个子项目可能依赖于另一个子项目,就像它依赖于外部库依赖一样。例如 services
项目的编译依赖于 repository
项目,这意味着我们需要从 repository
项目在 services
项目的编译类路径中可用。
为了实现这一点,我们当然可以在 services
项目中创建一个 build.gradle
文件,并将依赖声明放在那里.然而,为了展示另一种方式,我们将把这个声明放在 root
项目的 build.gradle
中。
与 allprojects
或 subprojects
不同,我们需要一种更精细的机制来仅从 配置单个项目root
项目的 build.gradle
。事实证明,使用 project
方法非常容易。除了将应用闭包的项目名称之外,此方法还接受一个闭包,就像 allprojects
和 subprojects
方法一样。在 配置阶段,闭包在该项目的对象上执行。
因此,让我们将其添加到根项目的 build.gradle
中:
在这里,我们只为 services
项目配置依赖项。在 dependencies
块中,我们声明 :repository
项目是 服务
项目。这或多或少类似于外部库声明;而不是 group-id:artifact-id:version
表示法中的库名称,我们使用 project(:sub-project)
引用子项目。
我们还说过 web-app
项目依赖于 services
项目。所以这一次,让我们使用 web-app
自己的build.gradle
来声明这个依赖。我们将在 web-app
目录下创建一个 build.gradle
文件:
由于这是一个特定于项目的构建文件,我们可以像在任何其他项目中一样添加 dependencies
块:
现在,让我们使用 dependencies
任务可视化 Web 项目的依赖关系:
Gradle向我们展示了web-app
在各种配置下的依赖关系。此外,我们可以清楚地看到 Gradle 理解传递依赖;因此,它显示 web-app
通过 services
传递依赖于 repository
。请注意,我们实际上并未在任何项目中声明任何外部依赖项(例如 servlet-api
),否则它们也会出现在此处。
为了过滤和配置选定的项目,值得查看 project
对象上的 configure
方法的变体。关于 configure
方法的更多信息可以在 https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html。