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 时,这种创建并不明显,如下例所示,其中 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 相同,只是它用于测试

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

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

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

  • JavaCompile 任务来 fork 并使用正确的 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 任务依赖于生成源的任务

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

调试编译错误

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 Library Plugin 还原生支持运行使用 JUnit 3、4 和 5 的测试(JUnit 5 支持在 Gradle 4.6 中引入)和 TestNG。您将获得

  • 类型为 Test 的自动 test 任务,使用 test source set

  • 一个 HTML 测试报告,其中包含来自所有运行的 Test 任务的结果

  • 轻松过滤要运行的测试

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

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

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

由于测试方面的内容很多,因此该主题在 自己的章节 中进行了介绍,我们在其中查看了

  • 测试是如何运行的

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

  • Gradle 如何发现测试

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

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

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

打包和发布

您如何打包并可能发布您的 Java 项目取决于它的项目类型。库、应用程序、Web 应用程序和企业应用程序都有不同的要求。在本节中,我们将重点介绍 Java Library Plugin 提供的基本功能。

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

构建 JVM 组件

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

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

构建 Java 库

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

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

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

构建 Java 应用程序

打包为 JAR 的 Java 应用程序未设置为易于从命令行或桌面环境启动。Application Plugin 通过创建一个发行版来解决命令行方面的问题,该发行版包括生产 JAR、其依赖项以及类 Unix 和 Windows 系统的启动脚本。

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

  • assemble 创建应用程序的 ZIP 和 TAR 发行版,其中包含运行它所需的一切

  • 一个 run 任务,从构建开始应用程序(用于轻松测试)

  • Shell 和 Windows Batch 脚本来启动应用程序

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

构建 Java Web 应用程序

Java Web 应用程序可以以多种方式打包和部署,具体取决于您使用的技术。例如,您可以使用带有 fat JAR 的 Spring Boot 或在 Netty 上运行的基于 Reactive 的系统。无论您使用何种技术,Gradle 及其庞大的插件社区都将满足您的需求。但是,Core Gradle 仅直接支持作为 WAR 文件部署的传统基于 Servlet 的 Web 应用程序。

这种支持来自 War Plugin,它自动应用 Java Plugin 并添加一个额外的打包步骤,该步骤执行以下操作

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

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

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

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

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

构建 Java EE 应用程序

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

构建 Java 平台

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

支持来自 Java Platform plugin,它设置了不同的配置和发布组件。

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

启用 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 编译(source set 的 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 语言的 优秀插件