Gradle 采用基于约定而非配置的方法来构建基于 JVM 的项目,该方法借鉴了 Apache Maven 的若干约定。具体而言,它使用相同的默认目录结构来存放源文件和资源,并且与兼容 Maven 的存储库配合使用。

我们将在本章详细了解 Java 项目,但大多数主题也适用于其他受支持的 JVM 语言,例如 KotlinGroovyScala。如果您在使用 Gradle 构建基于 JVM 的项目方面经验不足,请查看 Java 示例,了解有关如何构建各种类型的基本 Java 项目的分步说明。

本节中的示例使用 Java 库插件。但是,所述功能由所有 JVM 插件共享。不同插件的具体信息可在其专用文档中找到。

您可以探索一些动手示例,了解 JavaGroovyScalaKotlin

简介

Java 项目最简单的构建脚本应用 Java 库插件,并可选地设置项目版本并选择要使用的 Java 工具链

build.gradle.kts
plugins {
    `java-library`
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

version = "1.2.1"
build.gradle
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 库插件还将上述任务集成到标准 基本插件生命周期任务

  • jar 附加到 assemble

  • test 附加到 check

本章的其余部分介绍了根据您的要求自定义构建的不同途径。您还将在后面看到如何针对库、应用程序、Web 应用程序和企业应用程序调整构建。

通过源集声明您的源文件

Gradle 的 Java 支持率先引入了一个新的概念来构建基于源代码的项目:源代码集。其主要思想是,源代码文件和资源通常按类型进行逻辑分组,例如应用程序代码、单元测试和集成测试。每个逻辑组通常都有自己的一组文件依赖项、类路径等。值得注意的是,构成源代码集的文件不必位于同一目录中

源代码集是一个强大的概念,它将编译的几个方面联系在一起

  • 源代码文件及其所在位置

  • 编译类路径,包括任何必需的依赖项(通过 Gradle 配置

  • 已编译类文件放置的位置

您可以在此图表中看到它们之间的关系

java sourcesets compilation
图 1. 源代码集和 Java 编译

阴影框表示源代码集本身的属性。除此之外,Java 库插件还会自动为每个源代码集(由您或插件定义)创建一个编译任务,名为 compileSourceSetJava,以及几个 依赖项配置

main 源代码集

大多数语言插件(包括 Java)都会自动创建一个名为 main 的源代码集,用于项目的生产代码。此源代码集的特殊之处在于,其名称不包含在配置和任务的名称中,因此您只有 compileJava 任务和 compileOnlyimplementation 配置,而不是 compileMainJavamainCompileOnlymainImplementation

Java 项目通常包含除源代码文件之外的其他资源,例如属性文件,这些文件可能需要处理(例如替换文件中的标记)并在最终 JAR 中打包。Java 库插件通过为每个已定义的源代码集自动创建一个名为 processSourceSetResources(或 main 源代码集的 processResources)的专用任务来处理此问题。下图显示了源代码集如何与此任务配合使用

java sourcesets process resources
图 2. 处理源代码集的非源代码文件

与之前一样,阴影框表示源集的属性,在本例中包括资源文件的位置以及它们被复制到的位置。

除了main源集,Java 库插件还定义了一个test源集,它表示项目的测试。此源集由test任务使用,该任务运行测试。您可以在Java 测试章节中了解有关此任务和相关主题的更多信息。

项目通常将此源集用于单元测试,但您也可以根据需要将其用于集成、验收和其他类型的测试。另一种方法是为每种其他测试类型定义一个新的源集,通常出于以下一个或两个原因

  • 您希望出于美观和可管理性将测试彼此分开

  • 不同的测试类型需要不同的编译或运行时类路径或其他一些设置差异

您可以在 Java 测试章节中看到此方法的一个示例,该章节向您展示如何在项目中设置集成测试

您将在以下内容中了解有关源集及其提供的功能的更多信息

源集配置

创建源集时,它还会创建如上所述的许多配置。构建逻辑应尝试在源集首次创建这些配置之前创建或访问这些配置。

在创建源集时,如果这些自动创建的配置之一已存在,Gradle 将发出弃用警告。如果现有配置的角色与源集将分配的角色不同,则其角色将变异为正确的值,并且将发出另一个弃用警告。

下面的构建演示了这种不需要的行为。

build.gradle.kts
configurations {
    val myCodeCompileClasspath: Configuration by creating
}

sourceSets {
    val myCode: SourceSet by creating
}
build.gradle
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

遵循以下两个简单的最佳实践将避免此问题

  1. 不要创建名称将被源集使用的配置,例如以ApiImplementationApiElementsCompileOnlyCompileOnlyApiRuntimeOnlyRuntimeClasspathRuntimeElements结尾的名称。(此列表不详尽。)

  2. 在任何自定义配置之前创建任何自定义源集。

请记住,每当您在 configurations 容器中引用配置时 - 无论是否提供初始化操作 - Gradle 都会创建该配置。有时在使用 Groovy DSL 时,此创建并不明显,如下面的示例所示,其中在调用 extendsFrom 之前创建了 myCustomConfiguration

build.gradle
configurations {
    myCustomConfiguration.extendsFrom(implementation)
}

有关更多信息,请参阅 不要预先配置创建

管理您的依赖项

绝大多数 Java 项目都依赖于库,因此管理项目的依赖项是构建 Java 项目的重要组成部分。依赖项管理是一个很大的主题,因此我们将重点关注此处 Java 项目的基础知识。如果您想深入了解详细信息,请查看 依赖项管理简介

指定 Java 项目的依赖项只需要三条信息

  • 您需要的依赖项,例如名称和版本

  • 需要它的原因,例如编译或运行

  • 在哪里查找它

前两个在 dependencies {} 块中指定,第三个在 repositories {} 块中指定。例如,要告诉 Gradle 您的项目需要 Hibernate Core 的 3.6.7 版本来编译和运行您的生产代码,并且您希望从 Maven Central 存储库下载该库,您可以使用以下片段

示例 4. 声明依赖项
build.gradle.kts
repositories {
    mavenCentral()
}

dependencies {
    implementation("org.hibernate:hibernate-core:3.6.7.Final")
}
build.gradle
repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.hibernate:hibernate-core:3.6.7.Final'
}

Gradle 对这三个元素的术语如下

  • 存储库 (例如:mavenCentral()) — 在哪里查找您声明为依赖项的模块

  • 配置 (例如:implementation) — 依赖项的命名集合,针对特定目标(例如编译或运行模块)分组在一起 — Maven 作用域的更灵活形式

  • 模块坐标 (例如: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 库插件为依赖项提供了两个其他配置 — apicompileOnlyApi — 这些依赖项是编译模块和依赖于该模块的任何模块所必需的。

为什么没有compile配置?

Java 库插件历来使用compile配置来获取编译和运行项目生产代码所需的依赖项。它现在已被弃用,并且在使用时会发出警告,因为它没有区分影响 Java 库项目的公共 API 的依赖项和不影响公共 API 的依赖项。您可以在构建 Java 库中了解有关此区别重要性的更多信息。

我们在这里仅介绍了皮毛,因此我们建议您在熟悉使用 Gradle 构建 Java 项目的基础知识后阅读专门的依赖管理章节。需要进一步阅读的一些常见场景包括

您会发现 Gradle 具有一个丰富的 API,用于处理依赖项,该 API 需要时间来掌握,但对于常见场景来说,使用起来很简单。

编译代码

如果您遵循约定,编译您的生产和测试代码可以非常简单

  1. 将您的生产源代码放在 src/main/java 目录下

  2. 将您的测试源代码放在 src/test/java

  3. compileOnlyimplementation 配置中声明您的生产编译依赖项(参见上一部分)

  4. testCompileOnlytestImplementation 配置中声明您的测试编译依赖项

  5. 为生产代码运行 compileJava 任务,为测试运行 compileTestJava

其他 JVM 语言插件,例如 Groovy 的插件,遵循相同的约定模式。我们建议您尽可能遵循这些约定,但您不必这样做。有几个自定义选项,您将在接下来看到。

自定义文件和目录位置

假设您有一个旧项目,它使用 src 目录作为生产代码,使用 test 作为测试代码。传统的目录结构不起作用,因此您需要告诉 Gradle 在哪里查找源文件。您可以通过源集配置来做到这一点。

每个源集定义其源代码所在的位置,以及类文件的资源和输出目录。您可以使用以下语法覆盖约定值

build.gradle.kts
sourceSets {
    main {
        java {
            setSrcDirs(listOf("src"))
        }
    }

    test {
        java {
            setSrcDirs(listOf("test"))
        }
    }
}
build.gradle
sourceSets {
    main {
         java {
            srcDirs = ['src']
         }
    }

    test {
        java {
            srcDirs = ['test']
        }
    }
}

现在,Gradle 将仅直接在 srctest 中搜索各自的源代码。如果您不想覆盖约定,而只是想添加一个额外的源目录,也许是包含您想要单独保留的第三方源代码的目录,语法类似

build.gradle.kts
sourceSets {
    main {
        java {
            srcDir("thirdParty/src/main/java")
        }
    }
}
build.gradle
sourceSets {
    main {
        java {
            srcDir 'thirdParty/src/main/java'
        }
    }
}

至关重要的是,我们在这里使用方法 srcDir() 来附加目录路径,而设置 srcDirs 属性会替换任何现有值。这是 Gradle 中的一个常见约定:设置属性会替换值,而相应的方法会附加值。

您可以在 SourceSetSourceDirectorySet 的 DSL 参考中查看源集上可用的所有属性和方法。请注意,srcDirssrcDir() 都在 SourceDirectorySet 上。

更改编译器选项

大多数编译器选项可通过相应的任务访问,例如 compileJavacompileTestJava。这些任务的类型为 JavaCompile,因此请阅读任务参考以获取最新且全面的选项列表。

例如,如果你想为编译器使用一个单独的 JVM 进程并防止编译失败导致构建失败,你可以使用此配置

build.gradle.kts
tasks.compileJava {
    options.isIncremental = true
    options.isFork = true
    options.isFailOnError = false
}
build.gradle
compileJava {
    options.incremental = true
    options.fork = true
    options.failOnError = false
}

你还可以使用此方法更改编译器的详细程度,禁用字节码中的调试输出,并配置编译器查找注释处理程序的位置。

针对特定 Java 版本

默认情况下,Gradle 会将 Java 代码编译为运行 Gradle 的 JVM 的语言级别。如果你需要在编译时针对特定版本的 Java,Gradle 提供了多种选项

  1. 使用 Java 工具链 是针对语言版本的首选方式。
    工具链统一处理编译、执行和 Javadoc 生成,并且可以在项目级别进行配置。

  2. 从 Java 10 开始,可以使用 release 属性。
    选择一个 Java 版本可确保使用配置的语言级别和该 Java 版本的 JDK API 进行编译。

  3. 使用 sourceCompatibilitytargetCompatibility 属性。
    尽管通常不建议这样做,但这些选项在历史上用于在编译期间配置 Java 版本。

使用工具链

当使用特定工具链编译 Java 代码时,实际编译是由指定 Java 版本的编译器执行的。该编译器提供对所请求 Java 语言版本的语言特性和 JDK API 的访问。

在最简单的情况下,可以使用 java 扩展为项目配置工具链。这样,不仅编译可以从中受益,而且其他任务(例如 testjavadoc)也将始终如一地使用相同的工具链。

build.gradle.kts
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
build.gradle
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

你可以在 Java 工具链 指南中了解更多相关信息。

使用 Java 发布版本

设置 release 标志可确保使用指定的语言级别,而不管实际执行编译的编译器是什么。要使用此功能,编译器必须支持请求的发布版本。在使用较新的 工具链进行编译时,可以指定较早的发布版本。

Gradle 支持从 Java 10 使用 release 标志。可以在编译任务中按如下方式配置它。

build.gradle.kts
tasks.compileJava {
    options.release = 7
}
build.gradle
compileJava {
    options.release = 7
}

release 标志提供的保证与工具链类似。它验证 Java 源代码未使用在更高 Java 版本中引入的语言功能,并且代码不会访问来自更高 JDK 的 API。编译器生成的字节码也对应于请求的 Java 版本,这意味着已编译的代码无法在较旧的 JVM 上执行。

Java 编译器的 release 选项是在 Java 9 中引入的。但是,由于 Java 9 中的 错误,仅从 Java 10 开始才能将此选项与 Gradle 一起使用。

使用 Java 兼容性选项

由于它们提供的保证较弱,因此使用兼容性属性可能会导致在执行已编译代码时发生运行时故障。相反,请考虑使用 工具链release 标志。

sourceCompatibilitytargetCompatibility 选项对应于 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 任务用于分叉并使用正确的 Java 主目录

  • Javadoc 任务用于使用正确的 javadoc 可执行文件

  • TestJavaExec 任务用于使用正确的 java 可执行文件。

使用 Java 工具链时,可以按如下方式进行操作

build.gradle.kts
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(7)
    }
}
build.gradle
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(7)
    }
}

唯一的要求是安装 Java 7,并且它必须位于 Gradle 可以自动检测到的位置显式配置 的位置。

分别编译独立的源

大多数项目至少有两组独立的源:生产代码和测试代码。Gradle 已将此场景作为其 Java 惯例的一部分,但是如果您有其他源集怎么办?最常见的场景之一是当您有某种形式的单独集成测试时。在这种情况下,自定义源集可能正是您所需要的。

您可以在 Java 测试章节 中看到设置集成测试的完整示例。您可以以相同的方式设置其他源集,以履行不同的角色。然后问题就变成了:您应该何时定义自定义源集?

要回答这个问题,请考虑这些源

  1. 是否需要使用唯一的类路径进行编译

  2. 是否生成与 maintest 不同的类

  3. 是否形成项目的自然组成部分

如果您对 3 和其他任何一个问题的回答都是肯定的,那么自定义源集可能是正确的方法。例如,集成测试通常是项目的一部分,因为它们测试 main 中的代码。此外,它们通常具有独立于 test 源集的自己的依赖项,或者需要使用自定义 Test 任务运行它们。

其他常见场景不太明确,可能会有更好的解决方案。例如

  • 单独的 API 和实现 JAR — 如果将它们作为单独的项目,这可能是有意义的,特别是如果您已经有一个多项目构建

  • 生成源代码 — 如果结果源代码应与生产代码一起编译,请将它们的路径添加到main源代码集中,并确保compileJava任务取决于生成源代码的任务

如果您不确定是否创建自定义源代码集,那么请继续创建。它应该是简单的,如果不是,那么它可能不是这项工作的合适工具。

管理资源

许多 Java 项目使用源文件之外的资源,例如图像、配置文件和本地化数据。有时这些文件只需打包而不进行更改,而有时需要将它们作为模板文件或以其他方式进行处理。无论哪种方式,Java Library 插件都会为每个源代码集添加一个特定的 Copy 任务,用于处理其关联的资源。

该任务的名称遵循processSourceSetResources的约定 — 对于main源代码集为processResources — 它会自动将src/[sourceSet]/resources中的任何文件复制到将包含在生产 JAR 中的目录。此目标目录还将包含在测试的运行时类路径中。

由于processResourcesProcessResources任务的一个实例,因此您可以执行 使用文件 章节中描述的任何处理。

Java 属性文件和可重复构建

您可以通过 WriteProperties 任务轻松创建 Java 属性文件,该任务修复了 Properties.store() 的一个众所周知的问题,该问题可能会降低 增量构建 的实用性。

用于编写属性文件的标准 Java API 每次都会生成一个唯一的文件,即使使用了相同的属性和值,因为它在注释中包含了一个时间戳。如果没有任何属性发生更改,Gradle 的 WriteProperties 任务将生成完全相同的输出字节对字节。这是通过对属性文件生成方式进行一些调整来实现的

  • 输出中未添加时间戳注释

  • 行分隔符与系统无关,但可以显式配置(默认为'\n'

  • 属性按字母顺序排列

有时,可能需要在不同的机器上以字节对字节的方式重新创建存档。您希望确保从源代码构建工件会产生相同的结果,无论何时何地构建它都是字节对字节。对于 reproducible-builds.org 等项目来说,这是必需的。

这些调整不仅可以更好地集成增量构建,还可以帮助 可重复构建。从本质上讲,可重复构建保证您将从构建执行中看到相同的结果 — 包括测试结果和生产二进制文件 — 无论您何时或在什么系统上运行它。

运行测试

除了在 src/test/java 中提供单元测试的自动编译之外,Java 库插件还原生支持运行使用 JUnit 3、4 和 5(JUnit 5 支持 在 Gradle 4.6 中出现)和 TestNG 的测试。您会得到

  • 使用 test 源集的类型为 Test 的自动 test 任务

  • 包含运行的所有 Test 任务的结果的 HTML 测试报告

  • 轻松过滤要运行的测试

  • 对测试的运行方式进行精细控制

  • 创建您自己的测试执行和测试报告任务的机会

不会为声明的每个源集获得一个 Test 任务,因为并非每个源集都代表测试!这就是为什么您通常需要 创建您自己的 Test 任务 来处理集成和验收测试等内容的原因,如果它们不能包含在 test 源集中。

由于在测试方面有很多内容需要涵盖,因此该主题在其 自己的章节 中,我们将在其中查看

  • 如何运行测试

  • 如何通过过滤运行测试的子集

  • Gradle 如何发现测试

  • 如何配置测试报告并添加您自己的报告任务

  • 如何利用特定的 JUnit 和 TestNG 功能

您还可以了解有关 Test 的 DSL 参考中的测试配置的更多信息。

打包和发布

您如何打包和潜在发布您的 Java 项目取决于它是什么类型的项目。库、应用程序、Web 应用程序和企业应用程序都有不同的要求。在本节中,我们将重点关注 Java 库插件提供的基础内容。

默认情况下,Java 库插件提供 jar 任务,该任务将所有已编译的生产类和资源打包到单个 JAR 中。此 JAR 也由 assemble 任务自动构建。此外,可以配置该插件以提供 javadocJarsourcesJar 任务,以在需要时打包 Javadoc 和源代码。如果使用了发布插件,这些任务将在发布期间自动运行,或者可以被直接调用。

build.gradle.kts
java {
    withJavadocJar()
    withSourcesJar()
}
build.gradle
java {
    withJavadocJar()
    withSourcesJar()
}

如果您想创建一个“uber”(又称“fat”)JAR,可以使用类似这样的任务定义

build.gradle.kts
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) }
    })
}
build.gradle
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 清单

JarWarEar 任务的每个实例都具有 manifest 属性,该属性允许您自定义进入相应存档的 MANIFEST.MF 文件。以下示例演示如何设置 JAR 清单中的属性

build.gradle.kts
tasks.jar {
    manifest {
        attributes(
            "Implementation-Title" to "Gradle",
            "Implementation-Version" to archiveVersion
        )
    }
}
build.gradle
jar {
    manifest {
        attributes("Implementation-Title": "Gradle",
                   "Implementation-Version": archiveVersion)
    }
}

有关其提供的配置选项,请参阅 Manifest

您还可以创建 Manifest 的独立实例。这样做的一个原因是在 JAR 之间共享清单信息。以下示例演示如何在 JAR 之间共享通用属性

build.gradle.kts
val sharedManifest = java.manifest {
    attributes (
        "Implementation-Title" to "Gradle",
        "Implementation-Version" to version
    )
}

tasks.register<Jar>("fooJar") {
    manifest = java.manifest {
        from(sharedManifest)
    }
}
build.gradle
def sharedManifest = java.manifest {
    attributes("Implementation-Title": "Gradle",
               "Implementation-Version": version)
}
tasks.register('fooJar', Jar) {
    manifest = java.manifest {
        from sharedManifest
    }
}

您还可以使用另一个选项将清单合并到单个 Manifest 对象中。这些源清单可以采用文本或另一个 Manifest 对象的形式。在以下示例中,源清单都是文本文件,但 sharedManifest 除外,后者是前一个示例中的 Manifest 对象

build.gradle.kts
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()
                }
            })
        }
    }
}
build.gradle
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()
                }
            }
        }
    }
}

清单按在 from 语句中声明的顺序合并。如果基本清单和合并的清单都为同一密钥定义值,则默认情况下合并的清单获胜。您可以通过添加 eachEntry 操作来自定义合并行为,在这些操作中,您可以访问结果清单的每个条目的 ManifestMergeDetails 实例。请注意,合并是延迟进行的,要么在生成 JAR 时,要么在调用 Manifest.writeTo()Manifest.getEffectiveManifest() 时。

说到 writeTo(),你可以用它在任何时候轻松地将清单写入磁盘,如下所示

build.gradle.kts
tasks.jar { manifest.writeTo(layout.buildDirectory.file("mymanifest.mf")) }
build.gradle
tasks.named('jar') { manifest.writeTo(layout.buildDirectory.file('mymanifest.mf')) }

生成 API 文档

Java 库插件提供了一个 Javadoc 类型为 javadoc 的任务,它将为你的所有生产代码生成标准 Javadoc,即 main 源集中所有的源代码。该任务支持 Javadoc 参考文档 中描述的核心 Javadoc 和标准 doclet 选项。有关这些选项的完整列表,请参阅 CoreJavadocOptionsStandardJavadocDocletOptions

作为一个你可以做什么的示例,假设你希望在 Javadoc 注释中使用 Asciidoc 语法。要做到这一点,你需要将 Asciidoclet 添加到 Javadoc 的 doclet 路径。以下是一个执行此操作的示例

build.gradle.kts
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")
}
build.gradle
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 文档

build.gradle.kts
tasks.register<Javadoc>("testJavadoc") {
    source = sourceSets.test.get().allJava
}
build.gradle
tasks.register('testJavadoc', Javadoc) {
    source = sourceSets.test.allJava
}

这只是你可能遇到的两个非平凡但常见的自定义。

清理构建

通过应用 基本插件,Java 库插件会向你的项目添加一个 clean 任务。此任务只是删除 layout.buildDirectory 目录中的所有内容,因此你应该始终将构建生成的的文件放在那里。该任务是 Delete 的一个实例,你可以通过设置它的 dir 属性来更改它删除的目录。

构建 JVM 组件

所有特定的 JVM 插件都建立在 Java 插件 之上。上面的示例仅说明了此基本插件提供的概念,并与所有 JVM 插件共享。

继续阅读以了解哪些插件适合哪些项目类型,因为建议选择一个特定插件,而不是直接应用 Java 插件。

构建 Java 库

库项目的一个独特方面是它们被其他 Java 项目使用(或“消耗”)。这意味着使用 JAR 文件发布的依赖项元数据(通常以 Maven POM 的形式)至关重要。特别是,库的使用者应该能够区分两种不同类型的依赖项:仅编译库所需的依赖项和编译使用者也所需的依赖项。

Gradle 通过Java 库插件管理此区别,除了本章中介绍的implementation之外,还引入了api 配置。如果依赖项的类型出现在库的公共类的公共字段或方法中,那么该依赖项将通过库的公共 API 暴露,因此应该添加到api 配置中。否则,该依赖项是内部实现细节,应该添加到implementation 中。

如果您不确定 API 和实现依赖项之间的区别,Java 库插件章节 有详细的解释。此外,您还可以探索一个基本的、实用的构建 Java 库示例

构建 Java 应用程序

打包为 JAR 的 Java 应用程序无法轻松从命令行或桌面环境启动。应用程序插件通过创建一个包含生产 JAR、其依赖项和类似 Unix 和 Windows 系统的启动脚本的分发来解决命令行方面的问题。

有关更多详细信息,请参阅插件章节,但以下是对您获得的内容的快速总结

  • assemble 创建包含运行应用程序所需内容的应用程序的 ZIP 和 TAR 分发

  • 一个从构建启动应用程序的run 任务(以便于测试)

  • 启动应用程序的 Shell 和 Windows 批处理脚本

您可以在相应的示例中看到构建 Java 应用程序的基本示例。

构建 Java Web 应用程序

Java Web 应用程序可以根据您使用的技术以多种方式打包和部署。例如,您可以将Spring Boot与 fat JAR 或在Netty上运行的基于Reactive的系统一起使用。无论您使用什么技术,Gradle 及其庞大的插件社区都能满足您的需求。但是,核心 Gradle 仅直接支持部署为 WAR 文件的基于 Servlet 的传统 Web 应用程序。

该支持通过War 插件提供,该插件自动应用 Java 插件并添加一个额外的打包步骤,该步骤执行以下操作

  • 将静态资源从 src/main/webapp 复制到 WAR 的根目录

  • 将编译后的生产类复制到 WAR 的 WEB-INF/classes 子目录

  • 将库依赖项复制到 WAR 的 WEB-INF/lib 子目录

这是由 war 任务完成的,该任务有效地替换了 jar 任务(尽管该任务仍然存在)并附加到 assemble 生命周期任务。请参阅插件章节以了解更多详细信息和配置选项。

没有核心支持直接从构建中运行 Web 应用程序,但我们建议您尝试 Gretty 社区插件,它提供了一个嵌入式 Servlet 容器。

构建 Java EE 应用程序

多年来,Java 企业系统发生了很大变化,但如果您仍在部署到 JEE 应用程序服务器,则可以使用 Ear 插件。这增加了约定和构建 EAR 文件的任务。插件章节有更多详细信息。

构建 Java 平台

Java 平台表示一组依赖项声明和约束,这些声明和约束形成一个内聚单元,应用于使用项目。该平台没有自己的源代码和工件。它在 Maven 世界中映射到 BOM

支持通过 Java Platform 插件 提供,该插件设置了不同的配置和发布组件。

此插件是例外,因为它不应用 Java 插件。

启用 Java 预览功能

使用 Java 预览功能很可能会导致你的代码与未启用功能预览编译的代码不兼容。因此,我们强烈建议你不要发布使用预览功能编译的库,并将功能预览的使用限制在玩具项目中。

要为编译、测试执行和运行时启用 Java 预览功能,可以使用以下 DSL 代码段

build.gradle.kts
tasks.withType<JavaCompile>().configureEach {
    options.compilerArgs.add("--enable-preview")
}

tasks.withType<Test>().configureEach {
    jvmArgs("--enable-preview")
}

tasks.withType<JavaExec>().configureEach {
    jvmArgs("--enable-preview")
}
build.gradle
tasks.withType(JavaCompile).configureEach {
    options.compilerArgs += "--enable-preview"
}

tasks.withType(Test).configureEach {
    jvmArgs += "--enable-preview"
}

tasks.withType(JavaExec).configureEach {
    jvmArgs += "--enable-preview"
}

构建其他 JVM 语言项目

如果你想利用 JVM 的多语言特性,这里描述的大部分内容仍然适用。

Gradle 本身提供了 GroovyScala 插件。这些插件会自动应用对编译 Java 代码的支持,并且可以通过将它们与 java-library 插件结合使用来进一步增强。

语言之间的编译依赖

这些插件会在 Groovy/Scala 编译和 Java 编译(源代码位于源集的 java 文件夹中)之间创建依赖关系。你可以通过调整相关编译任务的类路径来更改此默认行为,如下例所示

build.gradle.kts
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)
}
build.gradle
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)
}
  1. 通过将 compileGroovy 类路径仅设置为 sourceSets.main.compileClasspath,我们有效地删除了之前对 compileJava 的依赖关系,该依赖关系是通过让类路径也考虑 sourceSets.main.java.classesDirectory 来声明的

  2. 通过将 sourceSets.main.groovy.classesDirectory 添加到 compileJava classpath,我们有效地声明了对 compileGroovy 任务的依赖关系

所有这些都可以通过使用 目录属性 来实现。

额外的语言支持

除了 Gradle 核心之外,还有其他 优秀的插件 适用于更多 JVM 语言!