构建 Java 和 JVM 项目
Gradle 在构建基于 JVM 的项目时采用了约定优于配置的方法,这借鉴了 Apache Maven 的一些约定。特别是,它使用了与 Maven 相同的源代码文件和资源默认目录结构,并且兼容 Maven 仓库。
本章将详细介绍 Java 项目,但大多数主题也适用于其他受支持的 JVM 语言,例如 Kotlin、Groovy 和 Scala。如果您在用 Gradle 构建基于 JVM 的项目方面经验不足,请查看 Java 示例,了解如何构建各种基本 Java 项目的分步说明。
本节中的示例使用了 Java 库插件。但是,所描述的特性是所有 JVM 插件共享的。不同插件的详细信息可在其专门的文档中找到。 |
介绍
plugins {
`java-library`
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
version = "1.2.1"
plugins {
id 'java-library'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
version = '1.2.1'
通过应用 Java 库插件,您可以获得许多特性
-
一个
compileJava
任务,用于编译 src/main/java 目录下所有 Java 源代码文件 -
一个
compileTestJava
任务,用于编译 src/test/java 目录下的源代码文件 -
一个
test
任务,用于运行 src/test/java 中的测试 -
一个
jar
任务,将main
编译后的类和 src/main/resources 中的资源打包到名为 <project>-<version>.jar 的单个 JAR 文件中 -
一个
javadoc
任务,为main
类生成 Javadoc
这不足以构建任何非简单的 Java 项目——至少,您可能有一些文件依赖。但这表示您的构建脚本只需包含针对 您的 项目的特定信息即可。
尽管示例中的属性是可选的,但我们建议您在项目中指定它们。配置工具链可以防止项目因使用不同 Java 版本构建而出现问题。版本字符串对于跟踪项目进展很重要。项目版本默认也用于归档文件名中。 |
Java 库插件还将上述任务集成到标准的 Base 插件生命周期任务中
-
jar
任务附加到assemble
任务 -
test
任务附加到check
任务
本章的其余部分将解释如何根据您的需求自定义构建的各种方法。您稍后还将看到如何调整构建以适应库、应用程序、Web 应用和企业应用。
通过源集声明源文件
Gradle 的 Java 支持首次引入了一个用于构建基于源代码的项目的新概念:源集(source sets)。主要思想是,源代码文件和资源通常按类型进行逻辑分组,例如应用程序代码、单元测试和集成测试。每个逻辑组通常都有自己的一组文件依赖项、类路径等。重要的是,构成源集的文件 不必位于同一目录中!
源集是一个强大的概念,它将编译的多个方面联系起来
-
源代码文件及其位置
-
编译类路径,包括任何必需的依赖项(通过 Gradle 配置)
-
编译后的 class 文件放置的位置
您可以在此图表中看到它们之间的关系

阴影框表示源集本身的属性。此外,Java 库插件会自动为您或插件定义的每个源集创建一个编译任务——命名为 compileSourceSetJava
——以及一些依赖配置。
main
源集大多数语言插件,包括 Java 插件,都会自动创建一个名为 main
的源集,用于项目的生产代码。这个源集很特别,因为它的名称不包含在配置和任务的名称中,因此您只有一个 compileJava
任务以及 compileOnly
和 implementation
配置,而不是 compileMainJava
、mainCompileOnly
和 mainImplementation
。
Java 项目通常包含源代码文件之外的资源,例如属性文件,这些资源可能需要处理——例如通过替换文件中的令牌——并打包到最终的 JAR 中。Java 库插件通过为每个定义的源集自动创建一个专用任务来处理此问题,该任务名为 processSourceSetResources
(对于 main
源集,则为 processResources
)。下图展示了源集如何与此任务配合使用

与之前一样,阴影框表示源集的属性,在这种情况下,包括资源文件的位置以及它们被复制到的位置。
除了 main
源集之外,Java 库插件还定义了一个 test
源集,用于表示项目的测试。此源集由 test
任务使用,该任务运行测试。您可以在Java 测试章节中了解有关此任务和相关主题的更多信息。
项目通常使用此源集进行单元测试,但您也可以根据需要将其用于集成测试、验收测试和其他类型的测试。另一种方法是为其他每种测试类型定义一个新的源集,这通常出于以下一个或两个原因:
-
您希望为了美观和可管理性而将不同类型的测试分开
-
不同测试类型需要不同的编译或运行时类路径,或者设置上存在其他差异
您可以在 Java 测试章节中看到这种方法的示例,其中展示了如何在项目中设置集成测试。
您将从以下章节了解有关源集及其提供的特性的更多信息:
源集配置
创建源集时,它也会创建如上所述的一些配置。构建逻辑在源集首次创建这些配置之前,不应尝试创建或访问它们。
创建源集时,如果其中一个自动创建的配置已经存在,Gradle 将发出弃用警告。如果现有配置的角色与源集将分配的角色不同,则其角色将被修改为正确的值,并再次发出弃用警告。
以下构建示例展示了这种不期望的行为。
configurations {
val myCodeCompileClasspath: Configuration by creating
}
sourceSets {
val myCode: SourceSet by creating
}
configurations {
myCodeCompileClasspath
}
sourceSets {
myCode
}
在这种情况下,会发出以下弃用警告:
When creating configurations during sourceSet custom setup, Gradle found that configuration customCompileClasspath already exists with permitted usage(s):
Consumable - this configuration can be selected by another project as a dependency
Resolvable - this configuration can be resolved by this project to a set of files
Declarable - this configuration can have dependencies added to it
Yet Gradle expected to create it with the usage(s):
Resolvable - this configuration can be resolved by this project to a set of files
遵循两个简单的最佳实践将避免此问题
-
不要创建源集将使用的名称的配置,例如名称以
Api
、Implementation
、ApiElements
、CompileOnly
、CompileOnlyApi
、RuntimeOnly
、RuntimeClasspath
或RuntimeElements
结尾的名称。(此列表不完整。) -
在创建任何自定义配置之前创建所有自定义源集。
请记住,任何时候您在 configurations
容器中引用配置——无论是否提供初始化动作——Gradle 都会创建该配置。有时在使用 Groovy DSL 时,这种创建并不明显,如下面的示例所示,其中 myCustomConfiguration
在调用 extendsFrom
之前创建。
configurations {
myCustomConfiguration.extendsFrom(implementation)
}
管理依赖项
绝大多数 Java 项目都依赖于库,因此管理项目的依赖项是构建 Java 项目的重要组成部分。依赖管理是一个很大的主题,所以我们这里将重点介绍 Java 项目的基础知识。如果您想深入了解详细信息,请查看依赖管理简介。
为您的 Java 项目指定依赖项只需要三个信息:
-
您需要哪个依赖项,例如名称和版本
-
它有什么用处,例如用于编译还是运行
-
在哪里查找它
前两个信息在 dependencies {}
代码块中指定,第三个信息在 repositories {}
代码块中指定。例如,要告诉 Gradle 您的项目需要 Hibernate Core 的 3.6.7 版本来编译和运行您的生产代码,并且您想从 Maven Central 仓库下载该库,您可以使用以下代码片段:
repositories {
mavenCentral()
}
dependencies {
implementation("org.hibernate:hibernate-core:3.6.7.Final")
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.hibernate:hibernate-core:3.6.7.Final'
}
这三个元素的 Gradle 术语如下:
-
仓库(Repository)(例如:
mavenCentral()
)——查找您声明为依赖项的模块的位置 -
配置(Configuration)(例如:
implementation
)——一个命名依赖项的集合,按特定目标分组,例如编译或运行模块——是比 Maven scope 更灵活的形式 -
模块坐标(Module coordinate)(例如:
org.hibernate:hibernate-core-3.6.7.Final
)——依赖项的 ID,通常格式为 '<group>:<module>:<version>'(在 Maven 术语中为 '<groupId>:<artifactId>:<version>')
您可以在这里找到更全面的依赖管理术语表。
就配置而言,主要关注的是:
-
compileOnly
— 用于编译生产代码所需但不应成为运行时类路径一部分的依赖项 -
implementation
(取代了compile
)— 用于编译和运行时 -
runtimeOnly
(取代了runtime
)— 仅在运行时使用,不用于编译 -
testCompileOnly
— 与compileOnly
相同,但用于测试 -
testImplementation
—implementation
的测试版本 -
testRuntimeOnly
—runtimeOnly
的测试版本
您可以在插件参考章节中了解有关这些配置以及它们之间关系的更多信息。
请注意,Java 库插件还提供了另外两个配置——api
和 compileOnlyApi
——用于编译模块本身以及依赖于该模块的任何模块所需的依赖项。
compile
配置?Java 库插件历史上使用 compile
配置来指定编译和运行项目生产代码所需的依赖项。现在它已被弃用,使用时会发出警告,因为它没有区分影响 Java 库项目公共 API 的依赖项和不影响公共 API 的依赖项。您可以在构建 Java 库中了解有关此区别重要性的更多信息。
我们在这里只是浅尝辄止,因此我们建议您在熟悉使用 Gradle 构建 Java 项目的基础知识后,阅读专门的依赖管理章节。一些需要进一步阅读的常见场景包括:
-
使用本地文件系统目录中的依赖项
-
声明同级项目作为依赖项
-
通过复合构建测试您对第三方依赖项的修复(这是一种比发布到并从Maven Local消费更好的替代方案)
您将发现 Gradle 拥有丰富的 API 用于处理依赖项——它需要时间来掌握,但在常见场景下使用起来很简单。
编译代码
如果您遵循约定,编译生产代码和测试代码会非常容易:
-
将您的生产源代码放在 src/main/java 目录下
-
将您的测试源代码放在 src/test/java 目录下
-
在
compileOnly
或implementation
配置中声明您的生产编译依赖项(参见上一节) -
在
testCompileOnly
或testImplementation
配置中声明您的测试编译依赖项 -
运行
compileJava
任务编译生产代码,运行compileTestJava
任务编译测试
其他 JVM 语言插件,例如 Groovy 插件,也遵循相同的约定模式。我们建议您尽可能遵循这些约定,但这不是强制的。有几种自定义选项,您将在下一节中看到。
自定义文件和目录位置
假设您有一个遗留项目,将 src 目录用于生产代码,将 test 目录用于测试代码。传统的目录结构将无法工作,因此您需要告诉 Gradle 到哪里查找源文件。您可以通过源集配置来做到这一点。
每个源集定义了其源代码、资源以及 class 文件输出目录的位置。您可以使用以下语法覆盖约定值:
sourceSets {
main {
java {
setSrcDirs(listOf("src"))
}
}
test {
java {
setSrcDirs(listOf("test"))
}
}
}
sourceSets {
main {
java {
srcDirs = ['src']
}
}
test {
java {
srcDirs = ['test']
}
}
}
现在 Gradle 将只直接在 src 和 test 目录中查找相应的源代码。如果您不想覆盖约定,而只是想 添加 一个额外的源目录,例如包含一些您想分开存放的第三方源代码的目录怎么办?语法是类似的:
sourceSets {
main {
java {
srcDir("thirdParty/src/main/java")
}
}
}
sourceSets {
main {
java {
srcDir 'thirdParty/src/main/java'
}
}
}
关键在于,我们在这里使用 srcDir()
方法 来追加目录路径,而设置 srcDirs
属性会替换任何现有值。这是 Gradle 中的一个常见约定:设置属性会替换值,而相应的方法会追加值。
您可以在 SourceSet 和 SourceDirectorySet 的 DSL 参考中查看源集上可用的所有属性和方法。请注意,srcDirs
和 srcDir()
都位于 SourceDirectorySet
上。
更改编译器选项
大多数编译器选项都可以通过相应的任务访问,例如 compileJava
和 compileTestJava
。这些任务的类型是 JavaCompile,因此请阅读任务参考以获取最新和全面的选项列表。
例如,如果您想为编译器使用单独的 JVM 进程并防止编译失败导致构建失败,您可以使用以下配置:
tasks.compileJava {
options.isIncremental = true
options.isFork = true
options.isFailOnError = false
}
compileJava {
options.incremental = true
options.fork = true
options.failOnError = false
}
这也是您更改编译器详细程度、禁用字节码中的调试输出以及配置编译器查找注解处理器位置的方式。
面向特定的 Java 版本
默认情况下,Gradle 会将 Java 代码编译到运行 Gradle 的 JVM 的语言级别。如果您在编译时需要面向特定的 Java 版本,Gradle 提供了多种选项:
使用工具链
当使用特定工具链编译 Java 代码时,实际的编译由指定 Java 版本的编译器执行。编译器提供了对所请求 Java 语言版本的语言特性和 JDK API 的访问。
最简单的情况是,可以使用 java
扩展为项目配置工具链。这样一来,不仅编译能从中受益,test
和 javadoc
等其他任务也将始终使用相同的工具链。
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
您可以在Java 工具链指南中了解更多信息。
使用 Java release 版本
设置 release 标志可以确保使用指定的语言级别,无论实际执行编译的是哪个编译器。要使用此特性,编译器必须支持所请求的 release 版本。在使用较新的工具链进行编译时,可以指定较早的 release 版本。
Gradle 从 Java 10 开始支持使用 release 标志。可以在编译任务上进行配置,如下所示:
tasks.compileJava {
options.release = 7
}
compileJava {
options.release = 7
}
release 标志提供的保证与工具链类似。它会验证 Java 源代码是否未使用后续 Java 版本中引入的语言特性,并且代码未访问较新 JDK 中的 API。编译器生成的字节码也对应于所请求的 Java 版本,这意味着编译后的代码无法在旧的 JVM 上执行。
Java 编译器的 release
选项是在 Java 9 中引入的。然而,由于Java 9 中的一个 bug,在 Gradle 中使用此选项仅从 Java 10 开始才可能。
使用 Java 兼容性选项
sourceCompatibility
和 targetCompatibility
选项对应于 Java 编译器选项 -source
和 -target
。它们被认为是面向特定 Java 版本的遗留机制。然而,这些选项并不能防止使用后续 Java 版本中引入的 API。
sourceCompatibility
-
定义您的源文件中使用的 Java 语言版本。
targetCompatibility
-
定义您的代码应运行的最低 JVM 版本,即它决定了编译器生成的字节码的版本。
这些选项可以按每个 JavaCompile 任务设置,或者在 java { }
扩展中为所有编译任务设置,使用的属性名称相同。
定位 Java 6 和 Java 7
Gradle 本身只能在 Java 8 或更高版本的 JVM 上运行。但是,Gradle 仍然支持针对 Java 6 和 Java 7 进行编译、测试、生成 Javadoc 以及执行应用程序。不支持 Java 5 及以下版本。
如果使用 Java 10+,利用 release 标志可能是一个更简单的解决方案,详见上文。 |
要使用 Java 6 或 Java 7,需要配置以下任务
-
JavaCompile
任务,用于 fork 并使用正确的 Java home -
Javadoc
任务,用于使用正确的javadoc
可执行文件 -
Test
和JavaExec
任务,用于使用正确的java
可执行文件。
通过使用 Java toolchains,可以按如下方式完成
java {
toolchain {
languageVersion = JavaLanguageVersion.of(7)
}
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(7)
}
}
唯一的要求是 Java 7 已安装,并且必须位于 Gradle 可以自动检测到的位置 或 显式配置。
独立编译不同的源
大多数项目至少有两个独立的源集:生产代码和测试代码。Gradle 已经将这种情况作为其 Java 约定的一个部分,但如果您有其他源集呢?最常见的场景之一是您有某种形式的单独的集成测试。在这种情况下,自定义源集可能正是您所需要的。
您可以在 Java 测试章节 中看到设置集成测试的完整示例。您可以以相同的方式设置扮演不同角色的其他源集。那么问题就变成了:什么时候应该定义一个自定义源集?
为了回答这个问题,请考虑源是否
-
需要使用唯一的 classpath 进行编译
-
生成与
main
和test
类不同处理的类 -
构成项目的自然组成部分
如果您对问题 3 和其他任意一个问题的回答都是肯定的,那么自定义源集可能是正确的方法。例如,集成测试通常是项目的一部分,因为它们测试 main
中的代码。此外,它们通常拥有独立于 test
源集的自身依赖项,或者需要使用自定义 Test
任务来运行。
其他常见场景则不那么明确,可能存在更好的解决方案。例如
-
分离 API 和实现 JAR — 将它们作为单独的项目可能更有意义,特别是如果您已经有一个多项目构建
-
生成的源 — 如果生成的源应该与生产代码一起编译,则将其路径添加到
main
源集,并确保compileJava
任务依赖于生成源的任务
如果您不确定是否创建自定义源集,那么请大胆去做。这应该很直接,如果不是,那么它可能不是完成这项工作的正确工具。
调试编译错误
Gradle 提供详细的编译失败报告。
如果编译任务失败,错误摘要将显示在以下位置
-
任务的输出,提供错误的即时上下文。
-
构建输出底部的“发生了什么错误”摘要,与其他所有失败合并以便于参考。
* What went wrong:
Execution failed for task ':project1:compileJava'.
> Compilation failed; see the compiler output below.
Java compilation warning
sample-project/src/main/java/Problem1.java:6: warning: [cast] redundant cast to String
var warning = (String)"warning";
^
Java compilation error
sample-project/src/main/java/Problem2.java:6: error: incompatible types: int cannot be converted to String
String a = 1;
^
此报告功能与 —continue
标志一起使用。
管理资源
许多 Java 项目会使用源代码文件之外的资源,例如图片、配置文件和本地化数据。有时这些文件只需原样打包,有时它们需要作为模板文件或其他方式进行处理。无论哪种方式,Java Library 插件都会为每个源集添加一个特定的 Copy 任务,用于处理其关联的资源。
该任务的名称遵循 processSourceSetResources
的约定 — 对于 main
源集则是 processResources
— 并且它会自动将 src/[sourceSet]/resources 中的任何文件复制到将包含在生产 JAR 中的目录。此目标目录也将包含在测试的运行时 classpath 中。
由于 processResources
是 ProcessResources
任务的一个实例,您可以执行 文件处理 章节中描述的任何处理。
Java properties 文件和可重现构建
您可以通过 WriteProperties 任务轻松创建 Java properties 文件,这解决了 Properties.store()
的一个已知问题,该问题可能会降低 增量构建 的有效性。
用于写入 properties 文件的标准 Java API 每次都会生成一个唯一的文件,即使使用相同的属性和值也是如此,因为它在注释中包含一个时间戳。如果没有任何属性发生更改,Gradle 的 WriteProperties
任务会生成完全相同的字节输出。这是通过对 properties 文件的生成方式进行一些调整来实现的
-
输出中不添加时间戳注释
-
行分隔符与系统无关,但可以显式配置(默认为
'\n'
) -
属性按字母顺序排序
有时,希望在不同的机器上以字节对字节的方式重新创建归档文件。您希望确保从源代码构建的 artifact 生成的结果完全相同,无论何时何地构建它。这对于 reproducible-builds.org 等项目是必要的。
这些调整不仅有助于更好地集成增量构建,还有助于 可重现构建。本质上,可重现构建保证您将从构建执行中看到相同的结果 — 包括测试结果和生产二进制文件 — 无论何时或在何种系统上运行它。
运行测试
除了提供对 src/test/java 中单元测试的自动编译之外,Java Library 插件还原生支持运行使用 JUnit 3、4 & 5(JUnit 5 支持 始于 Gradle 4.6)和 TestNG 的测试。您可以获得
-
一个使用
test
源集的,类型为 Test 的自动test
任务 -
一个 HTML 测试报告,其中包含运行的所有
Test
任务的结果 -
轻松过滤要运行的测试
-
精细控制测试的运行方式
-
创建自己的测试执行和测试报告任务的机会
您不会为声明的每个源集都获得一个 Test
任务,因为并非所有源集都代表测试!这就是为什么如果集成测试和验收测试等无法包含在 test
源集中,您通常需要 创建自己的 Test
任务。
关于测试有很多内容需要涵盖,因此该主题有其 自己的章节,在其中我们将介绍
-
如何运行测试
-
如何通过过滤运行测试子集
-
Gradle 如何发现测试
-
如何配置测试报告和添加自己的报告任务
-
如何利用特定的 JUnit 和 TestNG 特性
您还可以在 Test 的 DSL 参考中了解有关配置测试的更多信息。
打包和发布
如何打包和潜在地发布您的 Java 项目取决于它是什么类型的项目。库、应用程序、Web 应用程序和企业应用程序都有不同的要求。在本节中,我们将重点介绍 Java Library 插件提供的基础内容。
默认情况下,Java Library 插件提供 jar
任务,将所有编译后的生产类和资源打包到一个 JAR 中。该 JAR 也由 assemble
任务自动构建。此外,如果需要,可以配置该插件提供 javadocJar
和 sourcesJar
任务来打包 Javadoc 和源代码。如果使用发布插件,这些任务将在发布期间自动运行,或者可以直接调用。
java {
withJavadocJar()
withSourcesJar()
}
java {
withJavadocJar()
withSourcesJar()
}
如果您想创建一个“uber”(也称为“fat”)JAR,那么可以使用如下任务定义
plugins {
java
}
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
implementation("commons-io:commons-io:2.6")
}
tasks.register<Jar>("uberJar") {
archiveClassifier = "uber"
from(sourceSets.main.get().output)
dependsOn(configurations.runtimeClasspath)
from({
configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
})
}
plugins {
id 'java'
}
version = '1.0.0'
repositories {
mavenCentral()
}
dependencies {
implementation 'commons-io:commons-io:2.6'
}
tasks.register('uberJar', Jar) {
archiveClassifier = 'uber'
from sourceSets.main.output
dependsOn configurations.runtimeClasspath
from {
configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) }
}
}
有关可用的配置选项的更多详细信息,请参阅 Jar。请注意,在这里需要使用 archiveClassifier
而不是 archiveAppendix
才能正确发布 JAR。
您可以使用其中一个发布插件来发布 Java 项目创建的 JAR
修改 JAR manifest
Jar
、War
和 Ear
任务的每个实例都有一个 manifest
属性,允许您自定义对应归档中的 MANIFEST.MF 文件。以下示例演示如何在 JAR 的 manifest 中设置属性
tasks.jar {
manifest {
attributes(
"Implementation-Title" to "Gradle",
"Implementation-Version" to archiveVersion
)
}
}
jar {
manifest {
attributes("Implementation-Title": "Gradle",
"Implementation-Version": archiveVersion)
}
}
有关它提供的配置选项,请参阅 Manifest。
您还可以创建 Manifest
的独立实例。这样做的原因之一是在 JAR 之间共享 manifest 信息。以下示例演示如何在 JAR 之间共享通用属性
val sharedManifest = java.manifest {
attributes (
"Implementation-Title" to "Gradle",
"Implementation-Version" to version
)
}
tasks.register<Jar>("fooJar") {
manifest = java.manifest {
from(sharedManifest)
}
}
def sharedManifest = java.manifest {
attributes("Implementation-Title": "Gradle",
"Implementation-Version": version)
}
tasks.register('fooJar', Jar) {
manifest = java.manifest {
from sharedManifest
}
}
另一个可用的选项是将 manifest 合并到单个 Manifest
对象中。这些源 manifest 可以是文本文件或另一个 Manifest
对象的形式。在以下示例中,源 manifest 都是文本文件,除了 sharedManifest
,它是上一个示例中的 Manifest
对象
tasks.register<Jar>("barJar") {
manifest {
attributes("key1" to "value1")
from(sharedManifest, "src/config/basemanifest.txt")
from(listOf("src/config/javabasemanifest.txt", "src/config/libbasemanifest.txt")) {
eachEntry(Action<ManifestMergeDetails> {
if (baseValue != mergeValue) {
value = baseValue
}
if (key == "foo") {
exclude()
}
})
}
}
}
tasks.register('barJar', Jar) {
manifest {
attributes key1: 'value1'
from sharedManifest, 'src/config/basemanifest.txt'
from(['src/config/javabasemanifest.txt', 'src/config/libbasemanifest.txt']) {
eachEntry { details ->
if (details.baseValue != details.mergeValue) {
details.value = baseValue
}
if (details.key == 'foo') {
details.exclude()
}
}
}
}
}
Manifest 按其在 from
语句中声明的顺序进行合并。如果基本 manifest 和合并的 manifest 都为同一个键定义了值,则默认情况下合并的 manifest 获胜。您可以通过添加 eachEntry
操作来完全自定义合并行为,在这些操作中您可以访问结果 manifest 中每个条目的 ManifestMergeDetails 实例。请注意,合并是延迟完成的,要么在生成 JAR 时,要么在调用 Manifest.writeTo()
或 Manifest.getEffectiveManifest()
时。
说到 writeTo()
,您可以随时使用它轻松地将 manifest 写入磁盘,如下所示
tasks.jar { manifest.writeTo(layout.buildDirectory.file("mymanifest.mf")) }
tasks.named('jar') { manifest.writeTo(layout.buildDirectory.file('mymanifest.mf')) }
生成 API 文档
Java Library 插件提供一个类型为 Javadoc 的 javadoc
任务,它将为所有生产代码(即 main
源集中的任何源代码)生成标准的 Javadoc。该任务支持 Javadoc 参考文档 中描述的核心 Javadoc 和标准 doclet 选项。有关这些选项的完整列表,请参阅 CoreJavadocOptions 和 StandardJavadocDocletOptions。
举一个例子,假设您想在 Javadoc 注释中使用 Asciidoc 语法。为此,您需要将 Asciidoclet 添加到 Javadoc 的 doclet 路径。这是一个实现此目的的示例
val asciidoclet by configurations.creating
dependencies {
asciidoclet("org.asciidoctor:asciidoclet:1.+")
}
tasks.register("configureJavadoc") {
doLast {
tasks.javadoc {
options.doclet = "org.asciidoctor.Asciidoclet"
options.docletpath = asciidoclet.files.toList()
}
}
}
tasks.javadoc {
dependsOn("configureJavadoc")
}
configurations {
asciidoclet
}
dependencies {
asciidoclet 'org.asciidoctor:asciidoclet:1.+'
}
tasks.register('configureJavadoc') {
doLast {
javadoc {
options.doclet = 'org.asciidoctor.Asciidoclet'
options.docletpath = configurations.asciidoclet.files.toList()
}
}
}
javadoc {
dependsOn configureJavadoc
}
您不必为此创建一个配置,但这是处理特定用途所需依赖项的一种优雅方式。
您可能还想创建自己的 Javadoc 任务,例如为测试生成 API 文档
tasks.register<Javadoc>("testJavadoc") {
source = sourceSets.test.get().allJava
}
tasks.register('testJavadoc', Javadoc) {
source = sourceSets.test.allJava
}
这些只是您可能遇到的两个非平凡但常见的自定义设置。
构建 JVM 组件
所有特定的 JVM 插件都构建在 Java 插件 之上。上面的示例仅说明了由这个基础插件提供并与所有 JVM 插件共享的概念。
继续阅读以了解哪些插件适用于哪些项目类型,因为建议选择特定的插件而不是直接应用 Java 插件。
构建 Java 库
库项目的独特之处在于它们被其他 Java 项目使用(或“消费”)。这意味着与 JAR 文件一起发布的依赖元数据 — 通常以 Maven POM 的形式 — 至关重要。特别是,库的消费者应该能够区分两种不同类型的依赖项:仅编译您的库所需的依赖项和编译消费者也所需的依赖项。
Gradle 通过 Java Library 插件 管理这种区分,该插件除了本章介绍的 implementation 配置外,还引入了 api 配置。如果某个依赖项中的类型出现在库公共类的公共字段或方法中,则该依赖项通过库的公共 API 暴露,因此应将其添加到 api 配置中。否则,该依赖项是内部实现细节,应添加到 implementation 中。
如果您不确定 API 依赖项和实现依赖项之间的区别,Java Library 插件章节 提供了详细的解释。此外,您可以探索构建 Java 库的基础实用 示例。
构建 Java 应用程序
打包为 JAR 的 Java 应用程序不易从命令行或桌面环境启动。Application 插件 通过创建一个包含生产 JAR、其依赖项以及适用于类 Unix 和 Windows 系统的启动脚本的分发包来解决命令行启动问题。
有关更多详细信息,请参阅该插件的章节,以下是您将获得的功能摘要
-
assemble
创建应用程序的 ZIP 和 TAR 分发包,其中包含运行应用程序所需的一切 -
一个
run
任务,用于从构建启动应用程序(便于测试) -
Shell 和 Windows Batch 脚本用于启动应用程序
您可以在相应的 示例 中看到构建 Java 应用程序的基本示例。
构建 Java Web 应用程序
Java Web 应用程序可以根据您使用的技术以多种方式打包和部署。例如,您可以将 Spring Boot 与 fat JAR 一起使用,或者使用基于 Reactive 并在 Netty 上运行的系统。无论您使用何种技术,Gradle 及其庞大的插件社区都能满足您的需求。然而,核心 Gradle 仅直接支持部署为 WAR 文件的传统基于 Servlet 的 Web 应用程序。
该支持通过 War 插件 提供,该插件会自动应用 Java 插件并添加一个额外的打包步骤,执行以下操作
-
将 src/main/webapp 中的静态资源复制到 WAR 的根目录
-
将编译后的生产类复制到 WAR 的 WEB-INF/classes 子目录
-
将库依赖项复制到 WAR 的 WEB-INF/lib 子目录
这是由 war
任务完成的,该任务有效地替换了 jar
任务 — 尽管该任务仍然存在 — 并附加到 assemble
生命周期任务。有关更多详细信息和配置选项,请参阅该插件的章节。
核心 Gradle 不直接支持从构建运行您的 Web 应用程序,但我们建议您尝试提供嵌入式 Servlet 容器的 Gretty 社区插件。
构建 Java EE 应用程序
Java 企业系统多年来发生了很大变化,但如果您仍然部署到 JEE 应用程序服务器,您可以使用 Ear 插件。它添加了用于构建 EAR 文件的约定和任务。该插件的章节有更多详细信息。
构建 Java 平台
Java 平台表示一组依赖声明和约束,它们构成一个内聚单元,应用于消费项目。该平台没有自己的源文件和 artifact。在 Maven 世界中,它映射到 BOM。
该支持通过 Java Platform 插件 提供,该插件设置不同的配置和发布组件。
该插件是一个例外,因为它不应用 Java 插件。 |
启用 Java 预览特性
使用 Java 预览特性很可能会使您的代码与未启用预览特性编译的代码不兼容。因此,我们强烈建议您不要发布使用预览特性编译的库,并将预览特性的使用限制在测试项目。 |
要为编译、测试执行和运行时启用 Java 预览特性,您可以使用以下 DSL 片段
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("--enable-preview")
}
tasks.withType<Test>().configureEach {
jvmArgs("--enable-preview")
}
tasks.withType<JavaExec>().configureEach {
jvmArgs("--enable-preview")
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs += "--enable-preview"
}
tasks.withType(Test).configureEach {
jvmArgs += "--enable-preview"
}
tasks.withType(JavaExec).configureEach {
jvmArgs += "--enable-preview"
}
构建其他 JVM 语言项目
如果您想利用 JVM 的多语言特性,这里描述的大部分内容仍然适用。
语言之间的编译依赖关系
这些插件在 Groovy/Scala 编译和 Java 编译(源集中 java
文件夹中的源代码)之间创建了依赖关系。您可以通过调整相关编译任务的 classpath 来更改此默认行为,如下例所示
tasks.named<AbstractCompile>("compileGroovy") {
// Groovy only needs the declared dependencies
// (and not longer the output of compileJava)
classpath = sourceSets.main.get().compileClasspath
}
tasks.named<AbstractCompile>("compileJava") {
// Java also depends on the result of Groovy compilation
// (which automatically makes it depend of compileGroovy)
classpath += files(sourceSets.main.get().groovy.classesDirectory)
}
tasks.named('compileGroovy') {
// Groovy only needs the declared dependencies
// (and not longer the output of compileJava)
classpath = sourceSets.main.compileClasspath
}
tasks.named('compileJava') {
// Java also depends on the result of Groovy compilation
// (which automatically makes it depend of compileGroovy)
classpath += files(sourceSets.main.groovy.classesDirectory)
}
-
通过将
compileGroovy
的 classpath 仅设置为sourceSets.main.compileClasspath
,我们有效地消除了之前对compileJava
的依赖,该依赖是通过让 classpath 也考虑sourceSets.main.java.classesDirectory
来声明的 -
通过将
sourceSets.main.groovy.classesDirectory
添加到compileJava
的classpath
中,我们有效地声明了对compileGroovy
任务的依赖
所有这些都可以通过使用 目录属性 来实现。