Gradle 在构建基于 JVM 的项目时采用了约定优于配置的方法,这借鉴了 Apache Maven 的一些约定。特别是,它使用了与 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 库插件还将上述任务集成到标准的 Base 插件生命周期任务

  • jar 任务附加到 assemble 任务

  • test 任务附加到 check 任务

本章的其余部分将解释如何根据您的需求自定义构建的各种方法。您稍后还将看到如何调整构建以适应库、应用程序、Web 应用和企业应用。

通过源集声明源文件

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

源集是一个强大的概念,它将编译的多个方面联系起来

  • 源代码文件及其位置

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

  • 编译后的 class 文件放置的位置

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

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 时,这种创建并不明显,如下面的示例所示,其中 myCustomConfiguration 在调用 extendsFrom 之前创建。

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 术语如下:

  • 仓库(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 库插件还提供了另外两个配置——apicompileOnlyApi——用于编译模块本身以及依赖于该模块的任何模块所需的依赖项。

为什么没有 compile 配置?

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

我们在这里只是浅尝辄止,因此我们建议您在熟悉使用 Gradle 构建 Java 项目的基础知识后,阅读专门的依赖管理章节。一些需要进一步阅读的常见场景包括:

您将发现 Gradle 拥有丰富的 API 用于处理依赖项——它需要时间来掌握,但在常见场景下使用起来很简单。

编译代码

如果您遵循约定,编译生产代码和测试代码会非常容易:

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

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

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

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

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

其他 JVM 语言插件,例如 Groovy 插件,也遵循相同的约定模式。我们建议您尽可能遵循这些约定,但这不是强制的。有几种自定义选项,您将在下一节中看到。

自定义文件和目录位置

假设您有一个遗留项目,将 src 目录用于生产代码,将 test 目录用于测试代码。传统的目录结构将无法工作,因此您需要告诉 Gradle 到哪里查找源文件。您可以通过源集配置来做到这一点。

每个源集定义了其源代码、资源以及 class 文件输出目录的位置。您可以使用以下语法覆盖约定值:

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 release 可以确保编译使用配置的语言级别,并针对该 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 版本

设置 release 标志可以确保使用指定的语言级别,无论实际执行编译的是哪个编译器。要使用此特性,编译器必须支持所请求的 release 版本。在使用较新的工具链进行编译时,可以指定较早的 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 中的一个 bug,在 Gradle 中使用此选项仅从 Java 10 开始才可能。

使用 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 任务,用于 fork 并使用正确的 Java home

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

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

通过使用 Java toolchains,可以按如下方式完成

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

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

独立编译不同的源

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

您可以在 Java 测试章节 中看到设置集成测试的完整示例。您可以以相同的方式设置扮演不同角色的其他源集。那么问题就变成了:什么时候应该定义一个自定义源集?

为了回答这个问题,请考虑源是否

  1. 需要使用唯一的 classpath 进行编译

  2. 生成与 maintest 类不同处理的类

  3. 构成项目的自然组成部分

如果您对问题 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 中。

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

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 任务自动构建。此外,如果需要,可以配置该插件提供 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 manifest

JarWarEar 任务的每个实例都有一个 manifest 属性,允许您自定义对应归档中的 MANIFEST.MF 文件。以下示例演示如何在 JAR 的 manifest 中设置属性

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 之间共享 manifest 信息。以下示例演示如何在 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 对象中。这些源 manifest 可以是文本文件或另一个 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()
                }
            }
        }
    }
}

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

说到 writeTo(),您可以随时使用它轻松地将 manifest 写入磁盘,如下所示

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 Library 插件提供一个类型为 Javadocjavadoc 任务,它将为所有生产代码(即 main 源集中的任何源代码)生成标准的 Javadoc。该任务支持 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 Library 插件通过应用 Base 插件 为您的项目添加了一个 clean 任务。该任务只是删除了 layout.buildDirectory 目录中的所有内容,因此您应该始终将构建生成的文件放在那里。该任务是 Delete 的一个实例,您可以通过设置其 dir 属性来更改它删除的目录。

构建 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 片段

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 文件夹中的源代码)之间创建了依赖关系。您可以通过调整相关编译任务的 classpath 来更改此默认行为,如下例所示

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 的 classpath 仅设置为 sourceSets.main.compileClasspath,我们有效地消除了之前对 compileJava 的依赖,该依赖是通过让 classpath 也考虑 sourceSets.main.java.classesDirectory 来声明的

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

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

额外的语言支持

除了核心 Gradle 之外,还有许多其他 优秀插件 支持更多 JVM 语言!