配置子项目
到目前为止,您已经根据功能职责拆分了应用程序代码,并将其重新排列成单独的子项目。 现在,您将采用类似的方法以可维护的方式组织构建逻辑。 以下几点代表了实际多项目构建的常见要求:
-
根项目和所有子项目应使用相同的组和版本属性值。
-
所有子项目都是 Java 项目,并且需要 Java 插件才能正常运行,因此您只需将插件应用到子项目,而不是根项目。
-
Web 子项目是唯一声明外部依赖项的项目。 该项目类型源自其他子项目,因为它需要构建 WAR 存档而不是 JAR,并使用 Jetty 插件来运行应用程序。
-
对子项目之间的依赖关系进行建模。
在本节中,您将学习如何在多项目构建中定义项目的特定和常见行为,这是避免重复配置的强大方法。 某些子项目可能依赖于其他项目的已编译源代码 - 在您的应用程序中,存储库项目使用项目模型中的代码。 通过声明项目依赖项,您可以确保导入的类在类路径上可用。 在您用生命填充空的 build.gradle 文件之前,我们将回顾一下我尚未向您展示的 Project API 的方法,但这些方法与多项目构建的上下文相关。
了解项目 API 表示
在第 4 章中,我解释了您在日常业务中可能最常使用的 Project API 的属性和方法。 为了实现多项目构建,您需要了解一些新方法,如图 6.8 所示。
为了声明特定于项目的构建代码,使用方法项目。 至少,必须提供项目的路径(例如:model)。
很多时候,您会发现自己想要为所有项目或仅为构建的子项目定义通用行为。 对于每个用例,Project API 都提供了专门的方法:allprojects 和 subprojects。 假设您想要将 Java 插件应用到所有子项目,因为您需要编译 Java 源代码。 您可以通过在子项目闭包参数中定义代码来实现此目的。
多项目构建中项目的默认评估顺序基于其字母数字名称。 要在构建生命周期的配置时获得对评估顺序的显式控制,您可以使用项目评估方法 evaluationDependsOn 和 evaluationDependsOnChildren。 如果您需要确保在另一个项目使用某个属性之前为该项目设置该属性,则尤其如此。 我们不会在本章中讨论这些方法; 有关具体用例,请参阅 Gradle 的在线手册。
在本章中,您将使用所有介绍的方法来配置多项目构建。 首先,您将采用现有的构建代码并将其仅应用于特定的子项目。

定义具体行为
项目特定的行为是用方法项目定义的。 要为三个子项目(model、repository 和 Web)设置构建基础架构,您将为每个子项目创建一个项目配置块。 以下列表显示了单个 build.gradle 文件中的项目定义。
// 将额外的属性 projectIds 声明为保存组和版本的键值对的映射; 属性可以在子项目中使用
ext.projectIds = ['group': 'com.manning.gia', 'version': '0.1']
group = projectIds.group
version = projectIds.version
//按项目路径配置各个子项目; 实际配置发生在闭包中
project(':model') {
group = projectIds.group
version = projectIds.version
apply plugin: 'java'
}
project(':repository') {
group = projectIds.group
version = projectIds.version
apply plugin: 'java'
}
project(':web') {
group = projectIds.group
version = projectIds.version
apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'jetty'
repositories {
mavenCentral()
}
dependencies {
providedCompile 'javax.servlet:servlet-api:2.5'
runtime 'javax.servlet:jstl:1.1.2'
}
}
您可以看到该解决方案远非完美。 即使您定义了一个额外的属性来为每个子项目分配组和版本,您仍然会留下重复的代码,并且必须单独为每个子项目应用 Java 插件。 现在,只需让项目运行起来即可。 稍后您将改进该代码。
属性继承 项目中定义的属性会自动由其子项目继承,这一概念在其他构建工具(如 Maven)中也可用。 在清单 6.2 中,根项目中声明的额外属性 projectIds 可用于子项目模型、存储库和 Web。 |
从多项目构建的根目录中,您可以执行各个子项目的任务。 您需要做的就是命名串联的项目路径和任务名称。 请记住,路径由冒号字符 (:) 表示。 例如,执行子项目模型的任务构建可以通过在命令行引用完整路径来实现:
$ gradle :model:build
:model:compileJava
:model:processResources UP-TO-DATE
:model:classes
:model:jar
:model:assemble
:model:compileTestJava UP-TO-DATE
:model:processTestResources UP-TO-DATE
:model:testClasses UP-TO-DATE
:model:test
:model:check
:model:build
这对于自包含子项目模型非常有用,因为它不依赖于其他子项目的代码。 如果您对子项目存储库执行相同的任务,最终会出现编译错误。 这是为什么? 子项目存储库使用子项目模型中的代码。 为了正确运行,您需要声明项目的编译时依赖项。
声明项目依赖
声明对另一个项目的依赖关系看起来与声明对外部库的依赖关系非常相似。 在这两种情况下,都必须在依赖项配置块的闭包中声明依赖项。 项目依赖项必须分配给特定的配置 - 在您的情况下,是 Java 插件提供的配置编译。 以下清单概述了所有子项目的项目依赖关系声明。
// 模型子项目不声明任何外部或项目依赖项
project(':model') {
...
}
project(':repository') {
...
dependencies {
compile project(':model') // 声明对路径为 :model 的项目的编译时依赖
}
}
project(':web') {
...
dependencies {
// 声明对路径为 :repository 的项目的编译时依赖
compile project(':repository')
providedCompile 'javax.servlet:servlet-api:2.5'
runtime 'javax.servlet:jstl:1.1.2'
}
}
子项目存储库依赖于子项目模型,子项目 Web 依赖于同级项目存储库。 这就是对项目依赖关系进行建模的全部内容。 这样做具有三个重要含义:
-
项目依赖项的实际依赖项是它创建的库。 对于子项目模型,它是 JAR 文件。 这就是为什么项目依赖项也称为 lib 依赖项。
-
依赖于另一个项目也会将其传递依赖项添加到类路径中。 这意味着还添加了外部依赖项和其他项目依赖项。
-
在构建生命周期的初始化阶段,Gradle 确定项目的执行顺序。 依赖另一个子项目意味着必须先构建它。 毕竟,您依赖于它的库。
从根项目执行任务
通过初始化阶段后,Gradle 在内存中保存项目依赖项的内部模型。 它知道子项目 repository 依赖于 model,子项目 Web 依赖于 repository。 您不必从特定子项目执行任务 - 您可以为构建的所有项目执行一项任务。 假设您想从根项目执行任务构建。 鉴于 Gradle 知道子项目需要执行的顺序,您期望构建如图 6.9 所示。

您可以通过在项目的根级别执行任务来证明该假设:
$ gradle build
:model:compileJava
:model:processResources UP-TO-DATE
:model:classes
:model:jar
:model:assemble
:model:compileTestJava UP-TO-DATE
:model:processTestResources UP-TO-DATE
:model:testClasses UP-TO-DATE
:model:test
:model:check
:model:build
:repository:compileJava
:repository:processResources UP-TO-DATE
:repository:classes
:repository:jar
:repository:assemble
:repository:compileTestJava UP-TO-DATE
:repository:processTestResources UP-TO-DATE
:repository:testClasses UP-TO-DATE
:repository:test
:repository:check
:repository:build
:web:compileJava
:web:processResources UP-TO-DATE
:web:classes
:web:war
:web:assemble
:web:compileTestJava UP-TO-DATE
:web:processTestResources UP-TO-DATE
:web:testClasses UP-TO-DATE
:web:test
:web:check
:web:build
从根项目执行任务确实可以节省时间。 Gradle 执行所有子项目所需的任务,包括对增量构建的支持。 尽管这种行为很方便并确保您的类路径中始终拥有最新的类文件,但您可能需要更细粒度地控制何时构建所有依赖的子项目。
部分多项目构建
具有数十甚至数百个相关子项目的复杂多项目构建将显着影响平均执行时间。 Gradle 将检查所有项目依赖项并确保它们是最新的。 在开发过程中,您通常知道哪个子项目中的哪些源文件已更改。 从技术上讲,您不需要重建未更改的子项目。 对于这些情况,Gradle 提供了一个称为部分构建的功能。 部分构建通过命令行选项 –a 或 --no-rebuild 启用。 假设您只更改了子项目存储库中的代码,但不想重建子项目模型。 通过使用部分构建,您可以避免检查子项目模型的成本并缩短构建执行时间。 如果您正在开发一个具有数百个子项目依赖项的企业项目,那么您会感激在执行构建时可以节省的每一秒。 以下命令行输出显示了此选项的用法:
$ gradle :repository:build -a
:repository:compileJava
:repository:processResources UP-TO-DATE
:repository:classes
:repository:jar
:repository:assemble
:repository:compileTestJava UP-TO-DATE
:repository:processTestResources UP-TO-DATE
:repository:testClasses UP-TO-DATE
:repository:test
:repository:check
:repository:build
如果您只更改单个项目中的文件,则 --no-rebuild 选项非常有用。 作为日常开发实践的一部分,您需要从存储库中提取最新版本的源代码以集成团队成员所做的更改。 为了确保代码不会意外损坏,您需要重建并测试当前项目所依赖的项目。 常规构建任务仅编译依赖项目的代码,并组装 JAR 文件并使它们可用作项目依赖项。 要运行测试,请执行任务 buildNeeded,如以下命令行输出所示:
$ gradle :repository:buildNeeded
:model:compileJava
:model:processResources UP-TO-DATE
:model:classes
:model:jar
:model:assemble
:model:compileTestJava UP-TO-DATE
:model:processTestResources UP-TO-DATE
:model:testClasses UP-TO-DATE
:model:test UP-TO-DATE
:model:check UP-TO-DATE
:model:build
:model:buildNeeded
:repository:compileJava
:repository:processResources UP-TO-DATE
:repository:classes
:repository:jar
:repository:assemble
:repository:compileTestJava UP-TO-DATE
:repository:processTestResources UP-TO-DATE
:repository:testClasses UP-TO-DATE
:repository:test UP-TO-DATE
:repository:check UP-TO-DATE
:repository:build
:repository:buildNeeded
您对项目所做的任何更改都可能会对依赖于该项目的其他项目产生副作用。 借助任务 buildDependents,您可以通过构建和测试依赖项目来验证代码更改的影响。 以下命令行输出显示了其实际用途:
$ gradle :repository:buildDependents
:model:compileJava
:model:processResources UP-TO-DATE
:model:classes
:model:jar
:repository:compileJava
:repository:processResources UP-TO-DATE
:repository:classes
:repository:jar
:repository:assemble
:repository:compileTestJava UP-TO-DATE
:repository:processTestResources UP-TO-DATE
:repository:testClasses UP-TO-DATE
:repository:test UP-TO-DATE
:repository:check UP-TO-DATE
:repository:build
:web:compileJava
:web:processResources UP-TO-DATE
:web:classes
:web:war
:web:assemble
:web:compileTestJava UP-TO-DATE
:web:processTestResources UP-TO-DATE
:web:testClasses UP-TO-DATE
:web:test UP-TO-DATE
:web:check UP-TO-DATE
:web:build
:web:buildDependents
:repository:buildDependents
声明跨项目任务依赖关系
在上一节中,您看到从根项目执行特定任务会调用所有子项目中具有相同名称的所有任务,任务构建的执行顺序由声明的编译时项目依赖项决定。 如果您的项目不依赖于项目依赖项,或者为根项目和一个或多个子项目定义了一项具有相同名称的任务,则情况会有所不同。
默认任务执行顺序
假设您在根项目以及所有子项目中定义了一个名为 hello 的任务,如以下清单所示。 在每个 doLast 操作中,您都会在控制台上打印一条消息以指示您所在的项目。
// 为根项目和所有子项目声明一个同名的任务
task hello << {
println 'Hello from root project'
}
project(':model') {
task hello << {
println 'Hello from model project'
}
}
project(':repository') {
task hello << {
println 'Hello from repository project'
}
}
如果您从根项目运行任务 hello,您将看到以下输出:
$ gradle hello
:hello
Hello from root project
:model:hello
Hello from model project
:repository:hello
Hello from repository project
没有一个任务声明对另一个任务的依赖。 那么 Gradle 如何知道按什么顺序执行任务呢? 简单:多项目构建的根级别上的任务始终首先执行。 对于子项目,执行顺序仅由项目名称的字母数字顺序决定:模型位于存储库之前。 请记住,设置文件中子项目的声明顺序在执行顺序中不起任何作用。
控制任务执行顺序
您可以通过声明跨项目任务依赖关系来确定任务执行顺序。 为此,您需要从不同的项目引用任务的路径。 下一个清单演示了如何确保子项目存储库中的 hello 任务在子项目模型中的任务之前执行。
task hello << {
println 'Hello from root project'
}
project(':model') {
// 声明对子项目存储库中的任务的任务依赖性
task hello(dependsOn: ':repository:hello') << {
println 'Hello from model project'
}
}
project(':repository') {
task hello << {
println 'Hello from repository project'
}
}
如果您从根项目运行任务 hello,您会注意到依赖任务以正确的顺序执行:
$ gradle hello
:hello
Hello from root project
:repository:hello
Hello from repository project
:model:hello
Hello from model project
控制不同项目的任务之间的执行顺序不仅限于具有相同名称的任务。 如果您需要控制具有不同名称的任务的执行顺序,则适用相同的机制。 您所需要做的就是在声明任务依赖项时引用完整路径。
您具有基本的多项目构建运行能力,并对如何控制任务执行顺序有大致的了解。 接下来,我们将讨论定义常见行为的方法,以提高代码的可读性和可重用性。
定义常见行为
在清单 6.2 中,您需要将 Java 插件单独应用到每个子项目。 您还创建了一个名为 projectIds 的额外属性来定义组和版本。 您使用该额外属性将其值分配给根项目及其子项目的项目属性。 在这个相当小的项目中,这似乎不是一个大问题,但在具有超过 10 个子项目的大型项目中这样做可能会变得非常乏味。
在本部分中,您将使用 allprojects 和 subprojects 方法改进现有代码。 图 6.10 直观地展示了每种方法如何应用于多项目构建。

这对您的项目意味着什么? 您将需要使用 allprojects 方法来设置根项目和子项目的组和版本属性。 由于根项目没有定义任何 Java 代码,因此您不需要应用 Java 插件。 只有子项目是特定于 Java 的。 您可以使用 subprojects 方法将插件仅应用于子项目。 以下清单演示了多项目构建中 allprojects 和 subprojects 方法的用法。
allprojects { // 设置根项目和所有子项目的组和版本属性
group = 'com.manning.gia'
version = '0.1'
}
subprojects { // 仅将 Java 插件应用于子项目
apply plugin: 'java'
}
project(':repository') {
dependencies {
compile project(':model')
}
}
project(':web') {
apply plugin: 'war'
apply plugin: 'jetty'
repositories {
mavenCentral()
}
dependencies {
compile project(':repository')
providedCompile 'javax.servlet:servlet-api:2.5'
runtime 'javax.servlet:jstl:1.1.2'
}
}
执行此构建脚本将产生与先前构建相同的结果。 然而,很快就会发现,能够定义常见的项目行为有可能减少重复代码并提高构建的可读性。