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 测试章中看到这种方法的示例,它向您展示了如何在项目中设置集成测试

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

源集配置

创建源集时,它还会创建上述多个配置。构建逻辑**不应**尝试创建或访问这些配置,直到它们首先由源集创建。

创建源集时,如果其中一个自动创建的配置已经存在,并且其作用与源集将分配的作用不同,则构建将失败。

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

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

sourceSets {
    val myCode: SourceSet by creating
}
build.gradle
configurations {
    myCodeCompileClasspath
}

sourceSets {
    myCode
}

在这种情况下,构建将失败。

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

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

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

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

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

管理依赖项

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

指定 Java 项目的依赖项只需三部分信息

  • 您需要哪个依赖项,例如名称和版本。

  • 它需要什么,例如编译或运行。

  • 去哪里寻找它。

前两个在 dependencies {} 块中指定,第三个在 repositories {} 块中指定。例如,要告诉 Gradle 您的项目需要 3.6.7 版本的 Hibernate Core 来编译和运行您的生产代码,并且您想从 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 相同,但用于测试。

  • testImplementationimplementation 的测试等效项。

  • testRuntimeOnlyruntimeOnly 的测试等效项。

您可以在 插件参考章 中了解有关这些内容以及它们之间关系的更多信息。

请注意,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 在哪里可以找到源文件。您可以通过源集配置来完成此操作。

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

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 中的一个 bug,只有从 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 编译、测试、生成 Javadocs 和执行应用程序。不支持 Java 5 及更低版本。

如果使用 Java 10+,利用 release 标志可能是一个更简单的解决方案,请参见上文。

要使用 Java 6 或 Java 7,需要配置以下任务

  • JavaCompile 任务,用于分支并使用正确的 Java home

  • 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 任务依赖于生成源的任务

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

调试编译错误

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 库插件都会为每个源集添加一个特定的 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。请注意,为了正确发布 JAR,您需要在此处使用 archiveClassifier 而不是 archiveAppendix

您可以使用其中一个发布插件来发布 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 库插件提供了一个类型为 Javadocjavadoc 任务,它将为您的所有生产代码(即 main 源集中的任何源)生成标准 Javadocs。该任务支持 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 库插件通过应用 Base 插件 为您的项目添加了一个 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 应用程序可以根据您使用的技术以多种方式打包和部署。例如,您可以使用带有 fat JAR 的 Spring Boot 或在 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 平台插件,该插件设置不同的配置和发布组件。

此插件是一个例外,因为它不应用 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,我们有效地移除了之前通过让类路径也考虑 sourceSets.main.java.classesDirectory 而声明的对 compileJava 的依赖。

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

所有这些都通过使用 目录属性 变为可能。

额外语言支持

除了核心 Gradle,还有其他 很棒的插件 支持更多 JVM 语言!