多项目构建有助于模块化。它允许一个人在一个更大的项目中专注于一个领域的工作,而Gradle负责项目其他部分的依赖。
您将构建什么
您将构建一个包含文档的新的应用程序。在这个过程中,您将创建一个基于groovy的库项目、一个基于Asciidoctor的文档项目和一个Java分布式命令行应用程序。您将看到如何将这些项目连接在一起以创建最终产品。
您需要准备什么
1. 大概花费25分钟时间;
2. 一个text文档编辑器;
3. 一个命令行工具;
4. JDK 1.8或以上版本;
5. 一个Gradle分发包(安装包),版本需要5.0及以上;
创建一个根项目
第一步需要您去创建一个文件夹作为新的项目,并且将Gradle Wrapper添加到这个项目,如果您使用Build Init 插件,您还需要添加设置或构建脚本。
$ mkdir creating-multi-project-builds
$ cd creating-multi-project-builds
$ gradle init
如果您更喜欢使用Kotlin DSL,您也可以选择kotlin去编写构建脚本DSL。
init首先会运行wrapper任务来生成gradlew和 gradle.bat的wrapper脚本文件。
打开这个 settings脚本文件,您将看到一些自动生成的注释内容,这些注释内容您可以删除只留下以下内容:
settings.gradle
rootProject.name = 'creating-multi-project-builds'
settings.gradle.kts
rootProject.name = "creating-multi-project-builds"
非常棒,您现在可以开始进行开发了。
配置上面生成的文件
在一个多项目中,您可以使用顶层构建脚本(也称为根项目)来配置尽可能多的公共特性,让子项目只定制子项目所需的内容。
当使用没有参数的 init任务时, Gradle在注释块中生成一个基本的 Java布局的构建脚本文件。打开 build.gradle文件,将内容替换为:
build.gradle
allprojects {
repositories {
jcenter()
}
}
build.gradle.kts
allprojects {
repositories {
jcenter()
}
}
以上脚本的含义是将Jcenter仓库添加到所有项目。
allprojects块用于添加将应用于所有子项目和根项目的配置项。以类似的方式,子项目块只能用于为所有子项目添加配置项。您可以在根项目中多次使用这两个块。
现在,通过顶级构建脚本中的子项目块,为将要添加的每个模块设置版本,如下所示:
build.gradle
subprojects {
version = '1.0'
}
build.gradle.kts
subprojects {
version = "1.0"
}
添加一个Groovy库子项目
为您的库子项目创建目录:
$ mkdir greeting-library
在 greeting-library文件下创建构建脚本,并且添加基于 Groovy库项目的内容(如果您以前从未构建过 Groovy库,也不要担心。完整的内容将在这里提供。如果您对细节感兴趣,您可能想要查看用户手册中 构建Groovy库的入门指南),如下所示:
greeting-library/build.gradle
plugins {
id 'groovy'
}
dependencies {
compile 'org.codehaus.groovy:groovy:2.4.10'
testCompile 'org.spockframework:spock-core:1.0-groovy-2.4', {
exclude module: 'groovy-all'
}
}
greeting-library/build.gradle.kts
plugins {
groovy
}
dependencies {
compile("org.codehaus.groovy:groovy:2.4.10")
testCompile("org.spockframework:spock-core:1.0-groovy-2.4") {
exclude(module = "groovy-all")
}
}
现在编辑顶级项目中的settings脚本把新建的Groovy库子项目添加进去作为多项目构建的一部分。
settings.gradle.kts
include("greeting-library")
settings.gradle.kts
include("greeting-library")
最后,在greeting-library文件下创建src/main/groovy文件夹,并添加greeter包。
$ cd greeting-library
$ mkdir -p src/main/groovy/greeter
$ mkdir -p src/test/groovy/greeter
在src/main/groovy文件夹下的greeter包中添加一个 GreetingFormatter的类:
greeting-library/src/main/groovy/greeter/GreetingFormatter.groovy
package greeter
import groovy.transform.CompileStatic
@CompileStatic
class GreetingFormatter {
static String greeting(final String name) {
"Hello, ${name.capitalize()}"
}
}
在src/test/groovy文件夹的greeter包下添加一个基于Spock框架的测试类GreetingFormatterSpec
greeting-library/src/test/groovy/greeter/GreetingFormatterSpec.groovy
package greeter
import spock.lang.Specification
class GreetingFormatterSpec extends Specification {
def 'Creating a greeting'() {
expect: 'The greeting to be correctly capitalized'
GreetingFormatter.greeting('gradlephant') == 'Hello, Gradlephant'
}
}
在顶级项目下运行 ./gradlew build 命令
$ cd ..
$ ./gradlew build
Gradle会自动检测到在greeting-library中有一个构建任务,并执行它。这是Gradle多项目构建的强大特性之一。当子项目中的任务与顶级项目中的任务具有相同的名称时,构建的维护将更容易,Gradle可以通过在顶级指定公共任务名称来在每个项目中执行相同的任务。
然而,单个子项目并不能真正构建一个多项目。因此,下一步是添加一个将使用这个库的子项目。
添加一个Java 应用子项目
在根项目目录下创建以下子项目:
$ mkdir greeter
greeter/build.gradle.kts
plugins {
java ①
application ②
}
greeter/build.gradle.kts
plugins {
java ①
application ②
}
① 这是一个java项目,因此需要使用Java plugin;
② 添加一个Application plugin 去编译Java 应用;
Application插件允许您将所有应用程序的 jar及其所有传递依赖项打包到一个 ZIP或 TAR文件中。它还将向归档文件添加两个启动脚本 (一个用于类 unix操作系统,另一个用于 Windows),以方便用户运行应用程序。
再一次,我们需要修改顶级项目中的settings脚本文件去添加这个新的项目。
settings.gradle.kts
include("greeter")
settings.gradle.kts
include("greeter")
现在我们来创建一个包含main函数的Java类Greeter,这个类需要位于greeting-library子项目greeter中。
$ mkdir -p greeter/src/main/java/greeter
greeter/src/main/java/greeter/Greeter.java
package greeter;
public class Greeter {
public static void main(String[] args) {
final String output = GreetingFormatter.greeting(args[0]);
System.out.println(output);
}
}
由于目标是一个Java应用程序,您还需要告诉Gradle类的名称,它是入口点。编辑greeter子项目的构建脚本,并将mainClassName属性分配给包含main方法的Java类。
greeter/build.gradle.kts
application {
mainClassName = "greeter.Greeter" ①
}
greeter/build.gradle.kts
application {
mainClassName = "greeter.Greeter" ①
}
① 将mainClassName设置为入口点(被设置为入口的类必须拥有一个标准的main函数)。
运行构建(这时会失败,是因为我们还没有解决依赖问题)
$ ./gradlew build
这是因为greeter项目不知道在哪里可以找到greeting-library。创建子项目的集合并不会自动地使它们各自的工件对其他子项目自动可用——这将导致项目的脆弱性。Gradle有一个特定的语法来将一个子项目的工件链接到另一个子项目的依赖项上。再次编辑greeter子项目的build脚本,并添加:
greeter/build.gradle
dependencies {
compile project(':greeting-library') ①
}
greeter/build.gradle.kts
dependencies {
compile(project(":greeting-library")) ①
}
① 使用 project(NAME) 的语法结构将一个子项目的构件添加到另一个子项目的依赖项中。
再次运行构建,将会构架成功。
$ ./gradlew build
注意每个子项目是如何在输出中添加前缀的,这样您就知道从哪个项目执行哪个任务。还需要注意的是,Gradle并不会在转移到另一个子项目之前处理所有的任务。
添加一个测试,以确保应用程序本身中的代码能够工作。由于Spock框架也是测试Java代码的一种流行方法,所以创建测试时,首先要将Groovy插件添加到greeter子项目的构建脚本中。这需要groovy插件,其中包括java插件,因此可以将java替换为groovy,如下所示:
greeter/build.gradle
plugins {
id 'groovy' ①
}
// then, add the following testCompile dependency to the dependencies block:
dependencies {
testCompile 'org.spockframework:spock-core:1.0-groovy-2.4', {
exclude module: 'groovy-all'
}
}
greeter/build.gradle.kts
plugins {
groovy ①
}
// then, add the following testCompile dependency to the dependencies block:
dependencies {
testCompile("org.spockframework:spock-core:1.0-groovy-2.4") {
exclude(module = "groovy-all")
}
}
① 让groovy插件自动应用java插件,这样实际上就可以删除应用java插件的行。但是对于语义来说,最好同时保留这两个,因为它表明您主要是在构建一个Java项目。
然后将greeter包中名为GreeterSpec的测试添加到src/test/groovy/greeter目录中的子项目(如果该目录不存在,则必须创建该目录)。
greeter/src/test/groovy/greeter/GreeterSpec.groovy
package greeter
import spock.lang.Specification
class GreeterSpec extends Specification {
def 'Calling the entry point'() {
setup: 'Re-route standard out'
def buf = new ByteArrayOutputStream(1024)
System.out = new PrintStream(buf)
when: 'The entrypoint is executed'
Greeter.main('gradlephant')
then: 'The correct greeting is output'
buf.toString() == "Hello, Gradlephant\n".denormalize()
}
}
不需要重新运行完整的构建,只需在greeter子项目中运行测试即可。Gradle包装器脚本gradlew只存在于顶层,所以请先将目录改为顶层,然后执行以下命令:
$ ./gradlew :greeter:test
通过使用格式:subproject: task作为任务名,可以从顶部(或任何其他子项目)运行任何子项目中的任何任务。这样就不需要做以下选择(也允许这样操作):
$ cd greeter $ ../gradlew test ①
$ cd ..
① 执行的目录的父级目录必须包含生成的Gradle Wrapper脚本
如果您要在一个子项目中花费大量时间,那么切换到那个目录并从那里运行构建是正常的。但是,如果您需要在特定的子项目中快速运行任务,通过子项目路径指定任务的灵活性是非常有用的。
为项目添加文档
为软件项目创建文档被认为是很好的实践。尽管有许多创作方法可以实现这个目标,但是您将使用非常流行的Asciidoctor工具。
首先将Asciidoctor插件添加到根项目构建脚本顶部的plugins块中。
build.gradle
plugins {
id 'org.asciidoctor.convert' version '1.5.6' apply false ①
}
build.gradle.kts
plugins {
id("org.asciidoctor.convert") version "1.5.6" apply false ①
}
① 使用apply false会将插件添加到整个项目中,但不会将其添加到根项目中。
现在让我们来为项目文档创建另一个子项目,名叫:docs
$ mkdir docs
在docs目录下创建一个构建脚本,内容如下:
docs/build.gradle
plugins {
id 'org.asciidoctor.convert' ①
}
asciidoctor {
sources {
include 'greeter.adoc' ②
}
}
build.dependsOn 'asciidoctor' ③
docs/build.gradle.kts
import org.asciidoctor.gradle.AsciidoctorTask
plugins {
id("org.asciidoctor.convert") ①
}
tasks.asciidoctor {
sources(delegateClosureOf<PatternSet> {
include("greeter.adoc") ②
})
}
tasks.build { dependsOn(tasks.asciidoctor) } ③
① 将Asciidoctor插件应用到这个子项目。该技术允许您在根项目中定义所有插件的同时,有选择地将插件应用到子项目中。
② 告诉插件寻找一个名为greeter的文档。默认源文件夹src/docs/asciidoc中的adoc
③ 将asciidoctor任务添加到构建生命周期中,以便如果为顶级项目执行构建,那么也将构建文档。
将文档子项目添加到settings.gradle中:
settings.gradle
include 'docs'
settings.gradle.kts
include("docs")
在docs子项目的Asciidoctor源文件夹src/docs/asciidoc中添加一个名为greeter.adoc的文档。如果该目录不存在,则需要创建该目录。
docs/src/docs/asciidoc/greeter.adoc
= Greeter Command-line Application
A simple application demonstrating the flexibility of a Gradle multi-project.
== Installation
Unpack the ZIP or TAR file in a suitable location
== Usage
[listing]
----
$ cd greeter-1.0
$ ./bin/greeter gradlephant
Hello, Gradlephant
----
在顶级项目中运行asciidoctor任务
$ ./gradlew asciidoctor
文档工件将出现在docs/build/asciidoc/html5中。您可以随意打开greeter.html文件并检查输出。
将文档包含在分发归档中
文档在发布时很有用,但是与应用程序一起发布的使用文档对于尝试应用程序的人来说非常有价值。通过更新greeter子项目构建脚本中的任务依赖项,将生成的文档添加到您的发行版中。
greeter/build.gradle
distZip {
from project(':docs').asciidoctor, { ①
into "${project.name}-${version}"
}
}
distTar {
from project(':docs').asciidoctor, {
into "${project.name}-${version}"
}
}
greeter/build.gradle.kts
tasks.distZip {
from(project(":docs").tasks["asciidoctor"]) { ①
into("${project.name}-$version")
}
}
tasks.distTar {
from(project(":docs").tasks["asciidoctor"]) {
into("${project.name}-$version")
}
}
① 使用project(:NAME)语法结构去让这个任务依赖另一个项目。
再次从顶部构建,这次生成的归档文件将包括文档。
$ ./gradlew build
注意,大多数任务仍然是最新的,但是重新运行了distZip和distTar任务以包含文档。如果您解压缩其中一个归档文件(greeter-1.0.zip或greeter-1.0.tar在greeter/build/distribution目录中),您将看到包含在html5文件夹中的文档。
注意:在重新构建前别忘了执行清理命令:./gradlew clean
重构公共构建脚本代码
此时,您可能已经注意到,在greeting-library和greeter子项目构建脚本中都有公共脚本代码。Gradle的一个关键特性是能够在根项目中放置这样的公共构建脚本代码。
编辑根项目中的构建脚本添加以下代码:
build.gradle
configure(subprojects.findAll { it.name == 'greeter' || it.name == 'greeting-library' }) { ①
apply plugin: 'groovy'
dependencies {
testCompile 'org.spockframework:spock-core:1.0-groovy-2.4', {
exclude module: 'groovy-all'
}
}
}
build.gradle.kts
configure(subprojects.filter { it.name == "greeter" || it.name == "greeting-library" }) { ①
apply(plugin = "groovy")
dependencies {
"testCompile"("org.spockframework:spock-core:1.0-groovy-2.4") {
exclude(module = "groovy-all")
}
}
}
① 与谓词闭包一起使用configure允许配置有选择的子项目。谓词闭包传递一个子项目,子项目的名称可以查询。在这种情况下,只配置具有特定名称匹配的子项目。
同时从greeting-library子项目的构建脚本中删除以下代码行:
greeting-library/build.gradle (Removed code)
plugins {
id 'groovy'
}
dependencies {
testCompile 'org.spockframework:spock-core:1.0-groovy-2.4', {
exclude module: 'groovy-all'
}
}
greeting-library/build.gradle.kts (Removed code)
plugins {
id("groovy")
}
dependencies {
testCompile("org.spockframework:spock-core:1.0-groovy-2.4") {
exclude(module = "groovy-all")
}
}
还要从greeter子项目的构建脚本中删除类似的行。换句话说,从plugins块中删除groovy插件,从dependencies块中删除spock依赖项,而将dependencies块本身保留下来。
从根目录重新执行构建:
$ ./gradlew clean build
总结
从本指南你学会了:
1. 通过组合多个子项目来创建模块化软件项目。
2. 让一个子项目使用来自另一个子项目的工件。
3. 轻松使用polyglot项目。
4. 在所有子项目中运行类似的命名任务。
5. 在特定的子项目中运行任务,而不更改到该子项目的文件夹。
6. 将公共子项目设置重构到根项目中。
7. 有选择地从根项目配置子项目。
下一步
1. 记住这一类命令./gradlew,../gradlew甚至../../gradlew的工作可能变成一项艰巨的任务。如果您使用的是类unix操作系统,请考虑安装gdub。
请阅读 用户手册中关于多项目的内容。