对多项目构建的强大支持是Gradle的独特卖点之一. 这个话题也是最有智力挑战的.

gradle中的多项目构建包含一个根项目,以及一个或多个可能也有子项目的子项目.

Cross project configuration

虽然每个子项目都可以完全隔离其他子项目来配置自己,但是子项目具有共同的特征是很常见的. 因此,通常最好在项目之间共享配置,因此相同的配置会影响多个子项目.

让我们从一个非常简单的多项目构建开始. Gradle的核心是通用构建工具,因此项目不必是Java项目. 我们的第一个例子是关于海洋生物的.

Configuration and execution

构建阶段描述了每个Gradle构建的阶段. 让我们放大多项目构建的配置和执行阶段. 这里的配置意味着执行项目的build.gradle (或build.gradle.kts )文件,这意味着例如下载所有使用' apply plugin '或plugins块声明的plugins . 默认情况下,所有项目的配置都在执行任何任务之前进行. 这意味着当请求来自单个项目的单个任务时,将首先配置多项目构建的所有项目. 需要配置每个项目的原因是为了支持访问和更改Gradle项目模型的任何部分的灵活性.

Configuration on demand

配置注入功能和对完整项目模型的访问是可能的,因为每个项目都在执行阶段之前进行了配置. 但是,这种方法在大型的多项目构建中可能不是最有效的. 有Gradle构建,其中包含数百个子项目的层次结构. 庞大的多项目构建的配置时间可能会变得很明显. 可伸缩性是Gradle的一项重要要求. 因此,从版本1.4开始,引入了新的孵化"按需配置"模式.

按需配置模式尝试仅配置与所请求任务相关的项目,即,它仅执行参与构建的项目的build.gradle[.kts]文件. 这样,可以减少大型多项目构建的配置时间. 从长远来看,该模式将成为默认模式,可能是Gradle构建执行的唯一模式. 按需配置功能正在不断发展,因此不能保证每个构建都能正常工作. 对于已解耦项目的多项目构建,该功能应该可以很好地工作. 在"按需配置"模式下,项目配置如下:

  • 始终配置根项目. 这样,就可以支持典型的通用配置(所有项目或子项目脚本块).

  • 也只在执行Gradle而没有任何任务的情况下配置执行构建的目录中的项目. 这样,当按需配置项目时,默认任务将正确运行.

  • 支持标准项目依赖关系,并配置了相关项目. 如果项目A对项目B具有编译依赖性,则构建A会导致两个项目的配置.

  • 支持通过任务路径声明的任务依赖性,并导致配置相关项目. 示例: someTask.dependsOn(":someOtherProject:someOtherTask")

  • 通过任务路径从命令行(或Tooling API)请求的任务将导致配置相关项目. 例如,构建'projectA:projectB:someTask'导致配置projectB.

渴望尝试此新功能吗? 要在每次构建运行时按需配置,请参阅Gradle属性 . 要仅针对给定的构建按需配置,请参阅面向命令行性能的选项 .

Defining common behavior

让我们看下面的项目树的一些例子. 这是一个多项目构建,具有一个名为water的根项目和一个名为bluewhale的子项目.

例子1.多项目树-水和蓝鲸项目
项目布局
.
├── bluewhale/
├── build.gradle
└── settings.gradle
项目布局
.
├── bluewhale/
├── build.gradle.kts
└── settings.gradle.kts
该示例的代码可以在Gradle的'-all'发行版的samples/userguide/multiproject/firstExample/water中找到.
settings.gradle
rootProject.name = 'water'
include 'bluewhale'
settings.gradle.kts
rootProject.name = "water"
include("bluewhale")

bluewhale项目的构建脚本在bluewhale ? 在Gradle中,构建脚本是可选的. 显然,对于单个项目构建,没有构建脚本的项目没有多大意义. 对于多项目构建,情况有所不同. 让我们看一下water项目的构建脚本并执行它:

例子2.水(父)项目的构建脚本
build.gradle
Closure cl = { task -> println "I'm $task.project.name" }
task('hello').doLast(cl)
project(':bluewhale') {
    task('hello').doLast(cl)
}
build.gradle.kts
val cl = Action<Task> { println("I'm ${this.project.name}") }
tasks.register("hello") { doLast(cl) }
project(":bluewhale") {
    tasks.register("hello") { doLast(cl) }
}
gradle -q hello输出
> gradle -q hello
I'm water
I'm bluewhale

Gradle允许您从任何构建脚本访问多项目构建的任何项目. Project API提供了一个名为project()的方法,该方法将路径作为参数并返回此路径的Project对象. 通过任何我们称为跨项目配置的构建脚本来配置项目构建的功能. Gradle通过配置注入实现了这一点.

我们对water项目的构建脚本并不满意. 为每个项目显式添加任务很不方便. 我们可以做得更好. 首先,我们将一个名为krill项目添加到我们的多项目构建中.

例子3.多项目树-水,蓝鲸和磷虾项目
项目布局
.
├── bluewhale/
├── build.gradle
├── krill/
└── settings.gradle
项目布局
.
├── bluewhale/
├── build.gradle.kts
├── krill/
└── settings.gradle.kts
该示例的代码可以在Gradle的'-all'发行版的samples/userguide/multiproject/addKrill/water中找到.
settings.gradle
rootProject.name = 'water'

include 'bluewhale', 'krill'
settings.gradle.kts
rootProject.name = "water"

include("bluewhale", "krill")

现在,我们重写water构建脚本并将其简化为一行.

例子4.水利工程建设脚本
build.gradle
allprojects {
    task hello {
        doLast { task ->
            println "I'm $task.project.name"
        }
    }
}
build.gradle.kts
allprojects {
    tasks.register("hello") {
        doLast {
            println("I'm ${this.project.name}")
        }
    }
}
gradle -q hello输出
> gradle -q hello
I'm water
I'm bluewhale
I'm krill

这很酷还是很酷? 以及如何运作? Project API提供了allprojects属性,该属性返回一个列表,其中包含当前项目及其下的所有子项目. 如果使用闭包调用allprojects ,则闭包的语句将委派allprojects关联的项目. 您也可以通过allprojects.each (在Groovy中)或allprojects.forEach (在Kotlin中)进行迭代,但这会更加冗长.

其他构建系统使用继承作为定义常见行为的主要手段. 我们还将为项目提供继承,您将在后面看到. 但是Gradle使用配置注入作为定义常见行为的常用方法. 我们认为它提供了一种配置多项目构建的非常强大而灵活的方式.

共享配置的另一种可能性是使用通用的外部脚本.

Subproject configuration

Project API还提供了仅用于访问子项目的属性.

Defining common behavior

例子5.定义所有项目和子项目的共同行为
build.gradle
allprojects {
    task hello {
        doLast { task ->
            println "I'm $task.project.name"
        }
    }
}
subprojects {
    hello {
        doLast {
            println "- I depend on water"
        }
    }
}
build.gradle.kts
allprojects {
    tasks.register("hello") {
        doLast {
            println("I'm ${this.project.name}")
        }
    }
}
subprojects {
    tasks.named("hello") {
        doLast {
            println("- I depend on water")
        }
    }
}
gradle -q hello输出
> gradle -q hello
I'm water
I'm bluewhale
- I depend on water
I'm krill
- I depend on water

您可能会注意到,有两个代码段引用了" hello "任务. 第一个使用" task "关键字(在Groovy中)或task()函数(在Kotlin中)来构造任务并提供其基本配置. 第二部分不使用" task "关键字或函数,因为它进一步配置了现有的" hello "任务. 您只能在项目中构造一个任务,但是可以添加任意数量的代码块以提供其他配置.

Adding specific behavior

您可以在常见行为之上添加特定行为. 通常,我们将项目特定的行为放在要应用此特定行为的项目的构建脚本中. 但是,正如我们已经看到的那样,我们不必这样做. 我们可以为bluewhale项目添加项目特定的行为,如下所示:

例子6.为特定项目定义特定行为
build.gradle
allprojects {
    task hello {
        doLast { task ->
            println "I'm $task.project.name"
        }
    }
}
subprojects {
    hello {
        doLast {
            println "- I depend on water"
        }
    }
}
project(':bluewhale').hello {
    doLast {
        println "- I'm the largest animal that has ever lived on this planet."
    }
}
build.gradle.kts
allprojects {
    tasks.register("hello") {
        doLast {
            println("I'm ${this.project.name}")
        }
    }
}
subprojects {
    tasks.named("hello") {
        doLast {
            println("- I depend on water")
        }
    }
}
project(":bluewhale").tasks.named("hello") {
    doLast {
        println("- I'm the largest animal that has ever lived on this planet.")
    }
}
gradle -q hello输出
> gradle -q hello
I'm water
I'm bluewhale
- I depend on water
- I'm the largest animal that has ever lived on this planet.
I'm krill
- I depend on water

如前所述,我们通常更喜欢将项目特定的行为放入该项目的构建脚本中. 让我们进行重构,并向krill项目中添加一些项目特定的行为.

例子7.定义项目磷虾的特定行为
项目布局
.
├── bluewhale
│   └── build.gradle
├── build.gradle
├── krill
│   └── build.gradle
└── settings.gradle
项目布局
.
├── bluewhale
│   └── build.gradle.kts
├── build.gradle.kts
├── krill
│   └── build.gradle.kts
└── settings.gradle.kts
该示例的代码可以在Gradle的'-all'发行版的samples/userguide/multiproject/spreadSpecifics/water中找到.
settings.gradle
rootProject.name = 'water'
include 'bluewhale', 'krill'
bluewhale/build.gradle
hello.doLast {
  println "- I'm the largest animal that has ever lived on this planet."
}
krill/build.gradle
hello.doLast {
  println "- The weight of my species in summer is twice as heavy as all human beings."
}
build.gradle
allprojects {
    task hello {
        doLast { task ->
            println "I'm $task.project.name"
        }
    }
}
subprojects {
    hello {
        doLast {
            println "- I depend on water"
        }
    }
}
settings.gradle.kts
rootProject.name = "water"
include("bluewhale", "krill")
bluewhale/build.gradle.kts
tasks.named("hello") {
    doLast {
        println("- I'm the largest animal that has ever lived on this planet.")
    }
}
krill/build.gradle.kts
tasks.named("hello") {
    doLast {
        println("- The weight of my species in summer is twice as heavy as all human beings.")
    }
}
build.gradle.kts
allprojects {
    tasks.register("hello") {
        doLast {
            println("I'm ${this.project.name}")
        }
    }
}
subprojects {
    tasks.named("hello") {
        doLast {
            println("- I depend on water")
        }
    }
}
gradle -q hello输出
> gradle -q hello
I'm water
I'm bluewhale
- I depend on water
- I'm the largest animal that has ever lived on this planet.
I'm krill
- I depend on water
- The weight of my species in summer is twice as heavy as all human beings.

Project filtering

为了展示配置注入的更多功能,让我们添加另一个名为tropicalFish项目,并通过water项目的构建脚本向构建中添加更多行为.

Filtering by name

示例8.向某些项目添加自定义行为(按项目名称过滤)
项目布局
.
├── bluewhale/
│   └── build.gradle
├── build.gradle
├── krill/
│   └── build.gradle
├── settings.gradle
└── tropicalFish/
项目布局
.
├── bluewhale/
│   └── build.gradle.kts
├── build.gradle.kts
├── krill/
│   └── build.gradle.kts
├── settings.gradle.kts
└── tropicalFish/
该示例的代码可以在Gradle的" -all"分发版中的samples/userguide/multiproject/addTropical/water中找到.
settings.gradle
rootProject.name = 'water'
include 'bluewhale', 'krill', 'tropicalFish'
build.gradle
allprojects {
    task hello {
        doLast { task ->
            println "I'm $task.project.name"
        }
    }
}
subprojects {
    hello {
        doLast {
            println "- I depend on water"
        }
    }
}
configure(subprojects.findAll {it.name != 'tropicalFish'}) {
    hello {
        doLast {
            println '- I love to spend time in the arctic waters.'
        }
    }
}
settings.gradle.kts
rootProject.name = "water"
include("bluewhale", "krill", "tropicalFish")
build.gradle.kts
allprojects {
    tasks.register("hello") {
        doLast {
            println("I'm ${this.project.name}")
        }
    }
}
subprojects {
    tasks.named("hello") {
        doLast {
            println("- I depend on water")
        }
    }
}
configure(subprojects.filter { it.name != "tropicalFish" }) {
    tasks.named("hello") {
        doLast {
            println("- I love to spend time in the arctic waters.")
        }
    }
}
gradle -q hello输出
> gradle -q hello
I'm water
I'm bluewhale
- I depend on water
- I love to spend time in the arctic waters.
- I'm the largest animal that has ever lived on this planet.
I'm krill
- I depend on water
- I love to spend time in the arctic waters.
- The weight of my species in summer is twice as heavy as all human beings.
I'm tropicalFish
- I depend on water

configure()方法采用一个列表作为参数,并将配置应用于此列表中的项目.

Filtering by properties

使用项目名称进行过滤是一种选择. 使用额外的项目属性是另一种方法.

示例9.向某些项目添加自定义行为(按项目属性过滤)
项目布局
.
├── bluewhale
│   └── build.gradle
├── build.gradle
├── krill
│   └── build.gradle
├── settings.gradle
└── tropicalFish
    └── build.gradle
项目布局
.
├── bluewhale
│   └── build.gradle.kts
├── build.gradle.kts
├── krill
│   └── build.gradle.kts
├── settings.gradle.kts
└── tropicalFish
    └── build.gradle.kts
该示例的代码可以在Gradle的" -all"分布中的samples/userguide/multiproject/tropicalWithProperties/water中找到.
settings.gradle
rootProject.name = 'water'
include 'bluewhale', 'krill', 'tropicalFish'
bluewhale/build.gradle
ext.arctic = true
hello.doLast {
  println "- I'm the largest animal that has ever lived on this planet."
}
krill/build.gradle
ext.arctic = true
hello.doLast {
    println "- The weight of my species in summer is twice as heavy as all human beings."
}
build.gradle
allprojects {
    task hello {
        doLast { task ->
            println "I'm $task.project.name"
        }
    }
}
subprojects {
    hello {
        doLast {println "- I depend on water"}
    }

    afterEvaluate { Project project ->
        if (project.arctic) {
            hello.configure {
                doLast {
                    println '- I love to spend time in the arctic waters.'
                }
            }
        }
    }
}
tropicalFish/build.gradle
ext.arctic = false
settings.gradle.kts
rootProject.name = "water"
include("bluewhale", "krill", "tropicalFish")
bluewhale/build.gradle.kts
extra["arctic"] = true
tasks.named("hello") {
    doLast {
        println("- I'm the largest animal that has ever lived on this planet.")
    }
}
krill/build.gradle.kts
extra["arctic"] = true
tasks.named("hello") {
    doLast {
        println("- The weight of my species in summer is twice as heavy as all human beings.")
    }
}
build.gradle.kts
allprojects {
    tasks.register("hello") {
        doLast {
            println("I'm ${this.project.name}")
        }
    }
}
subprojects {
    val hello by tasks.existing

    hello {
        doLast { println("- I depend on water") }
    }

    afterEvaluate {
        if (extra["arctic"] as Boolean) {
            hello {
                doLast {
                    println("- I love to spend time in the arctic waters.")
                }
            }
        }
    }
}
tropicalFish/build.gradle.kts
extra["arctic"] = false
gradle -q hello输出
> gradle -q hello
I'm water
I'm bluewhale
- I depend on water
- I'm the largest animal that has ever lived on this planet.
- I love to spend time in the arctic waters.
I'm krill
- I depend on water
- The weight of my species in summer is twice as heavy as all human beings.
- I love to spend time in the arctic waters.
I'm tropicalFish
- I depend on water

water项目的构建文件中,我们使用afterEvaluate通知. 这意味着在评估子项目的构建脚本之后 ,将评估我们传递的闭包. 由于在这些构建脚本中设置了arctic属性,因此我们必须这样做. 您将在" 依赖关系-哪些依赖关系?"中找到有关此主题的更多信息.

Execution rules for multi-project builds

当我们从根项目目录执行hello任务时,事情以直观的方式表现出来. 执行了不同项目的所有hello任务. 让我们切换到bluewhale目录,看看如果我们从那里执行Gradle会发生什么.

从子项目运行构建
> gradle -q hello
I'm bluewhale
- I depend on water
- I'm the largest animal that has ever lived on this planet.
- I love to spend time in the arctic waters.

Gradle行为的基本规则很简单. Gradle从当前目录开始向下查找名称为hello任务的层次结构并执行它们. 需要注意的一件事很重要. Gradle 始终评估多项目构建中的每个项目并创建所有现有任务对象. 然后,根据任务名称参数和当前目录,Gradle过滤应执行的任务. 由于Gradle的跨项目配置,必须在执行任何任务之前对每个项目进行评估. 在下一部分中,我们将对此进行更详细的研究. 现在让我们来看最后一个海洋示例. 让我们添加一个任务bluewhalekrill .

例子10.项目的评估和执行
bluewhale/build.gradle
ext.arctic = true
hello {
    doLast {
        println "- I'm the largest animal that has ever lived on this planet."
    }
}

task distanceToIceberg {
    doLast {
        println '20 nautical miles'
    }
}
krill/build.gradle
ext.arctic = true
hello {
    doLast {
        println "- The weight of my species in summer is twice as heavy as all human beings."
    }
}

task distanceToIceberg {
    doLast {
        println '5 nautical miles'
    }
}
bluewhale/build.gradle.kts
extra["arctic"] = true
tasks.named("hello") {
    doLast {
        println("- I'm the largest animal that has ever lived on this planet.")
    }
}

tasks.register("distanceToIceberg") {
    doLast {
        println("20 nautical miles")
    }
}
krill/build.gradle.kts
extra["arctic"] = true
tasks.named("hello") {
    doLast {
        println("- The weight of my species in summer is twice as heavy as all human beings.")
    }
}

tasks.register("distanceToIceberg") {
    doLast {
        println("5 nautical miles")
    }
}
gradle -q distanceToIceberg输出
> gradle -q distanceToIceberg
20 nautical miles
5 nautical miles

这是不带-q选项的输出:

gradle distanceToIceberg输出
> gradle distanceToIceberg

> Task :bluewhale:distanceToIceberg
20 nautical miles

> Task :krill:distanceToIceberg
5 nautical miles

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

该构建从water项目中执行. watertropicalFish鱼都没有名为distanceToIceberg的任务. Gradle不在乎. 上面已经提到的简单规则是:在具有该名称的层次结构中执行所有任务. 只抱怨没有这样的任务!

Running tasks by their absolute path

As we have seen, you can run a multi-project build by entering any subproject dir and execute the build from there. All matching task names of the project hierarchy starting with the current dir are executed. But Gradle also offers to execute tasks by their absolute path (see also Project and task paths):

通过绝对路径运行任务
> gradle -q :hello :krill:hello hello
I'm water
I'm krill
- I depend on water
- The weight of my species in summer is twice as heavy as all human beings.
- I love to spend time in the arctic waters.
I'm tropicalFish
- I depend on water

该构建是从tropicalFish项目执行的. 我们执行waterkrilltropicalFish项目的hello任务. 前两个任务由其绝对路径指定,最后一个任务使用上述名称匹配机制执行.

Project and task paths

项目路径具有以下模式:它以一个可选的冒号开头,该冒号表示根项目. 根项目是路径中唯一没有由其名称指定的项目. 项目路径的其余部分是用冒号分隔的项目名称序列,其中下一个项目是上一个项目的子项目.

任务的路径只是其项目路径加上任务名称,例如" :bluewhale:hello ". 在项目内,您可以仅通过名称来解决同一项目的任务. 这被解释为相对路径.

Dependencies - Which dependencies?

上一节中的示例非常特殊,因为这些项目没有执行依赖项 . 他们只有配置依赖项 . 以下各节说明了这两种依赖关系之间的区别.

Execution dependencies

Dependencies and execution order

例子11.依赖关系和执行顺序
项目布局
.
├── build.gradle
├── consumer
│   └── build.gradle
├── producer
│   └── build.gradle
└── settings.gradle
Project layout
.
├── build.gradle.kts
├── consumer
│   └── build.gradle.kts
├── producer
│   └── build.gradle.kts
└── settings.gradle.kts
该示例的代码可以在Gradle的" -all"分发版中的samples/userguide/multiproject/dependencies/firstMessages/messages中找到.
build.gradle
ext.producerMessage = null
settings.gradle
include 'consumer', 'producer'
consumer/build.gradle
task action {
    doLast {
        println("Consuming message: ${rootProject.producerMessage}")
    }
}
producer/build.gradle
task action {
    doLast {
        println "Producing message:"
        rootProject.producerMessage = 'Watch the order of execution.'
    }
}
build.gradle.kts
extra["producerMessage"] = null
settings.gradle.kts
include("consumer", "producer")
consumer/build.gradle.kts
tasks.register("action") {
    doLast {
        println("Consuming message: ${rootProject.extra["producerMessage"]}")
    }
}
producer/build.gradle.kts
tasks.register("action") {
    doLast {
        println("Producing message:")
        rootProject.extra["producerMessage"] = "Watch the order of execution."
    }
}
gradle -q action输出
> gradle -q action
Consuming message: null
Producing message:

这并不能完全满足我们的要求. 如果没有其他定义,Gradle将按字母数字顺序执行任务. 因此,Gradle将在" :producer:action "之前执行" :consumer:action :producer:action ". 让我们尝试通过破解来解决此问题,并将生产者项目重命名为" aProducer ".

例子12.依赖关系和执行顺序
项目布局
.
├── aProducer
│   └── build.gradle
├── build.gradle
├── consumer
│   └── build.gradle
└── settings.gradle
项目布局
.
├── aProducer
│   └── build.gradle.kts
├── build.gradle.kts
├── consumer
│   └── build.gradle.kts
└── settings.gradle.kts
build.gradle
ext.producerMessage = null
settings.gradle
include 'consumer', 'aProducer'
consumer/build.gradle
task action {
    doLast {
        println("Consuming message: ${rootProject.producerMessage}")
    }
}
aProducer/build.gradle
task action {
    doLast {
        println "Producing message:"
        rootProject.producerMessage = 'Watch the order of execution.'
    }
}
build.gradle.kts
extra["producerMessage"] = null
settings.gradle.kts
include("consumer", "aProducer")
consumer/build.gradle.kts
tasks.register("action") {
    doLast {
        println("Consuming message: ${rootProject.extra["producerMessage"]}")
    }
}
aProducer/build.gradle.kts
tasks.register("action") {
    doLast {
        println("Producing message:")
        rootProject.extra["producerMessage"] = "Watch the order of execution."
    }
}
gradle -q action输出
> gradle -q action
Producing message:
Consuming message: Watch the order of execution.

如果我们现在切换到consumer目录并执行构建,我们可以显示该hack无效的地方.

consumer目录中gradle -q action输出
> gradle -q action
Consuming message: null

问题在于两个" action "任务是不相关的. 如果您从" messages "项目执行构建,则Gradle会执行它们,因为它们具有相同的名称并且在层次结构中. 在最后一个示例中,只有一个" action "任务位于层次结构中,因此这是唯一执行的任务. 我们需要比这个技巧更好的东西.

Real life examples

Gradle的多项目功能由现实生活中的用例驱动. 一个很好的示例由两个Web应用程序项目和一个父项目组成,该父项目创建一个包含两个Web应用程序的发行版. [ 1 ]在此示例中,我们仅使用一个构建脚本并进行跨项目配置 .

例子13.依赖关系-现实生活中的例子-跨项目配置
项目布局
.
├── build.gradle
├── date
│   └── src
│       └── main
│           ├── java
│           │   └── org
│           │       └── gradle
│           │           └── sample
│           │               └── DateServlet.java
│           └── webapp
│               └── web.xml
├── hello
│   └── src
│       └── main
│           ├── java
│           │   └── org
│           │       └── gradle
│           │           └── sample
│           │               └── HelloServlet.java
│           └── webapp
│               └── web.xml
└── settings.gradle
项目布局
.
├── build.gradle.kts
├── date
│   └── src
│       └── main
│           ├── java
│           │   └── org
│           │       └── gradle
│           │           └── sample
│           │               └── DateServlet.java
│           └── webapp
│               └── web.xml
├── hello
│   └── src
│       └── main
│           ├── java
│           │   └── org
│           │       └── gradle
│           │           └── sample
│           │               └── HelloServlet.java
│           └── webapp
│               └── web.xml
└── settings.gradle.kts
该示例的代码可以在Gradle的" -all"分发samples/userguide/multiproject/dependencies/webDist中的samples/userguide/multiproject/dependencies/webDist中找到.
settings.gradle
rootProject.name = 'webDist'
include 'date', 'hello'
build.gradle
allprojects {
    apply plugin: 'java'
    group = 'org.gradle.sample'
    version = '1.0'
}

subprojects {
    apply plugin: 'war'
    repositories {
        mavenCentral()
    }
    dependencies {
        implementation "javax.servlet:servlet-api:2.5"
    }
}

task explodedDist(type: Copy) {
    into "$buildDir/explodedDist"
    subprojects {
        from tasks.withType(War)
    }
}
settings.gradle.kts
rootProject.name = "webDist"
include("date", "hello")
build.gradle.kts
allprojects {
    apply(plugin = "java")
    group = "org.gradle.sample"
    version = "1.0"
}

subprojects {
    apply(plugin = "war")
    repositories {
        mavenCentral()
    }
    dependencies {
        "providedCompile"("javax.servlet:servlet-api:2.5")
    }
}

tasks.register<Copy>("explodedDist") {
    into("$buildDir/explodedDist")
    subprojects {
        from(tasks.withType<War>())
    }
}

我们有一组有趣的依赖项. 显然, datehello项目对webDist具有配置依赖性,因为webapp项目的所有构建逻辑都是由webDist注入的. 由于webDist依赖于datehello的构建工件,因此执行依赖关系是另一个方向. 甚至还有第三种依赖性. webDistdatehello具有配置依赖性,因为它需要知道archivePath . 但是它在执行时要求提供此信息. 因此,我们没有循环依赖.

Such dependency patterns are daily bread in the problem space of multi-project builds. If a build system does not support these patterns, you either can’t solve your problem or you need to do ugly hacks which are hard to maintain and massively impair your productivity as a build master.

Project lib dependencies

如果一个项目在编译路径中需要另一个项目生成的jar,而不仅是jar,还需要该jar的可传递依赖项,该怎么办? 显然,这是Java多项目构建的非常常见的用例. 如项目依赖项中所述 ,Gradle为此提供了项目库依赖项.

例子14.项目库依赖
项目布局
.
├── api
│   └── src
│       ├── main
│       │   └── java
│       │       └── org
│       │           └── gradle
│       │               └── sample
│       │                   ├── api
│       │                   │   └── Person.java
│       │                   └── apiImpl
│       │                       └── PersonImpl.java
│       └── test
│           └── java
│               └── org
│                   └── gradle
│                       └── PersonTest.java
├── build.gradle
├── services
│   └── personService
│       └── src
│           ├── main
│           │   └── java
│           │       └── org
│           │           └── gradle
│           │               └── sample
│           │                   └── services
│           │                       └── PersonService.java
│           └── test
│               └── java
│                   └── org
│                       └── gradle
│                           └── sample
│                               └── services
│                                   └── PersonServiceTest.java
├── settings.gradle
└── shared
    └── src
        └── main
            └── java
                └── org
                    └── gradle
                        └── sample
                            └── shared
                                └── Helper.java
项目布局
.
├── api
│   └── src
│       ├── main
│       │   └── java
│       │       └── org
│       │           └── gradle
│       │               └── sample
│       │                   ├── api
│       │                   │   └── Person.java
│       │                   └── apiImpl
│       │                       └── PersonImpl.java
│       └── test
│           └── java
│               └── org
│                   └── gradle
│                       └── PersonTest.java
├── build.gradle.kts
├── services
│   └── personService
│       └── src
│           ├── main
│           │   └── java
│           │       └── org
│           │           └── gradle
│           │               └── sample
│           │                   └── services
│           │                       └── PersonService.java
│           └── test
│               └── java
│                   └── org
│                       └── gradle
│                           └── sample
│                               └── services
│                                   └── PersonServiceTest.java
├── settings.gradle.kts
└── shared
    └── src
        └── main
            └── java
                └── org
                    └── gradle
                        └── sample
                            └── shared
                                └── Helper.java
该示例的代码可以在Gradle的'-all'发行版的samples/userguide/multiproject/dependencies/java中找到.

我们有" shared "," api "和" personService "项目. " personService "项目对其他两个项目具有lib依赖关系. " api "项目对" shared "项目具有lib依赖性. " services "也是一个项目,但是我们只是将其用作容器. 它没有构建脚本,并且没有任何内容由另一个构建脚本注入. 我们使用:分隔符来定义项目路径. 有关定义项目路径的更多信息,请查阅Settings.include(java.lang.String [])的DSL文档.

settings.gradle
include 'api', 'shared', 'services:personService'
build.gradle
/*
 * Copyright 2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

subprojects {
    apply plugin: 'java'
    group = 'org.gradle.sample'
    version = '1.0'
    repositories {
        mavenCentral()
    }
    dependencies {
        testImplementation "junit:junit:4.12"
    }
}

project(':api') {
    dependencies {
        implementation project(':shared')
    }
}

project(':services:personService') {
    dependencies {
        implementation project(':shared'), project(':api')
    }
}
settings.gradle.kts
include("api", "shared", "services:personService")
build.gradle.kts
subprojects {
    apply(plugin = "java")
    group = "org.gradle.sample"
    version = "1.0"
    repositories {
        mavenCentral()
    }
    dependencies {
        "testImplementation"("junit:junit:4.12")
    }
}

project(":api") {
    dependencies {
        "implementation"(project(":shared"))
    }
}

project(":services:personService") {
    dependencies {
        "implementation"(project(":shared"))
        "implementation"(project(":api"))
    }
}

所有构建逻辑都在根项目的构建脚本中. [ 2 ] " lib "依赖项是执行依赖项的一种特殊形式. 它导致首先构建另一个项目,并将具有另一个项目的类的jar添加到类路径. 还将另一个项目的依赖项添加到类路径中. 因此,您可以进入" api "目录并触发" gradle compile ". 首先构建" shared "项目,然后构建" api "项目. 项目依赖性使部分多项目生成成为可能.

If you come from Maven land you might be perfectly happy with this. If you come from Ivy land, you might expect some more fine grained control. Gradle offers this to you:

例子15.对依赖的细粒度控制
build.gradle
subprojects {
    apply plugin: 'java-library'
    group = 'org.gradle.sample'
    version = '1.0'
}

project(':api') {
    configurations {
        spi
    }
    dependencies {
        implementation project(':shared')
    }
    task spiJar(type: Jar) {
        archiveBaseName = 'api-spi'
        from sourceSets.main.output
        include('org/gradle/sample/api/**')
    }
    artifacts {
        spi spiJar
    }
}

project(':services:personService') {
    dependencies {
        implementation project(':shared')
        implementation project(path: ':api', configuration: 'spi')
        testImplementation "junit:junit:4.12", project(':api')
    }
}
build.gradle.kts
subprojects {
    apply(plugin = "java")
    group = "org.gradle.sample"
    version = "1.0"
}

project(":api") {
    configurations {
        create("spi")
    }
    dependencies {
        "implementation"(project(":shared"))
    }
    tasks.register<Jar>("spiJar") {
        archiveBaseName.set("api-spi")
        from(project.the<SourceSetContainer>()["main"].output)
        include("org/gradle/sample/api/**")
    }
    artifacts {
        add("spi", tasks["spiJar"])
    }
}

project(":services:personService") {
    dependencies {
        "implementation"(project(":shared"))
        "implementation"(project(path = ":api", configuration = "spi"))
        "testImplementation"("junit:junit:4.12")
        "testImplementation"(project(":api"))
    }
}

Java插件默认情况下会向您的项目库中添加一个jar,其中包含所有类. 在此示例中,我们创建了一个仅包含" api "项目的接口的附加库. 我们将此库分配给新的依赖项配置 . 对于人服务,我们声明,该项目应只针对"编译api "接口,而是来自"与所有类测试api ".

Depending on the task output produced by another project

项目依赖关系模型模块之间的依赖关系. 实际上,您说的是您依赖于另一个项目的主要输出. 在基于Java的项目中,通常是一个JAR文件.

Sometimes you may want to depend on an output produced by another task. In turn you’ll want to make sure that the task is executed beforehand to produce that very output. Declaring a task dependency from one project to another is a poor way to model this kind of relationship and introduces unnecessary coupling. The recommended way to model such a dependency is to produce the output, mark it as an "outgoing" artifact or add it to the output of the main source set which you can depend on in the consuming project.

假设您正在使用两个子项目producerconsumer进行多项目构建. 子项目producer定义了一个名为buildInfo的任务,该任务生成包含构建信息(例如项目版本)的属性文件. builtBy属性负责建立推断的任务依赖性. 有关更多信息builtBy ,见SourceSetOutput .

示例16.生成包含构建信息的属性文件的任务
build.gradle
task buildInfo(type: BuildInfo) {
    version = project.version
    outputFile = file("$buildDir/generated-resources/build-info.properties")
}

sourceSets {
    main {
        output.dir(buildInfo.outputFile.parentFile, builtBy: buildInfo)
    }
}
build.gradle.kts
val buildInfo by tasks.registering(BuildInfo::class) {
    version = project.version.toString()
    outputFile = file("$buildDir/generated-resources/build-info.properties")
}

sourceSets {
    main {
        output.dir(buildInfo.get().outputFile.parentFile, "builtBy" to buildInfo)
    }
}

消费项目应该能够在运行时读取属性文件. 在生产项目上声明项目依赖关系需要事先创建属性并将其提供给运行时类路径.

示例17.声明对生成属性文件的项目的项目依赖
build.gradle
dependencies {
    runtimeOnly project(':producer')
}
build.gradle.kts
dependencies {
    runtimeOnly(project(":producer"))
}

在上面的示例中,使用者现在声明了对producer项目输出的依赖.

Parallel project execution

随着开发人员桌面和CI服务器上越来越多的CPU内核可用,Gradle能够充分利用这些处理资源非常重要. 更具体地说,并行执行尝试:

  • 减少执行受IO约束或不消耗所有可用CPU资源的多项目构建的总构建时间.

  • 为小型项目的执行提供更快的反馈,而无需等待其他项目的完成.

尽管Gradle已经通过Test.setMaxParallelForks(int)提供了并行测试执行,但本节中描述的功能是在项目级别上的并行执行.

并行项目执行允许并行执行已解耦的多项目构建中的各个项目(另请参见解耦项目 ). 尽管并行执行并不严格要求在配置时进行解耦,但长期目标是提供一套功能强大的功能,这些功能可用于完全解耦的项目. 这些功能包括:

  • Configuration on-demand.

  • 并行配置项目.

  • 对未更改的项目重复使用配置.

  • 项目级别的最新检查.

  • 使用预先构建的工件代替构建依赖项目.

并行执行如何工作? 首先,您需要告诉Gradle使用并行模式. 您可以使用--parallel命令行参数或配置构建环境( Gradle属性 ). 除非您提供特定数量的并行线程,否则Gradle会尝试根据可用的CPU内核选择正确的数量. 每个并行工作者在执行任务时都专有拥有一个给定的项目. 完全支持任务依赖关系,并行工作者将首先开始执行上游任务. 请记住,在并行模式下不能保证解耦任务的字母顺序(在顺序执行期间可以看到). 换句话说,在并行模式下,任务将在它们的依赖关系完成后立即运行 ,并且可以使用任务工作程序来运行它们 ,这可能早于在顺序构建期间开始的任务. 您应确保正确声明了任务相关性和任务输入/输出,以避免出现排序问题.

Decoupled Projects

Gradle允许任何项目在配置和执行阶段都可以访问任何其他项目. 尽管这为构建作者提供了强大的功能和灵活性,但同时也限制了Gradle在构建这些项目时所具有的灵活性. 例如,这有效地防止了Gradle正确地并行构建多个项目,仅配置项目的子集或替代预先构建的工件来代替项目依赖项.

如果两个项目不直接访问彼此的项目模型,则据说它们是分离的. 解耦的项目只能根据已声明的依赖项进行交互: 项目依赖项和/或任务依赖项 . 项目交互的任何其他形式(即,通过修改另一个项目对象或通过从另一个项目对象读取值)都会导致项目耦合. 在配置阶段进行耦合的结果是,如果使用"按需配置"选项调用gradle,则构建结果可能会以多种方式出现缺陷. 在执行阶段进行耦合的结果是,如果使用parallel选项调用gradle,则一个项目任务会运行得太晚而无法影响并行构建项目的任务. Gradle不会尝试检测耦合并警告用户,因为引入耦合的可能性太多.

耦合项目的一种非常常见的方法是使用配置注入 . 这可能不会立即显现出来,但是使用关键的Gradle功能(例如allprojectssubprojects关键字)会自动使您的项目耦合. 这是因为这些关键字在定义项目的build.gradle文件中使用. 通常,这是一个"根项目",除了定义通用配置外,别无所求,但是就Gradle而言,该根项目仍然是一个成熟的项目,通过使用allprojects ,该项目可以有效地耦合到所有其他项目. 根项目与子项目的耦合不会影响"按需配置",但是在任何子项目的build.gradle文件中使用allprojectssubprojects将产生影响.

这意味着使用任何形式的共享构建脚本逻辑或配置注入( allprojectssubprojects等)将导致您的项目耦合. 在扩展项目去耦概念的同时,提供利用解耦项目的功能的同时,我们还将引入新功能,以帮助您解决常见的用例(例如配置注入),而不会导致您的项目被耦合.

为了充分利用跨项目配置,而又不会遇到并行和"按需配置"选项的问题,请遵循以下建议:

  • 避免子项目的构建脚本引用其他子项目; 首选从根项目进行交叉配置.

  • 避免在执行时更改其他项目的配置.

Multi-Project Building and Testing

Java插件的build任务通常用于编译,测试和执行单个项目的代码样式检查(如果使用了CodeQuality插件). 在多项目构建中,您可能经常需要在多个项目中执行所有这些任务. buildNeededbuildDependents任务可以对此提供帮助.

此示例中 ," :services:personservice "项目同时取决于" :api "和" :shared "项目. " :api "项目还取决于" :shared "项目.

假设您正在处理一个项目,即" :api "项目. 自执行清理以来,您一直在进行更改,但尚未构建整个项目. 您想构建任何必要的支持jar,但仅对已更改的项目执行代码质量和单元测试. build任务将执行此操作.

例子18.构建和测试单个项目
gradle :api:build输出
> gradle :api:build
> Task :shared:compileJava
> Task :shared:processResources
> Task :shared:classes
> Task :shared:jar
> Task :api:compileJava
> Task :api:processResources
> Task :api:classes
> Task :api:jar
> Task :api:assemble
> Task :api:compileTestJava
> Task :api:processTestResources
> Task :api:testClasses
> Task :api:test
> Task :api:check
> Task :api:build

BUILD SUCCESSFUL in 0s
9 actionable tasks: 9 executed

如果您刚从版本控制系统中获得了最新版本的源代码,其中包括" :api "所依赖的其他项目中的更改,则您可能不仅要构建所依赖的所有项目,还要对其进行测试. buildNeeded任务还从testRuntime配置的项目库依赖项测试所有项目.

例19.依赖于项目的构建和测试
gradle :api:buildNeeded输出
> gradle :api:buildNeeded
> Task :shared:compileJava
> Task :shared:processResources
> Task :shared:classes
> Task :shared:jar
> Task :api:compileJava
> Task :api:processResources
> Task :api:classes
> Task :api:jar
> Task :api:assemble
> Task :api:compileTestJava
> Task :api:processTestResources
> Task :api:testClasses
> Task :api:test
> Task :api:check
> Task :api:build
> Task :shared:assemble
> Task :shared:compileTestJava
> Task :shared:processTestResources
> Task :shared:testClasses
> Task :shared:test
> Task :shared:check
> Task :shared:build
> Task :shared:buildNeeded
> Task :api:buildNeeded

BUILD SUCCESSFUL in 0s
12 actionable tasks: 12 executed

您可能还想重构其他项目中使用的" :api "项目的某些部分. 如果进行这些类型的更改,仅测试" :api "项目是不够的,还需要测试所有依赖" :api "项目的项目. buildDependents任务还将测试在指定项目上具有项目lib依赖项(在testRuntime配置中)的所有项目.

例子20.建立和测试依赖项目
gradle :api:buildDependents输出
> gradle :api:buildDependents
> Task :shared:compileJava
> Task :shared:processResources
> Task :shared:classes
> Task :shared:jar
> Task :api:compileJava
> Task :api:processResources
> Task :api:classes
> Task :api:jar
> Task :api:assemble
> Task :api:compileTestJava
> Task :api:processTestResources
> Task :api:testClasses
> Task :api:test
> Task :api:check
> Task :api:build
> Task :services:personService:compileJava
> Task :services:personService:processResources
> Task :services:personService:classes
> Task :services:personService:jar
> Task :services:personService:assemble
> Task :services:personService:compileTestJava
> Task :services:personService:processTestResources
> Task :services:personService:testClasses
> Task :services:personService:test
> Task :services:personService:check
> Task :services:personService:build
> Task :services:personService:buildDependents
> Task :api:buildDependents

BUILD SUCCESSFUL in 0s
17 actionable tasks: 17 executed

最后,您可能要构建和测试所有项目中的所有内容. 您在根项目文件夹中运行的任何任务都将使相同的命名任务在所有子项上运行. 因此,您可以运行" gradle build "来构建和测试所有项目.

Multi Project and buildSrc

使用buildSrc来组织构建逻辑告诉我们,我们可以将要编译和测试的构建逻辑放在特殊的buildSrc目录中. 在多项目构建中,只能有一个buildSrc目录,该目录必须位于根目录中.


1 . 我们拥有的实际用例是使用http://lucene.apache.org/solr ,在这里您需要为要访问的每个索引单独进行战争. 这就是为什么我们创建了Webapp发行版的原因之一. Resin servlet容器使我们可以将这种分发指向servlet容器的基本安装.
2 . 我们在这里执行此操作,因为它使布局更加容易. 我们通常将项目特定的内容放入各个项目的构建脚本中.