Gradle-依赖管理指南-全-

Gradle 依赖管理指南(全)

原文:zh.annas-archive.org/md5/d8aa8278b91e9c8ca99c7430135267e9

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

当我们在 Java 或 Groovy 项目中编写代码时,我们通常依赖于其他项目或库。例如,我们可以在我们的项目中使用 Spring 框架,因此我们依赖于 Spring 框架中找到的类。我们希望能够从我们的构建自动化工具 Gradle 中管理此类依赖关系。

我们将看到如何定义和自定义我们需要的依赖关系。我们不仅学习如何定义依赖关系,还学习如何与存储依赖关系的仓库一起工作。接下来,我们将看到如何自定义 Gradle 解决依赖关系的方式。

除了依赖于其他库之外,我们的项目也可以成为其他项目的依赖。这意味着我们需要知道如何部署我们的项目工件,以便其他开发者可以使用它。我们学习如何定义工件以及如何将它们部署到,例如,Maven 或 Ivy 仓库。

本书涵盖的内容

第一章,定义依赖关系,介绍了依赖配置作为组织依赖关系的一种方式。您将了解 Gradle 中不同类型的依赖关系。

第二章,与仓库一起工作,涵盖了我们可以如何定义存储我们的依赖关系的仓库。我们将看到如何设置位置以及仓库的布局。

第三章,解决依赖关系,讲述了 Gradle 如何解决我们的依赖。您将学习如何自定义依赖关系解析以及解决依赖之间的冲突。

第四章,发布工件,涵盖了如何为我们的项目定义工件以便将其作为依赖关系发布给他人。我们将看到如何使用配置来定义工件。我们还使用本地目录作为仓库来发布工件。

第五章,发布到 Maven 仓库,探讨了如何将我们的工件发布到 Maven 仓库。您将学习如何为类似 Maven 的仓库(如 Artifactory 或 Nexus)定义发布,以及如何使用 Gradle 的新和孵化发布功能。

第六章,发布到 Bintray,涵盖了如何将我们的工件部署到 Bintray。Bintray 将自己称为“服务即分发”并提供了将我们的工件发布到世界的一种低级方式。在本章中,我们将探讨如何使用 Bintray Gradle 插件发布我们的工件。

第七章,发布到 Ivy 仓库,是关于将我们的工件发布到 Ivy 仓库。我们将探讨将我们的工件发布到 Ivy 仓库的不同选项,这实际上与发布到 Maven 仓库非常相似。

为本书所需

为了使用 Gradle 以及本书中的代码示例,我们需要至少 Java 开发工具包(版本 1.6 或更高),Gradle(示例是用 Gradle 2.3 编写的),以及一个好的文本编辑器。

本书面向对象

如果你正在从事 Java 或 Groovy 项目,并使用或打算使用 Gradle 来构建你的代码,这本书就是为你准备的。如果你的代码依赖于其他项目或库,你将学习如何定义和自定义这些依赖项。你的代码也可以被其他项目使用,因此你想将你的项目作为依赖项发布给那些想要阅读这本书的人。

规范

在本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号如下所示:“我们可以通过使用 include 指令包含其他上下文。”

代码块设置如下:

// Define new configurations for build.
configurations {

    // Define configuration vehicles.
    vehicles {
        description = 'Contains vehicle dependencies'
    }

    traffic {
        extendsFrom vehicles
        description = 'Contains traffic dependencies'
    }

}

任何命令行输入或输出都如下所示:

$ gradle bintrayUpload
:generatePomFileForSamplePublication
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:publishSamplePublicationToMavenLocal
:bintrayUpload

BUILD SUCCESSFUL

Total time: 9.125 secs

新术语重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“从该屏幕,我们点击新包按钮。”

读者反馈

我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。

要向我们发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及本书的标题。

如果你在某个主题上有专业知识,并且你对撰写或为本书做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。

下载示例代码

你可以从你的账户中下载示例代码文件www.packtpub.com,以获取你购买的所有 Packt 出版物的示例代码。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从 www.packtpub.com/sites/default/files/downloads/B03462_Coloredimages.pdf 下载此文件。

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

盗版

互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面所提供的帮助。

问题

如果您对本书的任何方面有问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。

第一章:定义依赖

当我们开发软件时,我们需要编写代码。我们的代码由包含类的包组成,而这些类可能依赖于我们项目中其他类和包。对于单个项目来说,这是可以的,但有时我们会依赖于我们未开发的项目的类,例如,我们可能想使用 Apache Commons 库中的类,或者我们可能正在开发一个更大的多项目应用程序的一部分,并且我们依赖于这些其他项目中的类。

大多数时候,当我们编写软件时,我们希望使用项目外的类。实际上,我们对这些类有依赖。这些依赖的类大多存储在归档文件中,例如Java 归档JAR)文件。这样的归档文件由一个唯一的版本号标识,因此我们可以对具有特定版本的库有依赖。

在本章中,你将学习如何在 Gradle 项目中定义依赖项。我们将看到如何定义依赖项的配置。你将了解 Gradle 中的不同依赖类型以及如何在配置构建时使用它们。

声明依赖配置

在 Gradle 中,我们定义依赖配置来将依赖项分组在一起。依赖配置有一个名称和几个属性,例如描述,实际上是一种特殊的FileCollection类型。配置可以相互扩展,因此我们可以在构建文件中构建配置的层次结构。Gradle 插件也可以为我们添加新的配置,例如,Java 插件为我们添加了几个新的配置,如compiletestRuntime。然后,compile配置用于定义编译源树所需的依赖项。依赖配置是用configurations配置块定义的。在块内部,我们可以为我们的构建定义新的配置。所有配置都添加到项目的ConfigurationContainer对象中。

在下面的示例构建文件中,我们定义了两个新的配置,其中traffic配置扩展自vehicles配置。这意味着添加到vehicles配置中的任何依赖项也将在traffic配置中可用。我们还可以为我们的配置分配一个description属性,以便为文档目的提供有关配置的更多信息。以下代码展示了这一点:

// Define new configurations for build.
configurations {

  // Define configuration vehicles.
  vehicles {
    description = 'Contains vehicle dependencies'
  }

  traffic {
    extendsFrom vehicles
    description = 'Contains traffic dependencies'
  }

}

要查看项目中可用的配置,我们可以执行dependencies任务。这个任务适用于每个 Gradle 项目。该任务会输出项目的所有配置和依赖。让我们为当前项目运行这个任务并检查输出:

$ gradle -q dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

traffic - Contains traffic dependencies
No dependencies

vehicles - Contains vehicle dependencies
No dependencies

注意,我们可以在输出中看到我们的两个配置,trafficvehicles。我们没有为这些配置定义任何依赖项,如输出所示。

Java 插件向项目中添加了一些配置,这些配置由 Java 插件的任务使用。让我们将 Java 插件添加到我们的 Gradle 构建文件中:

apply plugin: 'java'

要查看添加了哪些配置,我们调用dependencies任务并查看输出:

$ gradle -q dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

archives - Configuration for archive artifacts.
No dependencies

compile - Compile classpath for source set 'main'.
No dependencies

default - Configuration for default artifacts.
No dependencies

runtime - Runtime classpath for source set 'main'.
No dependencies

testCompile - Compile classpath for source set 'test'.
No dependencies

testRuntime - Runtime classpath for source set 'test'.
No dependencies

只需添加 Java 插件,我们项目中就出现了六个配置。archives配置用于组合我们项目创建的工件。其他配置用于组合我们项目的依赖。在以下表中,总结了依赖配置:

| 名称 | 扩展 | 描述 |
| --- | --- |
| compile | none | 这些是编译依赖。 |
| runtime | compile | 这些是运行时依赖。 |
| testCompile | compile | 这些是编译测试的额外依赖。 |
| testRuntime | runtime, testCompile | 这些是运行测试的额外依赖。 |
| default | runtime | 这些是本项目使用的依赖以及本项目创建的工件。 |

在本章的后面部分,我们将看到如何与分配给配置的依赖项一起工作。在下一节中,我们将学习如何声明我们项目的依赖项。

声明依赖

我们定义了配置或应用了一个添加新配置到我们项目的插件。然而,除非我们向配置添加依赖,否则配置是空的。为了在 Gradle 构建文件中声明依赖,我们必须添加dependencies配置块。配置块将包含我们依赖的定义。在以下示例 Gradle 构建文件中,我们定义了dependencies块:

// Dependencies configuration block.
dependencies {
    // Here we define our dependencies.
}

在配置块内部,我们使用依赖配置的名称后跟我们的依赖描述。依赖配置的名称可以在构建文件中明确定义,也可以由我们使用的插件添加。在 Gradle 中,我们可以定义多种类型的依赖。在以下表中,我们将看到我们可以使用的不同类型:

依赖类型 描述
外部模块依赖 这是对外部模块或库的依赖,可能存储在存储库中。
客户端模块依赖 这是外部模块的依赖,其中工件存储在存储库中,但模块的元信息在构建文件中。我们可以使用此类型的依赖来覆盖元信息。
项目依赖 这是同一构建中另一个 Gradle 项目的依赖。
文件依赖 这是本地计算机上文件集合的依赖。
Gradle API 依赖 这是当前 Gradle 版本的 Gradle API 的依赖。我们在开发 Gradle 插件和任务时使用此依赖。
本地 Groovy 依赖 这是当前 Gradle 版本使用的 Groovy 库的依赖。我们在开发 Gradle 插件和任务时使用此依赖。

外部模块依赖

外部模块依赖是项目中最常见的依赖。这些依赖指的是外部仓库中的一个模块。在本书的后续部分,我们将了解更多关于仓库的信息,但基本上,仓库是将模块存储在中央位置的地方。一个模块包含一个或多个工件和元信息,例如它所依赖的其他模块的引用。

我们可以使用两种表示法在 Gradle 中定义外部模块依赖。我们可以使用字符串表示法或映射表示法。使用映射表示法,我们可以使用所有可用的依赖属性。字符串表示法允许我们设置属性的一个子集,但具有非常简洁的语法。

在以下示例 Gradle 构建文件中,我们使用字符串表示法定义了几个依赖项:

// Define dependencies.
dependencies {
  // Defining two dependencies.
  vehicles 'com.vehicles:car:1.0', 'com.vehicles:truck:2.0'

  // Single dependency.
  traffic 'com.traffic:pedestrian:1.0'
}

字符串表示法具有以下格式:moduleGroup:moduleName:version。在第一个冒号之前使用模块组名,然后是模块名,最后是版本。

如果我们使用映射表示法,我们将显式使用属性的名称并设置每个属性的值。让我们重写我们之前的示例构建文件并使用映射表示法:

// Compact definition of configurations.
configurations {
  vehicles
  traffic.extendsFrom vehicles
}

// Define dependencies.
dependencies {
  // Defining two dependencies.
  vehicles(
    [group: 'com.vehicles', name: 'car', version: '1.0'],
    [group: 'com.vehicles', name: 'truck', version: '2.0'],
  )

  // Single dependency.
  traffic group: 'com.traffic', name: 'pedestrian', version: '1.0'
}

我们可以使用映射表示法指定额外的配置属性,或者添加一个额外的配置闭包。外部模块依赖的一个属性是transitive属性。我们在第三章中学习如何与传递依赖一起工作,解决依赖。在下一个示例构建文件中,我们将使用映射表示法和配置闭包设置此属性:

dependencies {
  // Use transitive attribute in map notation.
  vehicles group: 'com.vehicles', name: 'car',
      version: '1.0', transitive: false

  // Combine map notation with configuration closure.
  vehicles(group: 'com.vehicles', name: 'car', version: '1.0') {
    transitive = true
  }

  // Combine string notation with configuration closure.
  traffic('com.traffic:pedestrian:1.0') {
    transitive = false
  }
}

在本节的其余部分,你将了解你可以用来配置依赖项的更多属性。

Gradle 的一个优点是,我们可以在构建文件中编写 Groovy 代码。这意味着我们可以定义方法和变量,并在 Gradle 文件的其它部分使用它们。这样,我们甚至可以对构建文件进行重构,并创建可维护的构建脚本。请注意,在我们的示例中,我们包含了具有com.vehicles组名的多个依赖项。值被定义了两次,但我们也可以在依赖项配置中创建一个新的变量,并使用该变量的组名和引用。我们在构建文件内的ext配置块中定义变量。我们在 Gradle 中使用ext块向对象添加额外的属性,例如我们的项目。

以下示例代码定义了一个额外的变量来保存组名:

// Define project property with
// dependency group name 'com.vehicles'
ext {
  groupNameVehicles = 'com.vehicles'
}

dependencies {
  // Using Groovy string support with
  // variable substition.
  vehicles "$groupNameVehicles:car:1.0"

  // Using map notation and reference
  // property groupNameVehicles.
  vehicles group: groupNameVehicles, name: 'truck', version: '2.0'
}

如果我们定义外部模块依赖项,那么 Gradle 会尝试在存储库中找到一个模块描述符。如果模块描述符可用,它将被解析以查看需要下载哪些工件。此外,如果模块描述符包含有关模块所需依赖项的信息,那么这些依赖项也将被下载。有时,一个依赖项在存储库中没有描述符,这时 Gradle 才会下载该依赖项的工件。

基于 Maven 模块的依赖项仅包含一个工件,因此 Gradle 很容易知道要下载哪个工件。但对于 Gradle 或 Ivy 模块,这并不明显,因为一个模块可以包含多个工件。该模块将具有多个配置,每个配置都有不同的工件。Gradle 将为这些模块使用名为default的配置。因此,与default配置关联的任何工件和依赖项都将被下载。然而,可能default配置不包含我们需要的工件。因此,我们可以为依赖项配置指定configuration属性以指定我们需要的特定配置。

以下示例为依赖项配置定义了一个configuration属性:

dependencies {
  // Use the 'jar' configuration defined in the
  // module descriptor for this dependency.
  traffic group: 'com.traffic', 
      name: 'pedestrian', 
      version: '1.0',
      configuration: 'jar'

}

当一个依赖项没有模块描述符时,Gradle 只会下载工件。如果我们只想下载具有描述符的模块的工件,而不是任何依赖项,我们可以使用仅工件符号。或者,如果我们想从存储库中下载另一个存档文件,例如带有文档的 TAR 文件,我们也可以使用仅工件符号。

要使用仅工件符号,我们必须将文件扩展名添加到依赖项定义中。如果我们使用字符串符号,我们必须在版本后添加一个以@符号为前缀的扩展名。使用映射符号时,我们可以使用ext属性来设置扩展名。如果我们定义我们的依赖项为仅工件,Gradle 将不会检查是否为该依赖项提供了模块描述符。在下一个构建文件中,我们将看到不同工件仅符号的示例:

dependencies {
  // Using the @ext notation to specify
  // we only want the artifact for this
  // dependency.
  vehicles 'com.vehicles:car:2.0@jar'

  // Use map notation with ext attribute
  // to specify artifact only dependency.
  traffic group: 'com.traffic', name: 'pedestrian',
      version: '1.0', ext: 'jar'

  // Alternatively we can use the configuration closure.
  // We need to specify an artifact configuration closure
  // as well to define the ext attribute.
  vehicles('com.vehicles:car:2.0') {
    artifact {
      name = 'car-docs'
      type = 'tar'
      extension = 'tar'
    }
  }
}

Maven 模块描述符可以使用分类器来指定工件。这通常用于为不同的 Java 版本编译具有相同代码的库时,例如,使用jdk15jdk16分类器为 Java 5 和 Java 6 编译一个库。当我们定义外部模块依赖关系以指定我们想要使用的分类器时,我们可以使用classifier属性。此外,我们还可以在字符串或映射符号中使用它。使用字符串符号时,我们在版本属性后添加一个额外的冒号并指定分类器。对于映射符号,我们可以添加classifier属性并指定我们想要的值。以下构建文件包含了一个具有分类器的依赖项不同定义的示例:

dependencies {
  // Using string notation we can
  // append the classifier after
  // the version attribute, prefixed
  // with a colon.
  vehicles 'com.vehicles:car:2.0:jdk15'

  // With the map notation we simply use the
  // classifier attribute name and the value.
  traffic group: 'com.traffic', name: 'pedestrian',
      version: '1.0', classifier: 'jdk16'

  // Alternatively we can use the configuration closure.
  // We need to specify an artifact configuration closure
  // as well to define the classifier attribute.
  vehicles('com.vehicles:truck:2.0') {
    artifact {
      name = 'truck'
      type = 'jar'
      classifier = 'jdk15'
    }
  }
}

在以下部分,我们将看到如何在我们的构建文件中定义客户端模块依赖项。

定义客户端模块依赖项

当我们定义外部模块依赖项时,我们期望有一个包含有关这些工件及其依赖项信息的模块描述符文件。Gradle 将解析此文件并确定需要下载的内容。请记住,如果此类文件不可用,它将被下载。然而,如果我们想覆盖模块描述符或在没有它的情况下提供它怎么办?在我们提供的模块描述符中,我们可以自己定义模块的依赖项。

我们可以在 Gradle 中通过客户端模块依赖来实现这一点。我们不是依赖于存储库中的模块描述符,而是在构建文件中本地定义自己的模块描述符。现在,我们可以完全控制我们认为模块应该是什么样子,以及模块本身有哪些依赖。我们使用 module 方法来定义一个客户端模块依赖项,用于依赖配置。

在以下示例构建文件中,我们将为依赖项 car 编写一个客户端模块依赖项,并将传递依赖项添加到 driver

dependencies {
  // We use the module method to instruct
  // Gradle to not look for the module descriptor
  // in a repository, but use the one we have
  // defined in the build file.
  vehicles module('com.vehicles:car:2.0') {
    // Car depends on driver.
    dependency('com.traffic:driver:1.0')
  }
}

使用项目依赖项

项目可以是更大、多项目构建的一部分,项目之间可以相互依赖,例如,一个项目可以是另一个项目的生成工件(包括另一个项目的传递依赖项)的依赖项。为了定义这样的依赖项,我们在依赖配置块中使用 project 方法。我们指定项目名称作为参数。我们还可以定义我们依赖的另一个项目的依赖配置的名称。默认情况下,Gradle 将查找默认依赖配置,但使用 configuration 属性,我们可以指定要使用的特定依赖配置。

以下示例构建文件将定义对 cartruck 项目的项目依赖项:

dependencies {
  // Use project method to define project
  // dependency on car project.
  vehicles project(':car')

  // Define project dependency on truck
  // and use dependency configuration api
  // from that project.
  vehicles project(':truck') {
    configuration = 'api'
  }

  // We can use alternative syntax
  // to specify a configuration.
  traffic project(path: ':pedestrian',
          configuration: 'lib')
}

定义文件依赖项

我们可以直接在 Gradle 中向依赖配置添加文件。这些文件不需要存储在存储库中,但必须可以从项目目录中访问。尽管大多数项目将在存储库中存储模块描述符,但可能存在一个遗留项目依赖于公司共享网络驱动器上可用的文件。否则,我们必须在我们的项目中使用一个库,而这个库在任何存储库中都是不可用的。要向我们的依赖配置添加文件依赖项,我们使用 filesfileTree 方法指定一个文件集合。以下示例构建文件显示了所有这些方法的用法:

dependencies {
  // Define a dependency on explicit file(s).
  vehicles files(
    'lib/vehicles/car-2.0.jar',
    'lib/vehicles/truck-1.0.jar'
  )

  // We can use the fileTree method to include
  // multiples from a directory and it's subdirectories.
  traffic fileTree(dir: 'deps', include: '*.jar')
}

如果我们将项目工件发布到存储库,添加的文件将不会成为我们项目的传递依赖项,但如果我们的项目是多项目构建的一部分,则它们将是。

使用内部 Gradle 和 Groovy 依赖项

当我们编写代码以扩展 Gradle,例如自定义任务或插件时,我们可以对 Gradle API 和当前 Gradle 版本使用的 Groovy 库有依赖。我们可以在依赖配置中使用 gradleApilocalGroovy 方法来确保所有正确的依赖项。

如果我们正在编写一些用于扩展 Gradle 的 Groovy 代码,但未使用任何 Gradle API 类,我们可以使用 localGroovy。使用这个方法,当前 Gradle 版本中提供的 Groovy 版本的类和库被添加为依赖项。以下示例构建脚本使用 Groovy 插件,并将依赖项添加到 Gradle 打包的 Groovy 的 compile 配置中:

apply plugin: 'groovy'

dependencies {
  // Define dependency on Groovy
  // version shipped with Gradle.
  compile localGroovy()
}

当我们为 Gradle 编写自定义任务或插件时,我们依赖于 Gradle API。我们需要导入 API 的一些类来编写我们的代码。为了定义对 Gradle 类的依赖,我们使用 gradleApi 方法。这将包括为构建执行的 Gradle 版本包含的依赖项。下一个示例构建文件将展示这个方法的使用:

apply plugin: 'groovy'

dependencies {
  // Define dependency on Gradle classes.
  compile gradleApi()
}

使用动态版本

到目前为止,我们已通过完整的版本号显式设置了一个依赖项的版本。要设置最低版本号,我们可以使用特殊的动态版本语法,例如,要将依赖项版本设置为至少 2.1,我们使用版本值 2.1.+. Gradle 将将依赖项解析为 2.1.0 版本之后的最新版本,或者解析为 2.1 版本本身。上限是 2.2. 在以下示例中,我们将定义一个对至少为 4.0.x 版本的 spring-context 的依赖项:

dependencies {
  compile 'org.springframework:spring-context:4.0.+'
}

要引用模块的最新发布版本,我们可以使用 latest.integration 作为版本值。我们还可以设置我们想要的最低和最高版本号。以下表格显示了 Gradle 中我们可以使用的范围:

范围 描述
[1.0, 2.0] 我们可以使用所有大于或等于 1.0 且小于或等于 2.0 的版本
[1.0, 2.0[ 我们可以使用所有大于或等于 1.0 且小于 2.0 的版本
]1.0, 2.0] 我们可以使用所有大于 1.0 且小于或等于 2.0 的版本
]1.0, 2.0[ 我们可以使用所有大于 1.0 且小于 2.0 的版本
[1.0, ) 我们可以使用所有大于或等于 1.0 的版本
]1.0, ) 我们可以使用所有大于 1.0 的版本
(, 2.0] 我们可以使用所有小于或等于 2.0 的版本
(, 2.0[ 我们可以使用所有小于 2.0 的版本

在以下示例构建文件中,我们将 spring-context 模块的版本设置为大于 4.0.1.RELEASE 且小于 4.0.4.RELEASE

dependencies {
  // The dependency will resolve to version 4.0.3.RELEASE as
  // the latest version if available. Otherwise 4.0.2.RELEASE
  // or 4.0.1.RELEASE.
  compile 'org.springframework:spring-context:[4.0.1.RELEASE,4.0.4.RELEASE['
}

获取依赖信息

我们已经看到了如何在我们的构建脚本中定义依赖项。要获取有关我们依赖项的更多信息,我们可以使用 dependencies 任务。当我们调用此任务时,我们可以看到哪些依赖项属于我们项目的可用配置。此外,任何传递依赖项也会显示出来。下一个示例构建文件定义了对 Spring beans 的依赖项,并应用了 Java 插件。我们还在 repositories 配置块中指定了一个仓库。我们将在下一章中了解更多关于仓库的信息。以下代码捕捉了本段落的讨论:

apply plugin: 'java'

repositories {
  // Repository definition for JCenter Bintray.
  // Needed to download artifacts. Repository
  // definitions are covered later.
  jcenter()
}

dependencies {
  // Define dependency on spring-beans library.
  compile 'org.springframework:spring-beans:4.0.+'
}

当我们执行 dependencies 任务时,我们得到以下输出:

$ gradle -q dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

archives - Configuration for archive artifacts.
No dependencies

compile - Compile classpath for source set 'main'.
\--- org.springframework:spring-beans:4.0.+ -> 4.0.6.RELEASE
 \--- org.springframework:spring-core:4.0.6.RELEASE
 \--- commons-logging:commons-logging:1.1.3

default - Configuration for default artifacts.
\--- org.springframework:spring-beans:4.0.+ -> 4.0.6.RELEASE
 \--- org.springframework:spring-core:4.0.6.RELEASE
 \--- commons-logging:commons-logging:1.1.3

runtime - Runtime classpath for source set 'main'.
\--- org.springframework:spring-beans:4.0.+ -> 4.0.6.RELEASE
 \--- org.springframework:spring-core:4.0.6.RELEASE
 \--- commons-logging:commons-logging:1.1.3

testCompile - Compile classpath for source set 'test'.
\--- org.springframework:spring-beans:4.0.+ -> 4.0.6.RELEASE
 \--- org.springframework:spring-core:4.0.6.RELEASE
 \--- commons-logging:commons-logging:1.1.3

testRuntime - Runtime classpath for source set 'test'.
\--- org.springframework:spring-beans:4.0.+ -> 4.0.6.RELEASE
 \--- org.springframework:spring-core:4.0.6.RELEASE
 \--- commons-logging:commons-logging:1.1.3

我们看到我们项目的所有配置,并且对于每个配置,我们看到定义的依赖项及其传递依赖项。此外,我们还可以看到我们的动态版本 4.0.+ 如何解析为版本 4.0.6.RELEASE。要仅查看特定配置的依赖项,我们可以在 dependencies 任务中使用 --configuration 选项。我们必须使用我们想要查看依赖项的配置的值。在以下输出中,我们看到当我们只想查看编译配置的依赖项时,结果如下:

$ gradle -q dependencies --configuration compile

------------------------------------------------------------
Root project
------------------------------------------------------------

compile - Compile classpath for source set 'main'.
\--- org.springframework:spring-beans:4.0.+ -> 4.0.6.RELEASE
 \--- org.springframework:spring-core:4.0.6.RELEASE
 \--- commons-logging:commons-logging:1.1.3

在 Gradle 中还有一个名为 dependencyInsight 的孵化任务。由于它是孵化状态,Gradle 的功能或语法可能在未来的版本中发生变化。使用 dependencyInsight 任务,我们可以找出为什么特定的依赖项在我们的构建中,以及它属于哪个配置。我们必须使用 --dependency 选项,即必需的选项,并包含依赖项名称的一部分。Gradle 将查找包含 --dependency 选项指定的值的部分组、名称或版本的依赖项。可选地,我们可以指定 --configuration 选项,仅在该指定的配置中查找依赖项。如果我们省略此选项,Gradle 将在我们的项目的所有配置中查找依赖项。

让我们调用 dependencyInsight 任务来查找名称中包含 Spring 且在运行时配置中的依赖项:

$ gradle -q dependencyInsight --dependency spring --configuration runtime
org.springframework:spring-beans:4.0.6.RELEASE

org.springframework:spring-beans:4.0.+ -> 4.0.6.RELEASE
\--- runtime

org.springframework:spring-core:4.0.6.RELEASE
\--- org.springframework:spring-beans:4.0.6.RELEASE
 \--- runtime

在输出中,我们看到版本 4.0.+ 被解析为 4.0.6.RELEASE。我们还看到 spring-beans 依赖项和传递的 spring-core 依赖项是运行时配置的一部分。

访问依赖项

要访问配置,我们可以使用 Gradle 项目对象的 configurations 属性。configurations 属性包含一个 Configuration 对象的集合。记住,Configuration 对象是 FileCollection 的一个实例。因此,我们可以在允许 FileCollection 的构建脚本中引用 ConfigurationConfiguration 对象包含更多我们可以用来访问属于配置的依赖项的属性。

在下一个示例构建中,我们将定义两个任务,这些任务将使用项目配置中可用的文件和信息:

configurations {
  vehicles
  traffic.extendsFrom vehicles
}

task dependencyFiles << {
  // Loop through all files for the dependencies
  // for the traffic configuration, including
  // transitive dependencies.
  configurations.traffic.files.each { file ->
    println file.name
  }

  // We can also filter the files using
  // a closure. For example to only find the files
  // for dependencies with driver in the name.
  configurations.vehicles.files { dep ->
    if (dep.name.contains('driver')) {
      println dep.name
    }
  }

  // Get information about dependencies only belonging
  // to the vehicles configuration.
  configurations.vehicles.dependencies.each { dep ->
    println "${dep.group} / ${dep.name} / ${dep.version}"
  }

  // Get information about dependencies belonging
  // to the traffice configuration and
  // configurations it extends from.
  configurations.traffic.allDependencies.each {  dep ->
    println "${dep.group} / ${dep.name} / ${dep.version}"
  }
}

task copyDependencies(type: Copy) {
  description = 'Copy dependencies from configuration traffic to lib directory'

  // Configuration can be the source for a CopySpec.
  from configurations.traffic

  into "$buildDir/lib"
}

构建脚本依赖项

当我们定义依赖时,我们通常希望为正在开发的代码定义它们。然而,我们可能还想将依赖添加到 Gradle 构建脚本本身。我们可以在构建文件中编写代码,这些代码可能依赖于 Gradle 分发中未包含的库。假设我们想在构建脚本中使用 Apache Commons Lang 库中的一个类。我们必须在我们的构建脚本中添加一个 buildscript 配置闭包。在配置闭包内,我们可以定义仓库和依赖。我们必须使用特殊的 classpath 配置来添加依赖。任何添加到 classpath 配置的依赖都可以在我们的构建文件中的代码中使用。

让我们通过一个示例构建文件来看看它是如何工作的。我们想在 randomString 任务中使用 org.apache.commons.lang3.RandomStringUtils 类。这个类可以在 org.apache.commons:commons-lang3 依赖中找到。我们将它定义为 classpath 配置的外部依赖。我们还在 buildscript 配置块中包含了一个仓库定义,以便可以下载依赖。以下代码展示了这一点:

buildscript {
  repositories {
    // Bintray JCenter repository to download
    // dependency commons-lang3.
    jcenter()
  }

  dependencies {
    // Extend classpath of build script with
    // the classpath configuration.
    classpath 'org.apache.commons:commons-lang3:3.3.2'
  }
}

// We have add the commons-lang3 dependency
// as a build script dependency so we can
// reference classes for Apache Commons Lang.
import org.apache.commons.lang3.RandomStringUtils

task randomString << {
  // Use RandomStringUtils from Apache Commons Lang.
  String value = RandomStringUtils.randomAlphabetic(10)
  println value
}

为了包含 Gradle 分发之外的插件,我们也可以在 buildscript 配置块中使用 classpath 配置。在下一个示例构建文件中,我们将包含 Asciidoctor Gradle 插件:

buildscript {
  repositories {
    // We need the repository definition, from
    // where the dependency can be downloaded.
    jcenter()
  }

  dependencies {
    // Define external module dependency for the Gradle
    // Asciidoctor plugin.
    classpath 'org.asciidoctor:asciidoctor-gradle-plugin:0.7.3'
  }
}

// We defined the dependency on this external
// Gradle plugin in the buildscript {...}
// configuration block
apply plugin: 'org.asciidoctor.gradle.asciidoctor'

可选 Ant 任务依赖

我们可以在 Gradle 中重用现有的 Ant 任务。Ant 的默认任务可以从我们的构建脚本中调用。然而,如果我们想使用可选的 Ant 任务,我们必须为可选 Ant 任务所需的类定义一个依赖。我们创建一个新的依赖配置,然后向这个新配置添加一个依赖。我们可以在设置可选 Ant 任务的类路径时引用这个配置。

让我们添加可选的 Ant 任务 SCP,用于安全地将文件从远程服务器复制到/从服务器。我们创建 sshAntTask 配置以添加可选 Ant 任务的依赖。我们可以为配置选择任何名称。为了定义可选任务,我们使用来自内部 ant 对象的 taskdef 方法。该方法需要一个 classpath 属性,它必须是 sshAntTask 依赖中所有文件的实际路径。Configuration 类提供了 asPath 属性,以平台特定的方式返回文件的路径。因此,如果我们使用 Windows 计算机上的这个属性,文件路径分隔符是 ;,而对于其他平台则是 :。以下示例构建文件包含了定义和使用 SCP Ant 任务的全部代码:

configurations {
  // We define a new dependency configuration.
  // This configuration is used to assign
  // dependencies to, that are needed by the
  // optional Ant task scp.
  sshAntTask
}

repositories {
  // Repository definition to download dependencies.
  jcenter()
}

dependencies {
  // Define external module dependencies
  // for the scp Ant task.
  sshAntTask(group: 'org.apache.ant', 
        name: 'ant-jsch', 
        version: '1.9.4')
}

// New task that used Ant scp task.
task copyRemote(
  description: 'Secure copy files to remote server') << {

  // Define optional Ant task scp.
  ant.taskdef(
    name: 'scp',
    classname: 'org.apache.tools.ant.taskdefs.optional.ssh.Scp',

    // Set classpath based on dependencies assigned
    // to sshAntTask configuration. The asPath property
    // returns a platform-specific string value
    // with the dependency JAR files.
    classpath: configurations.sshAntTask.asPath)

  // Invoke scp task we just defined.
  ant.scp(
    todir: 'user@server:/home/user/upload',
    keyFile: '${user.home}/.ssh/id_rsa',
    passphrase: '***',
    verbose: true) {
    fileset(dir: 'html/files') {
      include name: '**/**'
    }
  }
}

管理依赖

你在本章中已经了解到,我们可以通过将公共部分提取到项目属性中来重构依赖定义。这样,我们只需更改几个项目属性值,就可以对多个依赖项进行更改。在下一个示例构建文件中,我们将使用列表来分组依赖项,并从依赖定义中引用这些列表:

ext {
  // Group is used multiple times, so
  // we extra the variable for re-use.
  def vehiclesGroup = 'com.vehicles'

  // libs will be available from within
  // the Gradle script code, like dependencies {...}.
  libs = [
    vehicles: [
      [group: vehiclesGroup, name: 'car', version: '2.0'],
      [group: vehiclesGroup, name: 'truck', version: '1.0']
    ],
    traffic: [
      [group: 'com.traffic', name: 'pedestrian', version: '1.0']
    ]
  ]
}

configurations {
  vehicles
}

dependencies {
  // Reference ext.libs.vehicles defined earlier
  // in the build script.
  vehicles libs.vehicles
}

Maven 有一个名为依赖管理元数据的功能,允许我们在构建文件的公共部分定义依赖项使用的版本。然后,当实际依赖项配置时,我们可以省略版本,因为它将从构建文件的依赖管理部分确定。Gradle 没有这样的内置功能,但如前所述,我们可以使用简单的代码重构来获得类似的效果。

我们仍然可以在我们的 Gradle 构建中使用 Maven 的声明式依赖管理,通过 Spring 的外部依赖管理插件。此插件向 Gradle 添加了一个dependencyManagement配置块。在配置块内部,我们可以定义依赖元数据,例如组、名称和版本。在我们的 Gradle 构建脚本中的dependencies配置闭包中,我们不再需要指定版本,因为它将通过dependencyManagement配置中的依赖元数据来解析。以下示例构建文件使用此插件并使用dependencyManagement指定依赖元数据:

buildscript {
  repositories {
    // Specific repository to find and download
    // dependency-management-plugin.
    maven {
      url 'http://repo.spring.io/plugins-snapshot'
    }
  }
  dependencies {
    // Define external module dependency with plugin.
    classpath 'io.spring.gradle:dependency-management-plugin:0.1.0.RELEASE'
  }
}

// Apply the external plugin dependency-management.
apply plugin: 'io.spring.dependency-management'
apply plugin: 'java'

repositories {
  // Repository for downloading dependencies.
  jcenter()
}

// This block is added by the dependency-management
// plugin to define dependency metadata.
dependencyManagement {
  dependencies {
    // Specify group:name followed by required version.
    'org.springframework.boot:spring-boot-starter-web' '1.1.5.RELEASE'

    // If we have multiple module names for the same group
    // and version we can use dependencySet.
    dependencySet(group: 'org.springframework.boot',
          version: '1.1.5.RELEASE') {
      entry 'spring-boot-starter-web'
      entry 'spring-boot-starter-actuator'
    }
  }
}

dependencies {
  // Version is resolved via dependencies metadata
  // defined in dependencyManagement.
  compile 'org.springframework.boot:spring-boot-starter-web'
}

要导入一个组织提供的 Maven 物料清单BOM),我们可以在dependencyManagement配置中使用imports方法。在下一个示例中,我们将使用 Spring IO 平台的 BOM。在dependencies配置中,我们可以省略版本,因为它将通过 BOM 来解析:

buildscript {
  repositories {
    // Specific repository to find and download
    // dependency-management-plugin.
    maven {
      url 'http://repo.spring.io/plugins-snapshot'
    }
  }
  dependencies {
    // Define external module dependency with plugin.
    classpath 'io.spring.gradle:dependency-management-plugin:0.1.0.RELEASE'
  }
}

// Apply the external plugin dependency-management.
apply plugin: 'io.spring.dependency-management'
apply plugin: 'java'

repositories {
  // Repository for downloading BOM and dependencies.
  jcenter()
}

// This block is added by the dependency-management
// plugin to define dependency metadata.
dependencyManagement {
  imports {
    // Use Maven BOM provided by Spring IO platform.
    mavenBom 'io.spring.platform:platform-bom:1.0.1.RELEASE'
  }
}

dependencies {
  // Version is resolved via Maven BOM.
  compile 'org.springframework.boot:spring-boot-starter-web'
}

摘要

在本章中,你学习了如何创建和使用依赖配置来分组依赖项。我们看到了如何定义几种类型的依赖项,例如外部模块依赖和内部依赖。

此外,我们还看到了如何在 Gradle 构建脚本中使用classpath配置和buildscript配置向代码中添加依赖项。

最后,我们探讨了使用代码重构和外部依赖管理插件定义依赖项的一些可维护的方法。

在下一章中,我们将学习如何配置存储依赖模块的仓库。

第二章 与仓库协同工作

在上一章中,你学习了如何为你的项目定义依赖项。这些依赖项大多存储在某个仓库或目录结构中。仓库通常有一个结构来支持同一依赖项的不同版本。此外,一些元数据,如模块的其他依赖项,也保存在仓库中。

在我们的构建文件中,我们必须定义依赖项仓库的位置。我们可以混合不同类型的仓库,例如 Maven 和 Ivy。我们甚至可以使用本地文件系统作为仓库。我们将看到如何在构建文件中定义和配置仓库。

此外,Gradle 还提供了配置仓库布局的选项,如果仓库使用自定义布局。我们将学习如何为使用基本身份验证的仓库提供凭据。

声明仓库

如果我们想在 Gradle 构建文件中添加来自仓库的依赖项,我们必须显式添加repositories配置块。在配置块内,我们定义仓库的位置,也许还有一些额外的配置。在以下构建文件的示例中,我们定义了一个具有自定义位置的 Maven 仓库:

// Repositories configuration block,
// must be present to define and
// configure repositories to get
// dependencies in our build script.
repositories {

  // Sample Maven repository with a
  // custom location.
  maven {
    url 'http://mycompany.net/maven'
  }

}

我们可以在构建文件中包含多个仓库。我们甚至可以混合仓库类型,例如,包括 Ivy 仓库和本地文件系统。Gradle 支持以下类型的仓库:

类型 描述
Maven JCenter 仓库 这是一个为 Bintray JCenter 预配置的仓库
Maven 中央仓库 这是一个为 Maven Central 预配置的仓库
Maven 本地仓库 这是一个为本地 Maven 仓库预配置的仓库
Maven 仓库 这是一个待配置的 Maven 仓库,它有一个自定义位置
Ivy 仓库 这是一个待配置的 Ivy 仓库,它有一个位置和布局
平坦目录仓库 这是一个本地文件系统仓库

我们将在稍后看到如何在构建文件中使用这些仓库。认识到 Gradle 将尝试从依赖项中下载所有工件,从找到依赖项模块描述符文件的同一仓库,这是很好的。因此,如果我们已经在构建脚本中定义了多个仓库,Gradle 仍然会使用找到模块描述符文件的第一个仓库来下载工件。

使用 Maven JCenter 仓库

Bintray 的 JCenter 是一个相对较新的公共 Maven 仓库,其中存储了大量的 Maven 开源依赖项。它是 Maven Central 仓库的超集,并且还包含直接发布到 JCenter 的依赖项工件。访问仓库的 URL 是jcenter.bintray.com。Gradle 为 JCenter 提供了一个快捷方式,因此我们不需要在repositories配置块中自己输入 URL。快捷方式是jcenter()

在下面的构建文件示例中,我们使用jcenter()快捷方式定义了对 Bintray 的 JCenter 仓库的引用:

repositories {
  // Define Bintray's JCenter
  // repository, to find
  // dependencies.
  jcenter()
}

自 Gradle 2.1 版本以来,JCenter 仓库 URL 的默认协议是https。如果我们想使用http协议,我们必须为仓库设置url属性。在下一个构建文件示例中,我们将重新定义url属性:

repositories {
  jcenter {
    // By default https is used as protocol,
    // but we can change it with the url
    // property.
    url = 'http://jcenter.bintray.com'
  }
}

可选地,我们可以为仓库定义分配一个名称。这可以适用于所有 Maven 仓库,因为 JCenter 也是一个 Maven 仓库,我们可以设置name属性。在下面的构建文件示例中,我们定义了多个仓库并设置了name属性。我们添加了一个新的任务repositoriesInfo,它将显示每个仓库的nameURL属性:

repositories {
  // Define multiple Maven repositories.
  jcenter()

  jcenter {
    name 'Bintray JCenter legacy'
    url = 'http://jcenter.bintray.com'
  }
}

task repositoriesInfo {
  description 'Display information about repositories'

  doFirst {
    // Access repositories as collection.
    project.repositories.each {
      // Display name and URL for each
      // repository.
      println "'${it.name}' uses URL ${it.url}"
    }
  }
}

当我们运行repositoriesInfo任务时,我们得到以下输出:

$ gradle -q repositoriesInfo
'BintrayJCenter' uses URL https://jcenter.bintray.com/
'Bintray JCenter legacy' uses URL http://jcenter.bintray.com

使用 Maven 中央仓库

我们可以在repositories配置块中配置中央 Maven 2 仓库。Gradle 提供了快捷方式方法mavenCentral。这配置了具有 URL https://repo1.maven.org/maven2/的中央 Maven 仓库。

在下一个构建文件示例中,我们将定义我们的构建的中央 Maven 2 仓库:

repositories {
  // Define central Maven repository
  // to use for dependencies.
  mavenCentral()
}

当我们使用mavenCentral方法时,Gradle 2.1 使用https协议。如果我们想使用http协议,我们可以重新定义url属性并使用http://repo1.maven.org/maven2/地址。在下一个构建文件示例中,我们将重新定义url属性:

repositories {
  mavenCentral(
    // Use http protocol for the
    // central Maven repository.
    url: 'http://repo1.maven.org/maven2/'
  )
}

除了更改url属性外,我们还可以在调用mavenCentral方法时设置一个可选的name属性。在下面的构建脚本示例中,我们为name属性赋值。我们添加了一个新的任务repositoriesInfo,用于显示配置的仓库信息:

repositories {
  // Define multiple Maven repositories.
  mavenCentral()

  mavenCentral(
    name: 'HTTP Maven Central',
    url:  'http://repo1.maven.org/maven2/'
  )
}

task repositoriesInfo {
  description 'Display information about repositories'

  doFirst {
    // Access repositories as collection.
    project.repositories.each {
      // Display name and URL for each
      // repository.
      println "'${it.name}' uses URL ${it.url}"
    }
  }
}

让我们调用repositoriesInfo任务来查看输出:

$ gradle -q repositoriesInfo
'MavenRepo' uses URL https://repo1.maven.org/maven2/
'HTTP Maven Central' uses URL http://repo1.maven.org/maven2/

使用 Maven 本地仓库

如果我们之前在我们的本地计算机上使用过 Maven,那么我们有一个包含已下载组件的本地 Maven 缓存。我们可以使用这个本地缓存作为 Gradle 构建的仓库,使用mavenLocal快捷方式方法。尽管可以使用我们的本地 Maven 缓存,但这样做并不可取,因为它使得构建依赖于本地设置。如果我们在一个由更多开发者组成的大项目上工作,那么我们不能仅依赖每个开发者的计算机上的本地 Maven 缓存作为唯一的仓库。

在下面的构建文件示例中,我们使用mavenLocal快捷方式方法:

repositories {
  // Define the local Maven cache as
  // a repository for dependencies.
  mavenLocal()
}

本地 Maven 缓存的定位方式与 Maven 相同。Gradle 将尝试在USER_HOME/.m2M2_HOME/conf中找到settings.xml文件,其中前者优先于后者。如果找到settings.xml文件,则使用文件中定义的本地 Maven 仓库的位置。如果找不到settings.xml文件,或者本地 Maven 仓库的位置未定义,则默认位置为USER_HOME/. m2/repository

使用 Maven 仓库

我们已经学习了定义 Maven 仓库的快捷方法。如果我们有自己的 Maven 仓库,例如 Nexus 或 Artifactory,我们可以在 repositories 配置块中使用 maven 方法。使用这种方法,我们可以定义 url 属性来访问仓库。我们可以在下面的示例构建脚本中看到这个方法的应用:

repositories {

  // Define a custom Maven repository and
  // set the url property so Gradle can look
  // for the dependency module descripts
  // and artifacts.
  maven {
    url = 'http://ourcompany.com/maven'
    // Alternative syntax is to use
    // the url method:
    // url 'http://ourcompany.com/maven'
  }

}

当 Gradle 在 Maven 仓库中找到模块依赖描述符时,将在这个仓库中搜索这些工件。如果工件存储在其他位置,我们使用 artifactUrls 属性来指定位置。这样,Gradle 将在 url 属性指定的位置查找依赖模块描述符,并在 artifactUrls 属性指定的位置查找工件。

下一个示例构建脚本将定义一个自定义的 Maven 仓库,并为工件指定多个位置:

repositories {
  maven {
    // At this location at the least the
    // dependency module descriptor files
    // must be stored.
    url 'http://ourcompany.com/maven'

    // Define extra locations where artifacts
    // can be found and downloaded.
    artifactUrls 'http://ourcompany.com/jars'
    artifactUrls 'http://ourcompany.com/lib'

    // Alternative syntax is to use the
    // artifactUrls property assignment:
    // artifactUrls = [
    //   'http://ourcompany.com/jars', 'http://ourcompany.com/lib'
    // ]
  }
}

如果我们已经配置了带有基本身份验证的自定义 Maven 仓库,我们必须提供用户名和密码来访问该仓库。在我们的 Gradle 构建文件中,我们在 maven 配置的 credentials 块中设置用户名和密码。让我们首先将用户名和密码添加到构建文件中,然后稍后看看我们如何将这些属性外部化。下一个示例构建文件将使用 credentials 配置块:

repositories {
  maven {
    url 'http://ourcompany.com/maven'

    // Here we assign the username and
    // password to access the repository.
    credentials {
      username = 'developer'
      password = 'secret'

      // Alternate syntax is to use
      // the username and password
      // methods.
      // username 'developer'
      // password 'secret'
    }
  }
}

将用户名和密码添加到构建文件中不是一个好主意,因为这个文件是与参与我们项目的所有开发者共享的。我们使用项目属性来解决这个问题,而不是使用硬编码的用户名和密码。项目属性的值可以通过命令行使用 -P--project-prop 选项来设置。或者,我们可以在项目目录中创建 gradle.properties 文件,包含项目属性的名称和值。gradle.properties 文件不应放入我们项目的版本控制系统中,这样值对开发者来说是私有的。

下面的示例构建文件使用 mavenUsername 项目属性和 mavenPassword 作为 Maven 仓库的凭证:

repositories {
  maven {
    name = 'Company Maven Repository'

    url 'http://ourcompany.com/maven'

    // Check that properties mavenUsername
    // and mavenPassword are set when
    // we run the script.
    verifyProperty('mavenUsername')
    verifyProperty('mavenPassword')

    credentials {
      // Use project properties instead
      // of hard coded username and password.
      username mavenUsername
      password mavenPassword
    }
  }
}

/**
* Helper method to check if project property
* with the given name is set.
*
* @param propertyName Name of the property to check
* @throws GradleException When property is not set.
*/
void verifyProperty(final String propertyName) {
  if (!hasProperty(propertyName)) {
    throw new GradleException("Property $propertyName must be set")
  }
}

当我们为此脚本执行任何任务时,我们应该通过命令行提供项目属性的值:

$ gradle -PmavenUsername=developer -PmavenPassword=secret

或者,我们可以在项目目录中创建 gradle.properties 文件,内容如下:

mavenUsername = developer
mavenPassword = secret

如果我们有多个使用相同自定义 Maven 仓库的项目,我们还可以创建一个带有正确凭证的 Gradle init 脚本。Gradle init 脚本在构建开始之前运行。在脚本中,我们希望为具有特定名称的 Maven 仓库设置凭证。使用 init 脚本有几种方法:

  • 我们可以直接从命令行使用带有 -I--init-script 选项的 init 脚本。在这里,我们指定 init 脚本的名字。

  • 我们将 init.gradle 文件放在 USER_HOME/.gradle 目录中。这个文件在我们计算机上每次执行 Gradle 构建之前都会运行。

  • 我们在USER_HOME/.gradle/init.d目录中放置了一个以.gradle扩展名的文件。所有来自此目录的 Gradle 初始化脚本都会在每次构建之前运行。

  • 我们在GRADLE_HOME/init.d目录中放置了一个以.gradle扩展名的文件。这样,我们可以打包一个带有始终需要执行的初始化脚本的自定义 Gradle 发行版。

让我们看看下一个示例初始化脚本文件中的初始化脚本内容:

// Run for all projects.
allprojects {

  // After the project is evaluated, we can access
  // the repository by name.
  afterEvaluate { project ->

    // Check if project contains a repository
    // with the given name.
    if (project.repositories.any { it.name == 'Company Maven Repository' }) {

      // Set credentials for custom Maven repository
      // with the given name.
      configure(project.repositories['Company Maven Repository']) {
        credentials {
          username 'developer'
          password 'secret'
        }
      }

    }

  }

}

我们必须更改我们的项目 Gradle 构建文件,因为凭证现在是通过初始化脚本设置的。我们将从项目构建文件中删除凭证。在下一个示例构建文件中,我们将删除凭证和辅助方法,以设置凭证属性。凭证由初始化脚本设置。以下代码显示了这一点:

repositories {
  maven {
    name = 'Company Maven Repository'
    url 'http://ourcompany.com/maven'

    // Credentials are set via init script.
  }
}

使用平面目录仓库

Gradle 还允许使用目录作为仓库来解决依赖项。我们可以使用flatDir方法指定一个或多个目录。可选地,我们可以为仓库指定一个名称。在下一个示例构建文件中,我们指定了libjars目录作为仓库:

repositories {

  // Define the directories jars and lib
  // to be used as repositories.
  flatDir {
    name 'Local lib directory'
    dirs "${projectDir}/jars", "${projectDir}/lib"
  }

  // Alternate syntax is using a Map
  // with the flatDir method.
  // flatDir name: 'Local lib directory',
  //         dirs: ["${projectDir}/jars", "${projectDir}/lib"]

}

当我们使用平面目录仓库时,Gradle 根据工件名称和版本解析依赖项工件。依赖项的组部分被忽略。如果我们只在项目中使用平面目录仓库,我们甚至可以在配置依赖项时省略组部分。Gradle 使用以下规则来解析依赖项:

  • [artifact]-[version].[ext]

  • [artifact]-[version]-[classifier].[ext]

  • [artifact].[ext]

  • [artifact]-[classifier].[ext]

在下一个示例构建文件中,我们将定义一个平面目录仓库和一个单一依赖项:

repositories {
  flatDir name: 'Local lib directory',
      dirs: ["${projectDir}/lib"]
}

dependencies {
  traffic group: 'com.traffic', name: 'pedestrian',
      version: '1.0', classifier: 'jdk16'
}

Gradle 将在lib目录中解析以下文件;使用第一个匹配的文件:

  • pedestrian-1.0.jar

  • pedestrian-1.0-jdk16.jar

  • pedestr``ian.jar

  • pedestrian-jdk16.jar

使用 Ivy 仓库

Ivy 仓库允许自定义和灵活的仓库布局模式。Gradle 支持 Ivy 仓库,我们可以在我们的 Gradle 构建脚本中配置仓库布局模式。要定义一个 Ivy 仓库,我们在repositories配置块中使用ivy方法。

在以下示例构建文件中,我们定义了一个标准的 Ivy 仓库,并且我们还为仓库设置了可选的name属性:

repositories {

  // Define an Ivy repository with
  // a URL and optional name property.
  ivy {
    name 'Ivy Repository'
    url 'http://ourompany.com/repo'
  }

}

Ivy 仓库的布局定义了用于搜索模块依赖元数据和依赖项的模式的布局。我们可以在我们的构建脚本中使用一些预定义的布局。在先前的示例构建文件中,我们没有指定布局。然后 Gradle 将使用默认的gradle布局。下表显示了我们可以使用的不同布局名称,它们用于查找 Ivy 元数据 XML 文件和依赖项的工件:

名称 Ivy 模式 工件模式
gradle [organisation]/[module]/[revision]/ivy-[revision].xml [organisation]/[module]/[revision]/[artifact]-revision(.[ext])
maven [organisation]/[module]/[revision]/ivy-[revision].xml [organisation]/[module]/[revision]/[artifact]-revision(.[ext])
ivy [organisation]/[module]/[revision]/[type]s/artifact [organisation]/[module]/[revision]/[type]s/artifact

organisation 中的 . 被替换为 /

要使用布局,我们在 ivy 配置中使用 layout 方法。例如,在下一个构建脚本中,我们使用 mavenivy 布局:

repositories {

  ivy {
    // Set layout to maven.
    layout 'maven'
    name 'Ivy repository Maven layout'
    url 'http://ourcompany.com/repo1'
  }

  ivy {
    // Set layout to ivy.
    layout 'ivy'
    name 'Ivy repository'
    url 'http://ourcompany.com/repo'
  }

}

要为 Ivy XML 文件和工件定义自定义模式,我们使用 pattern 布局。使用此布局,我们使用 Ivy 定义的令牌定义自己的模式。在以下表中,我们可以看到可以用来构建模式的令牌:

令牌 描述
[organisation] 这是组织名称。
[orgPath] 这是组织名称,其中 . 已被替换为 /。这可以用来配置类似 Maven2 的仓库。
[module] 这是模块名称。
[branch] 这是分支名称。
[revision] 这是修订名称。
[artifact] 这是工件名称(或 ID)。
[type] 这是工件类型。
[ext] 这是工件文件扩展名。
[conf] 这是配置名称。
[originalname] 这是原始工件名称(包括扩展名)。

指定一个可选的令牌时,我们将令牌用括号(())括起来。如果括号中定义的令牌为空或为空字符串,则忽略该令牌。例如,artifact.[ext] 模式在未设置 revision 时将接受 artifact.jar,如果设置了 revision,则接受 artifact-1.1.jar

我们通过指定 pattern 名称并在其中添加一个配置块来定义 Ivy XML 文件和工件的模式来在我们的构建脚本中定义自定义布局。如果我们没有为 Ivy XML 文件指定特殊模式,则使用工件模式。我们需要定义至少一个工件模式。这些模式附加到仓库的 url 属性。可选地,我们可以设置 pattern 布局属性 m2compatible。如果值为 true,则 [organisation] 令牌中的 . 被替换为 /

在下一个示例构建脚本中,我们将定义两个具有自定义布局的新仓库:

repositories {

  ivy {
    url 'http://ourcompany.com/repo'

    // Here we define a custom pattern
    // for the artifacts and Ivy XML files.
    layout('pattern') {
      // Define pattern with artifact method.
      // This pattern is used for both Ivy XML files
      // and artifacts.
      artifact '[module]/[type]/[artifact]-[revision].[ext]'
    }
  }

  ivy {
    url 'http://ourcompany.com/repo1'

    layout('pattern') {
      // We can define multiple patterns. 
      // The order of the definitions
      // defines search path.
      artifact 'binaries/[organisation]/[module]/[artifact]-[revision].[ext]'
      artifact 'signed-jars/[organisation]/[module]/[artifact]-[revision].[ext]'

      // Seperate definition for Ivy XML files 
      // with ivy method.
      ivy '[organisation]/[module]/metadata/ivy-[revision].xml'
    }
  }

}

定义自定义模式的另一种语法是在 ivy 配置块中使用 artifactPatternivyPattern。我们不需要使用 layout 方法进行此定义。如果我们没有指定 ivyPattern,则使用 artifactPattern 定义的模式来查找 Ivy XML 文件。在以下示例构建脚本中,我们重写了上一个示例构建文件中的仓库定义:

repositories {

  ivy {
    url 'http://ourcompany.com/repo'

    // Define pattern with artifact method.
    // This pattern is used for both Ivy XML files
    // and artifacts.
    artifactPattern '[module]/[type]/[artifact]-[revision].[ext]'
  }

  ivy {
    url 'http://ourcompany.com/repo1'

    // We can define multiple patterns. The order of the definitions
    // defines search path.
    artifactPattern 'binaries/[organisation]/[module]/[artifact]-[revision].[ext]'
    artifactPattern 'signed-jars/[organisation]/[module]/[artifact]-[revision].[ext]'

    // Seperate definition for Ivy XML files with ivy method.
    ivyPattern '[organisation]/[module]/metadata/ivy-[revision].xml'
  }

}

要为具有基本身份验证的 Ivy 仓库指定用户名和密码,我们使用 credentials 方法,就像我们在 Maven 仓库中所做的那样。在下一个示例构建文件中,我们将设置访问 Ivy 仓库的凭证。查看有关 Maven 仓库的部分,了解我们如何外部化用户名和密码,以便它们不是构建脚本代码的一部分。以下代码显示了这一点:

repositories {
  ivy {
    url 'http://ourcompany.com/repo'

    // Here we assign the username and
    // password to access the repository.
    credentials {
      username = 'developer'
      password = 'secret'

      // Alternate syntax is to use
      // the username and password
      // methods.
      // username 'developer'
      // password 'secret'
    }
  }
}

使用不同的协议

Maven 和 Ivy 仓库可以通过多种协议访问。我们已经了解到我们可以使用 httphttps 协议。然而,我们也可以使用 filesftp 协议。当我们使用 sftp 协议时,我们必须提供凭证。file 协议不支持身份验证。

下一个示例构建文件将使用 filesftp 协议来定义 Maven 和 Ivy 仓库:

repositories {
  ivy {
    // Use file protocol, for example an
    // accessible network share or local directory.
    url 'file://Volumes/shared/developers/repo'
  }

  maven {
    url 'sftp://ourcompany.com:22/repo'

    // With the sftp protocol we must provide
    // the username and password.
    credentials {
      username 'developer'
      password 'secret'
    }
  }
}

摘要

在本章中,你学习了如何在 Gradle 构建脚本中定义仓库。你看到了如何使用预定义的快捷方法:jcentermavenCentralmavenLocal。要访问位于自定义位置的 Maven 仓库,我们可以使用 url 属性和 maven 方法。当我们配置 Ivy 仓库时,我们拥有最大的控制权。我们可以指定一个 URL,也可以指定仓库的布局模式。你了解到你还可以在你的构建脚本中使用平面目录仓库。

你看到了如何为具有基本身份验证的仓库提供凭证。现在你知道了如何将用户名和密码保存在构建脚本之外。最后,你学习了如何使用不同的传输协议来访问仓库。

在下一章中,我们将看到 Gradle 将如何解析依赖项。

第三章 解析依赖项

在第一章中,定义依赖项,你学习了如何将依赖项添加到你的项目中。我们看到了指定依赖项的不同方法,例如模块或项目依赖项。在前一章中,我们探讨了如何定义托管我们依赖项的仓库。Gradle 将使用这些信息来进行实际的依赖项解析。在本章中,我们将看到 Gradle 是如何解析依赖项的。

Gradle 在解决版本冲突方面与其他构建工具不同,因此我们将仔细观察依赖项解析时发生的情况。我们将看到如何在 Gradle 中自定义解析过程,以便我们可以获得我们想要的精确依赖项,并实现可靠和可重复的构建。

理解依赖解析

Gradle 将使用repositoriesdependencies配置块中的信息来收集和下载所有依赖项。这个过程也被称为依赖解析。Gradle 在解析依赖项时采取以下步骤:

  1. 在定义的仓库中搜索依赖项的模块描述符文件。使用仓库定义的顺序进行搜索。因此,先于其他仓库定义的仓库首先被搜索,依此类推。如果找到了 POM 或 Ivy 描述符文件,则使用它。如果没有找到描述符文件,则搜索依赖项的工件文件。如果找到了描述符文件或工件文件,那么 Gradle 就知道这个仓库可以用来下载依赖项。

    • 如果找到一个带有父 POM 描述符文件的 POM 描述符文件,则 Gradle 将解析父 POM。

    • 动态版本,如4.1.+,解析为仓库中可用的最高静态版本。例如,如果仓库包含版本4.1.04.1.1,则使用4.1.1版本。

  2. Gradle 将根据以下标准确定哪个仓库最适合使用:

    • 模块描述符文件,如 POM 和 Ivy 描述符文件,比仅包含工件文件的文件更受欢迎。

    • 优先考虑在早期仓库中找到的依赖项,而不是在后期仓库中找到的依赖项。

    • 如果使用了类似2.+的动态版本,则更倾向于使用比它低的静态版本。

  3. 模块依赖项的工件是从 Gradle 选择的仓库下载的。这意味着工件不是从与定义的依赖项的描述符文件或工件文件不同的仓库下载的。

如果一个依赖项使用静态版本定义,并且 Gradle 在仓库中找到了这个依赖项的模块描述符文件,那么对这个依赖项的搜索就完成了,其他仓库将不会用于搜索。这个过程无法找到更好的仓库候选者,因此对于这个依赖项的依赖项解析就完成了。

配置传递依赖项

大多数情况下,我们项目中的依赖项也依赖于其他库。因此,这些依赖项有自己的依赖项。这些被称为传递依赖项。Gradle 必须能够解析传递依赖项。

在以下示例构建文件中,我们使用版本 1.1.2 和组名 ch.qos.logback 定义了 logback-classic 模块依赖项:

apply plugin: 'java'

repositories.jcenter()

dependencies {
  // Dependency definition for Logback classic
  // library, used as implementation for SLF4J API.
  compile 'ch.qos.logback:logback-classic:1.1.2'
}

当我们运行 Gradle 的 dependencies 任务时,我们可以看到我们定义的 logback-classic 依赖项依赖于 ch.qos.logback:logback-core:1.1.2org.slf4j:slf4j-api:1.7.6。以下代码显示了这一点:

$ gradle -q dependencies --configuration compile
------------------------------------------------------------
Root project
------------------------------------------------------------

compile - Compile classpath for source set 'main'.
\--- ch.qos.logback:logback-classic:1.1.2
 +--- ch.qos.logback:logback-core:1.1.2
 \--- org.slf4j:slf4j-api:1.
7.6

禁用传递依赖项

如果我们不想在我们的项目中包含传递依赖项,我们必须重新配置依赖项或配置。使用 Gradle,我们有不同的方法来禁用依赖项的传递行为。首先,我们可以在依赖定义中添加一个配置闭包,使用 transitive 属性,并将其值设置为 false。默认情况下,所有依赖项都被视为传递依赖项,正如我们在示例构建文件中看到的那样。

在以下示例构建文件中,我们指定我们想要将 logback-classic 依赖项视为非传递依赖项:

apply plugin: 'java'

repositories.jcenter()

dependencies {
  // Dependency definition for Logback classic.
  compile 'ch.qos.logback:logback-classic:1.1.2', {
    // We don't want to have the transitive dependencies.
    transitive = false
  }
}

如果我们再次运行 dependencies 任务,我们可以在输出中看到传递依赖项不再被解析:

$ gradle -q dependencies --configuration compile

------------------------------------------------------------
Root project
------------------------------------------------------------

compile - Compile classpath for source set 'main'.
\--- ch.qos.logback:logback-classic:1.1.2

我们还可以完全禁用依赖项配置中的传递依赖项。这意味着使用该配置定义的任何依赖项都不会有传递依赖项。配置中的单个依赖项可以在配置闭包中使用 transitive 属性来再次启用该依赖项的传递行为。要完成此操作,请执行以下步骤:

  1. 首先,我们将在下一个示例构建文件中禁用 compile 配置的传递依赖项:

    apply plugin: 'java'
    
    repositories.jcenter()
    
    configurations {
      // Disable transitive dependencies for
      // all dependencies defined in this
      // configuration.
      // Configurations extended
      // from the compile configuration will not
      // inherit this transitive property value.
      compile.transitive = false
    }
    
    dependencies {
      // Dependency definition for Logback classic
      compile 'ch.qos.logback:logback-classic:1.1.2'
    }
    
  2. 接下来,我们将执行 dependencies 任务,并看到传递依赖项不再被解析:

    $ gradle -q dependencies --configuration compile
    
    ------------------------------------------------------------
    Root project
    ------------------------------------------------------------
    
    compile - Compile classpath for source set 'main'.
    \--- ch.qos.logback:logback-classic:1.1.2
    

排除传递依赖项

我们还可以对传递依赖项有更精细的控制。我们可以在依赖定义中排除某些传递依赖项。这样,我们可以选择只使用某些传递依赖项,而排除其他依赖项。要定义我们想要排除的传递依赖项,我们使用依赖项配置闭包中的 exclude 方法。

让我们看看我们如何包含 logback-core 传递依赖项,但移除 slf4j-api 依赖项。我们在配置闭包中使用 exclude 方法。exclude 方法接受一个 Map 作为参数,其中包含一个或两个键:modulegroup。在以下构建文件中,我们包含 logback-core 传递依赖项:

apply plugin: 'java'

repositories.jcenter()

dependencies {
  // Dependency definition for Logback classic
  compile('ch.qos.logback:logback-classic:1.1.2') {
    // Exclude slf4j-api transitive dependency.
    exclude module: 'slf4j-api'
    // Alternative syntax:
    // Exclude all modules in the group org.slf4j:
    // exclude group: 'org.slf4j'
    // Or specify both group and module name:
    // exclude group: 'org.slf4j', module: 'slf4j-api'
  }
}

我们执行 dependencies 任务以查看我们的配置定义是否具有预期的效果:

$ gradle -q dependencies --configuration compile

------------------------------------------------------------
Root project
------------------------------------------------------------

compile - Compile classpath for source set 'main'.
\--- ch.qos.logback:logback-classic:1.1.2
 \--- ch.qos.logback:logback-core:1.1.2

注意,在输出中,传递依赖项 org.slf4j:slf4j-api:1.7.6 已不再是我们的传递依赖项的一部分。

我们还可以在单个依赖项之外对配置设置排除规则。配置上的排除规则将用于配置中定义的所有依赖项。在下一个示例 Gradle 构建文件中,我们将从 compile 配置中排除 slf4j-api 模块:

apply plugin: 'java'

repositories.jcenter()

configurations {
  compile {
    // Exclude slf4j-api transitive dependency.
    exclude module: 'slf4j-api'
    // Alternative syntax:
    // Exclude all modules in the group org.slf4j:
    // exclude group: 'org.slf4j'
    // Or specify both group and module name:
    // exclude group: 'org.slf4j', module: 'slf4j-api'
  }

  // To exclude a module and/or group from all configurations
  // we can use the all method:
  // all { exclude module: 'slf4j-api' }
}

dependencies {
  // Dependency definition for Logback classic.
  compile('ch.qos.logback:logback-classic:1.1.2')
}

我们添加到配置或依赖项中的任何排除规则都可以通过相应对象的 excludeRules 属性再次访问。我们可以使用此属性来找出负责排除特定依赖项的配置或依赖项。在以下示例构建文件中,我们创建了一个新的任务 showExcludeRules,其中我们遍历所有配置和依赖项并收集排除规则。在任务的末尾,我们将所有信息打印到标准输出。以下代码显示了这一点:

apply plugin: 'java'

repositories.jcenter()

configurations {
  compile {
    exclude module: 'slf4j-api'
  }
}

dependencies {
  compile('ch.qos.logback:logback-classic:1.1.2') {
    exclude group: 'ch.qos.logback', module: 'logback-core'
  }
}

task showExcludeRules {
  description 'Show exclude rules for configurations and dependencies'

  doFirst {
    // Store found exclude rules.
    def excludes = []

    // Go through all configurations to find exclude rules
    // defined at configuration level and at
    // dependency level for dependencies in the configuration.
    configurations.all.each { configuration ->
      def configurationExcludes = configuration.excludeRules
      configurationExcludes.findAll().each { rule ->
        // Add found excludeRule to excludes collection.
        excludes << [type: 'container',
              id: configuration.name,
              excludes: rule]
      }

      def dependencies = configuration.allDependencies
      dependencies.all { dependency ->
        def excludeRules = dependency.excludeRules

        excludeRules.findAll().each { rule ->
          def dep = dependency
          def id = "${dep.group}:${dep.name}:${dep.version}"
          // Add found excludeRule to excludes collection.
          excludes << [type: 'dependency', id: id, excludes: rule]
        }
      }
    }

    // Printing exclude rule information for output.
    def printExcludeRule = {
      def rule = "${it.excludes.group ?: '*'}:${it.excludes.module ?: '*'}"
      println "$it.id >> $rule"
    }

    // Print formatted header for output.
    def printHeader = { header ->
      println()
      println '-' * 60
      println header
      println '-' * 60
    }

    // Group rules by organisation or dependency.
    def excludeRules = excludes.groupBy { it.type }

    printHeader 'Configurations'
    excludeRules.container.toSet().each(printExcludeRule)

    printHeader 'Dependencies'
    excludeRules.dependency.toSet().each(printExcludeRule)
  }
}

当我们运行任务时,我们得到以下输出:

$ gradle -q showExcludeRules

------------------------------------------------------------
Configurations
------------------------------------------------------------
compile >> *:slf4j-api

------------------------------------------------------------
Dependencies
------------------------------------------------------------
ch.qos.logback:logback-classic:1.1.2 >> ch.qos.log
back:logback-core

使用仅软件包依赖项

最后,如果我们知道我们只想包含依赖项中的一个软件包,我们可以使用 ext 属性来为外部模块依赖项。使用此属性,不会解决传递依赖项,因为我们指定了我们特别想要由 ext 属性指定的软件包。

在我们的示例中,我们可以使用具有 jar 值的 ext 属性来仅解决 logback-classic 依赖项的 JAR 软件包。在下一个示例构建文件中,我们将使用 ext 属性为我们的 logback-classic 依赖项:

apply plugin: 'java'

repositories.jcenter()

dependencies {
  // Dependency definition for Logback classic library
  compile 'ch.qos.logback:logback-classic:1.1.2@jar'

  // Alternative syntax:
  //compile group: 'ch.qos.logback',
  //        name: 'logback-classic',
  //        version: '1.1.2',
  //        ext: 'jar'
}

解决版本冲突

我们之前的示例很简单,只包含一个依赖项和一些传递依赖项。当我们向我们的项目添加更多依赖项,或者有一个多模块项目,其中每个项目都有很多依赖项时,可能会发生相同或传递依赖项被多次包含的情况。Gradle 会检测到这一点,并确保依赖项只下载一次。我们将在 Gradle 中稍后看到关于高级依赖项缓存管理的更多内容。

当同一个依赖被多次包含但版本不同时,问题就开始了。应该使用哪个版本的依赖?这正是 Gradle 的解析策略发挥作用的地方。下表显示了 Gradle 具有的解析策略:

名称 描述
最新版 使用冲突依赖项的最新版本。这是 Gradle 使用的默认策略。如果冲突依赖项的版本向后兼容,则此策略有效。
失败 当依赖项存在版本冲突时,构建过程会失败。我们必须明确地在我们的构建文件中添加代码来解决版本冲突。我们将在本章后面看到如何自定义解析策略来显式解决版本冲突。

使用最新版本

让我们看看如果我们有一个版本冲突并使用 Gradle 的默认解决策略会发生什么。Gradle 将使用具有版本冲突的依赖项的最新版本。为了完成此操作,请执行以下步骤:

  1. 在下一个构建文件中,我们在compile配置中定义了对slf4j-api的依赖,在runtime配置中定义了对logback-classic的依赖:

    apply plugin: 'java'
    
    repositories.jcenter()
    
    dependencies {
      // Define dependency on SLF4J API for
      // compiling source files.
      compile 'org.slf4j:slf4j-api:1.7.7'
    
      // Define implementation Logback classic
      // of SLF4J API in runtime configuration.
      // This has a transitive dependency on
      // org.slf4j:slf4j-api:1.7.6, which is a version
      // conflict with org.slf4j:slf4j-api:1.7.7
      runtime 'ch.qos.logback:logback-classic:1.1.2'
    }
    
  2. 我们运行dependencies任务以查看使用了哪些依赖项的版本。以下输出显示,logback-classic的传递依赖项org.slf4j:slf4j-api:1.7.6已更改,因此使用了定义在compile配置中的版本1.7.7

    $ gradle -q dependencies --configuration runtime
    
    ------------------------------------------------------------
    Root project
    ------------------------------------------------------------
    
    runtime - Runtime classpath for source set 'main'.
    +--- org.slf4j:slf4j-api:1.7.7
    \--- ch.qos.logback:logback-classic:1.1.2
     +--- ch.qos.logback:logback-core:1.1.2
     \--- org.slf4j:slf4j-api:1.7.6 -> 1.7.7
    
    (*) - dependencies omitted (listed previously)
    
    

    注意到行org.slf4j:slf4j-api:1.7.6 → 1.7.7,它直观地显示了该依赖项的版本已从1.7.6增加到1.7.7

  3. dependencies任务显示了依赖项和传递依赖项的层次树视图。要从一个特定的依赖项获取视图,并查看它是如何进入依赖项图的,我们使用dependencyInsight任务。使用此任务,我们可以看到依赖项是如何解决的,以及是否发生了任何冲突解决。

  4. 当我们从命令行调用dependencyInsight任务时,我们必须使用以下两个选项:

    1. 我们使用--configuration选项指定依赖项的配置。

    2. 然后,我们使用--dependency选项来指定依赖项的名称。

  5. 依赖项的名称不必是全名;我们甚至可以使用名称的一部分。例如,我们可以使用org.slf4j:slf4j-apislf4j-apislf4j来深入了解一个依赖项。

  6. 我们执行dependencyInsight任务以查看示例构建文件中slf4j-api依赖项的更多信息:

    $ gradle -q dependencyInsight --configuration runtime --dependency slf4j-api
    org.slf4j:slf4j-api:1.7.7 (conflict resolution)
    \--- runtime
    
    org.slf4j:slf4j-api:1.7.6 -> 1.7.7
    \--- ch.qos.logback:logback-classic:1.1.2
     \--- runtime
    
    

    在输出中,我们看到org.slf4j:slf4j-api:1.7.7依赖项已为runtime配置解决,并且已对该依赖项进行了冲突解决。在下一行中,我们将看到org.slf4j:slf4j-api:1.7.6的传递依赖项版本已从1.7.6增加到1.7.7dependencyInsight任务已经告诉我们更多关于应用的依赖项解决信息。我们可能会从使用dependencies任务进行的大致概述开始,如果我们想获取有关特定依赖项的更多信息,我们将使用dependencyInsight任务。

  7. 我们还可以使用另一个任务,该任务将dependenciesdependencyInsight任务结合起来。htmlDependencyReport任务是project-report插件的一部分。使用此任务,我们可以获得一个 HTML 报告,显示所有依赖项,并且我们可以点击依赖项以获取更多信息。要使用此任务,我们首先将project-report插件添加到我们的示例项目文件中。以下代码显示了这一点:

    apply plugin: 'java'
    apply plugin: 'project-report'
    
    repositories.jcenter()
    
    dependencies {
      compile 'org.slf4j:slf4j-api:1.7.7'
    
      runtime 'ch.qos.logback:logback-classic:1.1.2'
    }
    
  8. 我们为这个构建文件执行htmlDependencyReport任务。以下代码显示了这一点:

    $ gradle htmlDependencyReport
    :htmlDependencyReport
    
    BUILD SUCCESSFUL
    
    Total time: 1.645 secs
    $
    
    
  9. 在任务执行后,build/reports/project/dependencies/目录中会创建新的文件。

  10. 当我们在网页浏览器中打开 index.html 文件时,我们可以看到我们项目的名称。如果我们有一个多模块项目,我们在这里会看到所有项目的名称。我们可以点击名称并获取所有配置的概述。在下一张截图中,我们看到我们项目中所有配置的概述:使用最新版本

  11. 当我们点击 runtime 配置链接时,所有依赖项都会显示出来。我们可以看到存在版本冲突,因为 org.sfl4j:slf4j-api:1.7.6 依赖项的颜色是橙色。这个视图就是我们通过命令行调用依赖项任务时看到的:使用最新版本

  12. 要获取依赖项洞察视图,我们点击 org.sfl4j:slf4j-api:1.7.6 → 1.7.7 链接。在我们的网页浏览器中打开一个弹出窗口,我们看到以下截图:使用最新版本

现在,我们看到如果我们从命令行运行 dependencyInsight 任务时通常会看到的内容。

htmlDependencyReport 非常有用,可以获取我们项目中依赖项的图形化和交互式视图。通过在生成的 HTML 报告中点击它,我们也可以轻松地获取更多关于依赖项的详细信息。

在版本冲突时失败

如果默认的 Gradle 解析策略(使用(传递性)依赖项的最新版本)不能解决问题,我们可以选择在版本冲突时让构建失败。为了再次成功运行构建,我们必须明确在我们的构建文件中解决版本冲突。

在以下示例构建文件中,我们为 runtime 配置配置了解决策略,如果存在版本冲突则失败。resolutionStrategy 方法接受一个配置闭包,在其中我们调用 failOnVersionConflict 方法。以下代码展示了这一点:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      // If there is a version conflict,
      // then the build must fail.
      failOnVersionConflict()
    }
  }

  // Alternatively we could apply
  // this to all configurations:
  // all {
  //     resolutionStrategy {
  //         failOnVersionConflict()
  //     }
  // }
}

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7'

  runtime 'ch.qos.logback:logback-classic:1.1.2'
}

构建现在配置为在版本冲突时失败。我们知道在本章前面的示例中,slf4j-api 上存在版本冲突。我们现在执行 dependencies 任务来看看会发生什么:

$ gradle -q dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

runtime - Runtime classpath for source set 'main'.

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':dependencies'.
> Could not resolve all dependencies for configuration ':runtime'.
 > A conflict was found between the following modules:
 - org.slf4j:slf4j-api:1.7.7
 - org.slf4j:slf4j-api:1.7.6

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

我们看到这次构建失败了。在输出中,我们看到原因。org.slf4j:slf4j-api:1.7.7org.slf4j:slf4j-api:1.7.6 模块之间存在冲突。

强制版本

我们可以强制 Gradle 在我们的项目中使用特定版本的依赖项。这个依赖项也可以是传递性的。我们使用依赖项的配置闭包,并将 force 属性设置为 true。这指示 Gradle 依赖项解析过程始终使用指定的版本,即使依赖项是依赖图中的传递性依赖项。

在我们的示例构建文件中,我们有一个版本冲突。我们可以通过强制 Gradle 使用 org.slf4j:slf4j-api 依赖项的版本 1.7.7 来修复这个问题。以下示例构建文件应用了 force 属性:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      failOnVersionConflict()
    }
  }
}

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7', {
    // Force Gradle to use this version
    // for this dependency (even transtive).
    force = true
  }

  runtime 'ch.qos.logback:logback-classic:1.1.2'
}

让我们运行 dependencies 任务来看看版本冲突是否已经解决:

$ gradle -q dependencies --configuration runtime

------------------------------------------------------------
Root project
------------------------------------------------------------

runtime - Runtime classpath for source set 'main'.
+--- org.slf4j:slf4j-api:1.7.7
\--- ch.qos.logback:logback-classic:1.1.2
 +--- ch.qos.logback:logback-core:1.1.2
 \--- org.slf4j:slf4j-api:1.7.6 -> 1.7.7

(*) - dependencies omitted (listed previously)

我们已经解决了版本冲突,构建现在再次成功。我们还可以在输出中看到,对于 org.slf4j:slf4j-api:1.7.6 传递依赖项,版本现在是 1.7.7

除了在依赖项配置中设置 force 属性外,我们还可以在 configurations 配置块的 resolutionStrategy 方法中作为依赖项的一部分强制设置版本。我们使用 force 方法添加具有强制版本的依赖项。或者,我们可以使用 forcedModules 属性来定义所有强制依赖项。这可能是一个更好的解决方案,因为我们可以在 resolutionStrategy 配置闭包中将具有强制版本的多个依赖项放在一起,从而使构建文件更易于阅读和维护。

在下一个示例构建文件中,我们将强制 org.slf4j:slf4j-api 依赖项的版本为 1.7.7,但这次作为 resolutionStrategy 配置的一部分:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      failOnVersionConflict()

      // Make sure version 1.7.7 is used for
      // (transitive) dependency org.slf4j:slf4j-api.
      force 'org.slf4j:slf4j-api:1.7.7'

      // Alternate syntax is to define the
      // forced module collection.
      // forcedModules = ['org.slf4j:slf4j-api:1.7.7']
    }
  }
}

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7'

  runtime 'ch.qos.logback:logback-classic:1.1.2'
}

当我们从命令行执行 dependencies 任务时,我们看到所有 org.slf4j:slf4j-api 依赖项都使用了版本 1.7.7

$ gradle -q dependencies --configuration runtime

------------------------------------------------------------
Root project
------------------------------------------------------------

runtime - Runtime classpath for source set 'main'.
+--- org.slf4j:slf4j-api:1.7.7
\--- ch.qos.logback:logback-classic:1.1.2
 +--- ch.qos.logback:logback-core:1.1.2
 \--- org.slf4j:slf4j-api:1.7.6 -> 1.7.7

(*) - dependencies omitted (listed previously)

自定义依赖项解析规则

对于我们在构建文件中定义的每个依赖项,都有一个依赖项解析规则。当需要解析依赖项时,将执行此规则。我们可以在构建文件中自定义此规则,因此我们可以在依赖项实际解析之前更改规则的某些部分。Gradle 允许我们通过自定义解析规则来更改依赖项组、名称和版本。这样,我们甚至可以用其他依赖项完全替换依赖项或强制使用特定版本。

依赖项解析规则细节在 org.gradle.api.artifacts.DependencyResolveDetails 类中实现。在 resolutionStrategy 配置块内部,我们使用 eachDependency 方法来自定义解析规则。此方法接受一个闭包,闭包参数是 DependencyResolveDetails 的一个实例。我们使用 DependencyResolveDetailsuseVersionuseTarget 方法来更改请求的依赖项的版本或完整的组、名称和版本。

让我们更改之前的示例构建文件,并为 org.slf4j:slf4j-api 依赖项定义一个自定义解析规则,以便始终使用版本 1.7.7。在下一个示例构建文件中,我们将看到如何实现这一点:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      failOnVersionConflict()

      // Customize dependency resolve rules.
      eachDependency { DependencyResolveDetails details ->
        def requestedModule = details.requested

        // Force version for 
        // org.slf4j:slf4j-api dependency.
        if (requestedModule.group == 'org.slf4j'
          && requestedModule.name == 'slf4j-api') {

          // Force version 1.7.7.
          details.useVersion '1.7.7'
        }
      }
    }
  }
}

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7'

  runtime 'ch.qos.logback:logback-classic:1.1.2'
}

这种机制非常强大。除了强制指定特定版本外,我们还可以使用依赖项解析规则将完整的依赖项替换为另一个依赖项。假设我们项目中的一个依赖项有一个对 Log4j 日志框架的传递依赖。我们不希望这个依赖项,而是想使用 log4j-over-slf4j 桥接。这个桥接包含 Log4j 类的替代实现,因此我们可以使用 SLF4J API 实现。log4j-over-slf4j 桥接由org.slf4j:log4j-over-slf4j:1.7.7依赖项定义。我们使用解析规则细节的useTarget方法设置新的目标。该方法接受依赖项的字符串表示法和映射表示法。

以下示例构建文件包含替换Log4j依赖项到log4j-over-slf4j桥接的依赖项解析规则:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      eachDependency { DependencyResolveDetails details ->
        def requestedModule = details.requested

        // Change resolve rule for log4j:log4j
        // (transitive) dependency.
        if (requestedModule.group == 'log4j'
          && requestedModule.name == 'log4j') {

          // Replace log4j:log4j:<version> with
          // org.slf4j:log4j-over-slf4j:1.7.7.
          details.useTarget group: 'org.slf4j',
                  name: 'log4j-over-slf4j',
                  version: '1.7.7'
          // Alternative syntax:
          // useTarget 'org.slf4j:log4j-over-slf4j:1.7.7'
        }
      }
    }
  }
}

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7'

  // In real life this is probably a transitive
  // dependency from a dependency we need in our project.
  // We put it here as an example to show we
  // can completely replace a dependency with
  // another.
  runtime 'log4j:log4j:1.2.17'

  runtime 'ch.qos.logback:logback-classic:1.1.2'
}

我们可以通过命令行验证 Log4j 依赖项是否被dependencies任务替换。以下代码显示了这一点:

$ gradle -q dependencies --configuration runtime

------------------------------------------------------------
Root project
------------------------------------------------------------

runtime - Runtime classpath for source set 'main'.
+--- org.slf4j:slf4j-api:1.7.7
+--- log4j:log4j:1.2.17 -> org.slf4j:log4j-over-slf4j:1.7.7
|    \--- org.slf4j:slf4j-api:1.7.7
\--- ch.qos.logback:logback-classic:1.1.2
 +--- ch.qos.logback:logback-core:1.1.2
 \--- org.slf4j:slf4j-api:1.7.6 -> 1.7.7

(*) - dependencies omitted (listed previously)

注意到log4j:log4j:1.2.17 → org.slf4j:log4j-over-slf4j:1.7.7这一行,它直观地显示了依赖项被新的依赖项替换。

自定义依赖项解析规则还允许我们定义自定义版本方案。例如,在我们的组织中,我们可以定义如果依赖项的版本设置为fixed值,则实际版本将从企业内部网络的中央位置获取。这样,组织中的所有项目都可以共享依赖项的相同版本。

在下一个示例构建文件中,我们将实现自定义版本方案。如果省略了version属性或具有fixed值,则版本信息将从预定义的版本列表中获取。以下代码显示了这一点:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      eachDependency { DependencyResolveDetails details ->
        def requestedModule = details.requested

        // If version is not set or version 
        // has value 'fixed' set
        // version based on external definition.
        if (!requestedModule.version
          || requestedModule.version == 'fixed') {
          def moduleVersion = findModuleVersion(requestedModule.name)
          details.useVersion moduleVersion
        }
      }
    }
  }
}

dependencies {
  // Version is not defined for this dependency,
  // is resolved via custom dependency resolve rule.
  compile 'org.slf4j:slf4j-api'

  // Version is set to 'fixed', so version can
  // be resolved via custom dependency resolve rule.
  runtime 'ch.qos.logback:logback-classic:fixed'
}

/**
* Find version for given module name. In real life
* this could be part of a company Gradle plugin
* or intranet resource with version information.
*
* @param name Module descriptor name
* @return Version for given module name
*/
def findModuleVersion(String name) {
  ['slf4j-api': '1.7.7', 'logback-classic': '1.1.2']
  .find { it.key == name}
  .value
}

当我们从命令行运行dependencies任务时,看到其输出是很有趣的:

$ gradle -q dependencies --configuration runtime

------------------------------------------------------------
Root project
------------------------------------------------------------

runtime - Runtime classpath for source set 'main'.
+--- org.slf4j:slf4j-api: -> 1.7.7
\--- ch.qos.logback:logback-classic: -> 1.1.2
 +--- ch.qos.logback:logback-core:1.1.2
 \--- org.slf4j:slf4j-api:1.7.6 -> 1.7.7

(*) - dependencies omitted (listed previously)

在输出中,我们可以清楚地看到没有版本号的org.slf4j:slf4j-api依赖项现在使用的是版本1.7.7ch.qos.logback:logback-classic依赖项的fixed版本解析为版本1.1.2

使用客户端模块

我们可以不在存储库中找到的模块描述符上依赖外部模块依赖项,而是在我们的构建文件中将模块的元数据定义为客户端模块。记得从第一章,定义依赖项,中提到的,使用客户端模块,我们在构建文件中定义模块描述符,并仍然从存储库获取工件。

让我们在以下示例构建文件中使用客户端模块。我们重新定义了logback-classic依赖项的传递依赖,并使用slf4j-api依赖项的版本1.7.7。以下代码显示了这一点:

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      failOnVersionConflict()
    }
  }
}

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7'

  // Use a client module to redefine the transitive
  // dependencies for the logback-classic.
  runtime module('ch.qos.logback:logback-classic:1.1.2') {
    dependency 'ch.qos.logback:logback-core:1.1.2'

    // Force the correct version of
    // the slf4j-api dependency/
    dependency 'org.slf4j:slf4j-api:1.7.7'
  }
}

我们从命令行调用dependencies任务来检查是否使用了正确的依赖项:

$ gradle -q dependencies --configuration runtime

------------------------------------------------------------
Root project
------------------------------------------------------------

runtime - Runtime classpath for source set 'main'.
+--- org.slf4j:slf4j-api:1.7.7
\--- ch.qos.logback:logback-classic:1.1.2
 +--- org.slf4j:slf4j-api:1.7.7
 \--- ch.qos.logback:logback-core:1.1.2

(*) - dependencies omitted (listed previously)

我们在输出中看到,对org.slf4j:slf4j-api的依赖现在为1.7.7,并且我们不再有版本冲突。

使用动态版本和变更模块

在第一章 定义依赖 中,我们已经学习了动态版本的概念。例如,我们可以使用一系列版本,如 [4.0.1.RELEASE,4.0.4.RELEASE[。当依赖项通过 Gradle 解析时,会选取该范围内可用的最新静态版本。

变更模块与动态版本不同。变更模块引用具有相同版本但具有变更 artifacts 的依赖项。例如,在一个 Maven 仓库中,变更模块是由version属性中的-SNAPSHOT指示的快照模块。Gradle 可以解析变更模块依赖项并获取给定版本的最新 artifacts。然而,下次可能会下载新的 artifacts,因为内容已更改。

Gradle 会缓存动态版本和变更模块 24 小时。我们将看到如何在我们的 Gradle 构建文件和命令行中自定义此行为。然而,在我们查看选项之前,我们首先会了解 Gradle 缓存的工作原理。

理解依赖项缓存

Gradle 的依赖项缓存试图最小化远程请求和下载的数量,以便构建可以快速且可靠。缓存有两个部分来执行适当的依赖项缓存:

  • 首先,它有一个用于依赖项元数据(POM 或 Ivy 描述文件)的缓存,包括依赖项组、名称和版本。Gradle 为每个仓库保留一个单独的缓存。因此,如果相同的依赖项在多个仓库中找到,则元数据信息将缓存在多个依赖项元数据缓存中。

  • 依赖项缓存也有一个用于存储依赖项下载的 artifacts 的单个缓存。多个元数据缓存共享下载 artifacts 的相同缓存。artifacts 通过其内容的 SHA1 哈希码存储,而不是通过元数据,如组、名称或版本。

根据仓库和 artifacts 缓存分离的元数据缓存提供了足够的灵活性,以执行可重复和可靠的依赖项解析。如果 Gradle 无法解析依赖项元数据,则依赖项解析将停止,即使本地缓存有从不同仓库(未定义在我们的构建中)下载的 artifacts 副本。这种仓库独立性将构建彼此隔离,并防止依赖项 artifacts 的问题。

Gradle 首先尝试确定一个工件文件的 SHA1 校验和,然后再下载该工件。如果可以确定校验和,并且缓存中已经存在具有相同校验和的文件,则不会下载该文件。Gradle 还尝试重用本地 Maven 仓库中的工件。如果本地 Maven 仓库中某个工件的校验和与远程仓库中工件的校验和匹配,则不需要下载该工件,可以直接从本地 Maven 仓库复制。

由于 Gradle 使用 SHA1 校验和来验证工件内容,因此可以存储同一工件的多个版本。例如,当一个工件是变化模块的一部分,或者工件的 内容在仓库中发生变化,而版本号没有变化时。

元数据缓存和工件缓存都存储在由 GRADLE_USER_HOME 环境变量定义的目录中,默认情况下是用户主目录中的 .gradle/caches 目录。Gradle 使用复杂的锁定机制来管理缓存,因此多个项目可以同时使用缓存目录和文件。在下一个示例构建文件中,我们创建 artifactsLocation 任务以打印下载的工件存储位置:

apply plugin: 'java'

repositories.jcenter()

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7'
  runtime 'ch.qos.logback:logback-classic:1.1.2'
}

task artifactsLocation {
  description 'Show location of artifact on disk'

  doFirst {
    configurations.runtime.each { println it }
  }
}

当我们执行 artifactsLocation 任务时,在输出中可以看到文件存储在用户主目录(/Users/mrhaki)中的 .gradle/caches 目录中。我们还可以看到用于目录名的 SHA1 校验和。以下代码展示了这一点:

$ gradle -q artifactsLocation
/Users/mrhaki/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.7/2b8019b6249bb05d81d3a3094e468753e2b21311/slf4j-api-1.7.7.jar
/Users/mrhaki/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.1.2/b316e9737eea25e9ddd6d88eaeee76878045c6b2/logback-classic-1.1.2.jar
/Users/mrhaki/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.1.2/2d23694879c2c12f125dac5076bdfd5d771cc4cb/logback-core-1.1.2.jar

缓存命令行选项

我们可以使用 --offline 命令行选项来跳过任何网络请求。因此,使用此选项时,Gradle 从不尝试访问远程仓库,所有信息都从 Gradle 依赖缓存中获取。如果缓存中的信息不足以进行构建,则构建失败。

使用 --refresh-dependencies 选项,我们可以刷新元数据缓存。如果由于某种原因,我们预期元数据不再正确,可以使用此选项。Gradle 将刷新每个仓库中的元数据缓存中的所有信息。只有当 SHA1 校验和与工件缓存中工件的校验和不同时,才会下载工件。

更改缓存过期时间

具有静态版本的依赖关系可以轻松缓存。工件的内容有一个校验和,这可以用来决定是使用缓存还是下载工件(并将其放置在工件缓存中)。具有动态版本或变化模块的依赖关系可能具有变化的工件,因此我们需要能够自定义缓存设置。我们可以更改具有动态版本和变化模块的缓存依赖关系的过期时间。默认过期时间是 24 小时。在过期时间之后,Gradle 将使缓存失效并确定是否需要再次下载工件。

我们使用resolutionStrategy配置闭包中的cacheDynamicVersionsFor方法更改具有动态版本的依赖项的过期时间。此方法接受一个数字和时间单位来设置缓存过期时间的值。时间单位可以是java.util.concurrent.TimeUnit类型或转换为TimeUnit的字符串。

要更改模块,我们使用cacheChangingModulesFor方法来更改过期时间。此方法也接受一个数字和时间单位,就像cacheDynamicVersionsFor方法一样。

在下一个示例构建文件中,我们更改了runtime配置中动态版本和更改模块的缓存过期时间。我们也可以使用all方法和配置块将其设置为所有配置。以下代码展示了这一点:

// Import needed for cache methods time unit.
import java.util.concurrent.TimeUnit

apply plugin: 'java'

repositories.jcenter()

configurations {
  runtime {
    resolutionStrategy {
      // Change expiration time for caching
      // dynamic version to 30 minutes.
      cacheDynamicVersionsFor 30, 'minutes'

      // Alternative syntax is using TimeUnit:
      // cacheDynamicVersionsFor 1, TimeUnit.HOURS

      // Change expiration time for cached
      // changing modules to 5 minutes using
      // java.util.concurrent.TimeUnit.
      cacheChangingModulesFor 5, TimeUnit.MINUTES

      // Or we could use string notation.
      // cacheChangingModulesFor 1, 'minutes'
    }
  }

  // Alternatively we could apply
  // this to all configurations:
  // all {
  //     resolutionStrategy {
  //         cacheDynamicVersionsFor 4, 'hours'
  //         cacheChangingModulesFor 10, 'minutes'
  //     }
  // }
}

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.7'

  runtime 'ch.qos.logback:logback-classic:1.1.2'
}

Gradle 了解存储在 Maven 仓库中的工件,并且如果版本属性以-SNAPSHOT结尾,则模块是更改模块。我们也可以在我们的构建脚本中定义一个依赖项是否是更改模块,例如,如果无法从版本属性中推导出来。我们必须在依赖项的配置闭包中将changing属性设置为true

在以下示例构建文件中,我们有com.vehicles:cars依赖项,这是一个更改模块,但我们使用静态版本1.0

apply plugin: 'java'

repositories {
  maven {
    url 'http://ourcompany.com/maven'
  }
}

dependencies {
  compile('com.vehicles:cars:1.0') {
    // Instruct Gradle this a changing
    // module, although it cannot
    // be derived from the version attribute.
    changing = true
  }

  // Other syntax using the map notation:
  // compile group: 'com.vehicles', name: 'cars',
  //         version: '1.0', changing: true
}

摘要

在本章中,你学习了 Gradle 如何解析依赖项以及如何自定义它。你看到了如何重新配置你的构建脚本以处理传递依赖,以及如何解决构建中依赖项之间的版本冲突。Gradle 提供了对我们如何定义依赖项的精细控制,并允许非常明确的依赖项定义。

对于传递依赖,我们有几种方法来重新定义哪些依赖需要使用,可以通过禁用或排除传递依赖来实现。

当我们的构建中的依赖项出现版本冲突时,我们可以依赖使用最新版本的默认策略,或者实现一个自定义策略。Gradle 有几种方法可以通过重新定义解析策略来自定义依赖项解析规则。例如,我们可以覆盖依赖项的版本属性,甚至完全用兼容但不同的依赖项替换它。

最后,你看到了 Gradle 的依赖项缓存是如何工作的。它是为了减少网络请求,提供具有依赖项的可靠和可重复的构建。你学习了如何自定义更改或定义为动态版本的依赖项模块的过期时间。

到目前为止,我们看到了如何在项目中包含依赖项,但在下一章中,我们将看到如何发布我们为其他项目创建的依赖项的工件。

第四章 发布工件

在前面的章节中,我们学习了如何在项目中定义和使用依赖关系。然而,我们在项目中编写的代码也可以成为另一个项目的依赖。为了使另一个项目能够将我们的代码作为依赖使用,我们应该将我们的代码发布为依赖项工件,以便其他项目可以使用它。

在本章中,你将学习如何在你的项目中定义工件。这些工件需要发布以便其他人可以使用它们。我们首先使用文件系统发布它们,这样工件就可以在同一台计算机上使用,甚至如果我们使用内网上的网络共享。在后面的章节中,我们将看到如何将我们的工件发布到 Maven 仓库、Ivy 仓库和 Bintray。

定义工件配置

一个 Gradle 项目可以包含我们想要发布的工件。一个工件可以是 ZIP 或 JAR 存档文件或任何其他文件。我们可以在一个项目中定义一个或多个工件。因此,如果我们想要从同一个源树中获取两个不同的工件,我们不需要创建两个不同的项目。

在 Gradle 中,我们使用配置来分组工件。我们曾使用配置来定义项目的依赖关系,但现在我们将使用配置来分组我们的工件,这些工件可以是其他项目的依赖项。因此,一个配置可以包含依赖项和工件。如果我们将 Java 插件应用到我们的项目中,我们得到一个名为archives的配置,它包含项目的默认 JAR 工件。

在以下示例 Gradle 构建文件中,我们使用了 Java 插件。我们添加了一个任务来显示属于archives配置的工件文件名。以下代码展示了这一点:

apply plugin: 'java'

// Set the archivesBaseName property,
// to change the name of the
// default project artifact.
archivesBaseName = 'java_lib'

task artifactsInfo << {
  // Find archives configuration
  // and display file name(s)
  // for artifacts belonging
  // to the configuration.
  configurations
    .findByName('archives')
    .allArtifacts
    .each { artifact ->
      println artifact.file.name
    }
}

当我们从命令行运行artifactsInfo任务时,我们在输出中看到java_lib.jar文件名。以下代码展示了这一点:

$ gradle artifactsInfo
:artifactsInfo
java_lib.jar

BUILD SUCCESSFUL

Total time: 1.088 secs

对于我们项目中的每个配置,Gradle 都会为项目添加两个任务:build<ConfigurationName>upload<ConfigurationName>build<ConfigurationName>任务为给定的配置名称创建工件。upload<ConfigurationName>任务为给定的配置名称创建和上传工件。upload<ConfigurationName>任务需要额外的配置来知道上传工件的位置。我们将在本章后面看到如何配置这个任务。

在我们的示例项目中,我们有buildArchivesuploadArchives任务。让我们运行我们的示例项目的buildArchives任务,看看哪些任务被执行:

$ gradle buildArchives
:compileJava
:processResources
:classes
:jar
:buildArchives

BUILD SUCCESSFUL

Total time: 1.209 secs
$ ls build/libs
java_lib.jar

在这里,我们可以看到首先在我们的 Java 项目中准备了一切以创建 JAR 工件。然后,JAR 工件被添加到artifacts配置中。创建的java_lib.jar JAR 文件可以在build/libs目录中找到。

如果我们为我们的项目设置 version 属性,那么它将被用于我们创建的工件名称中。在下一个示例构建文件中,我们将设置 version 属性并查看为工件创建的名称:

apply plugin: 'java'

archivesBaseName = 'java_lib'

// Set project version,
// which is then used in the 
// artifact name.
version = '2.3'

task artifactsInfo << {
configurations
    .findByName('archives')
    .allArtifacts
    .each { artifact ->
      println artifact.file.name
    }
}

让我们运行 artifactsInfo 任务来查看我们工件的名称:

$ gradle artifactsInfo
:artifactsInfo
java_lib-2.3.jar

BUILD SUCCESSFUL

Total time: 2.831 secs

定义工件

在前面的章节中,你了解到 Java 插件添加了一个 archives 配置,用于从项目中分组工件。就像我们在项目中创建依赖项配置一样,我们也可以为其工件创建自己的配置。要将归档或文件分配给此配置,我们必须在我们的构建脚本中使用 artifacts 配置块。在配置闭包内部,我们使用配置名称后跟工件名称。我们还可以在 artifacts 块内部进一步自定义工件定义。

我们可以使用以下三种类型来定义工件:

类型 描述
AbstractArchiveTask 工件的信息是从归档任务中提取的。工件是 org.gradle.api.artifact 包中的 PublishArtifact 的一个实例。
File 工件的信息是从文件名中提取的。工件是扩展 PublishArtifactConfigurablePublishArtifact 的一个实例。
Map 这是定义文件工件的另一种方式。该映射必须包含一个 file 键,其他属性用于进一步配置工件。

使用归档任务

在下一个示例构建文件中,我们将使用归档任务来定义项目的工件。记住将 Gradle 基础插件应用到项目中非常重要,因为基础插件为 build<ConfigurationName>upload<ConfigurationName> 添加了任务规则。以下代码展示了这一点:

// The base plugin adds the
// build<ConfigurationName> and
// upload<ConfigurationName> tasks
// to our project.
apply plugin: 'base'

// Add archive task that will
// create a ZIP file with some
// contents we want to be published.
task manual(type: Zip) {
  baseName = 'manual'

  from 'src/manual'
}

// Create a new artifacts configuration
// with the name manualDistribution.
configurations {
  manualDistribution
}

// Use the manual archive task
// to define the artifact for the
// manualDistribution configuration.
// Syntax:
// configurationName archiveTask
artifacts {
  manualDistribution manual
}

当我们使用归档任务来定义配置的工件时,Gradle 也会为该工件添加一个任务依赖。这意味着,如果我们调用 buildManualDistribution 任务,Gradle 也会调用生成工件配置归档的 manual 任务。当我们从命令行执行任务时,我们会看到这一点。以下命令展示了这一点:

$ gradle buildManualDistribution
:manual
:buildManualDistribution

BUILD SUCCESSFUL

Total time: 1.368 s
ecs

使用工件文件

除了归档任务之外,我们还可以使用文件作为工件。Gradle 将使用文件属性来定义工件名称、类型和扩展名。在以下示例构建文件中,我们使用文件作为工件:

apply plugin: 'base'

configurations {
  readmeDistribution
}

artifacts {
  // Use a file as artifact.
  // Name and extension are extracted
  // from the actual file.
  readmeDistribution file('src/files/README.txt')
}

当我们使用文件工件表示法时,我们可以添加一个额外的配置闭包。在闭包中,我们可以设置名称、类型、扩展和分类器属性。以下代码展示了这一点:

apply plugin: 'base'

configurations {
  readmeDistribution
}

artifacts {
  // Define file artifact, but we also
  // customize the file artifact
  // name, extension and classifier.
  readmeDistribution file('src/files/README.txt'), {
    name 'PLEASE_READ_THIS'
    extension ''
    classifier 'docs'
  }
}

在文件艺术品配置闭包中,我们可以使用的一个有趣的方法是 builtBy 方法。此方法接受一个或多个负责构建艺术品的任务名称。如果我们使用此方法,Gradle 可以确定当我们运行 build<ConfigurationName>upload<ConfigurationName> 任务时需要执行的任务。

在下一个示例构建文件中,我们将使用 builtBy 方法:

apply plugin: 'base'

configurations {
  readmeDistribution
}

// New task that copies
// a file to the build directory.
task docFiles(type:Copy) {
  from 'src/files'
  into "${buildDir}/docs"
  include 'README.txt'
}

artifacts {
  // Define file artifact.
  readmeDistribution(file("${buildDir}/docs/README.txt")) {
    // Define which task is responsible
    // for creating the file, so a
    // task dependency is added for
    // the buildReadmeDistribution and
    // uploadReadmeDistribution tasks.
    builtBy docFiles
  }
}

为了确保 docFiles 任务被添加为任务依赖项,我们从命令行运行 buildReadmeDistribution。以下命令展示了这一点:

$ gradle buildReadmeDistribution
:docFiles
:buildReadmeDistribution

BUILD SUCCESSFUL

Total time: 0.864 secs

最后,当我们定义文件艺术品时,我们可以使用映射符号。我们使用 file 属性来定义文件。我们还可以使用 nameextensiontypeclassifierbuiltBy 键来定义。在以下示例构建文件中,我们使用了映射符号:

apply plugin: 'base'

configurations {
  readmeDistribution
}

task docFiles(type:Copy) {
  from 'src/files'
  into "${buildDir}/docs"
  include 'README.txt'
}

artifacts {
  // Define file artifact.
  readmeDistribution(
    file: "${buildDir}/docs/README.txt",
    name: 'DO_READ',
    extension: 'me',
    type: 'text',
    classifier: 'docs'
    builtBy: docFiles
  )
}

创建艺术品

我们看到了如何定义艺术品,但我们也需要在我们的构建文件中创建艺术品。我们可以使用归档任务来创建艺术品,或者文件本身可以是一个艺术品。大多数时候,当我们使用 Gradle 进行 Java 项目开发时,我们会构建一个包含编译类和资源的归档。实际上,Java 插件会为我们项目添加一个 jar 任务,它只会做这件事。创建的 JAR 文件随后被添加到 archives 配置中。

在下一个示例构建文件中,我们将使用 Java 插件并简单地依赖默认的艺术品配置和任务。以下代码展示了这一点:

apply plugin: 'java'

// Define project properties.
group = 'com.mrhaki.sample'
version = '2.1'
archivesBaseName = 'sample'

// Extra task to check the artifacts.
task artifactsInfo << {
  configurations
    .findByName('archives')
    .allArtifacts
    .each { artifact ->
      println artifact.file.name
    }
}

我们现在可以运行 buildArchives 任务,并从命令行使用 artifactsInfo 任务检查艺术品:

$ gradle buildArchives artifactsInfo
:compileJava
:processResources
:classes
:jar
:buildArchives
:artifactsInfo
sample-2.1.jar

BUILD SUCCESSFUL

Total time: 7.643 secs
$

在这种情况下,我们有一个单一的艺术品;然而,当我们使用 Gradle 时,在同一个项目中我们可以有多个艺术品。例如,我们可能想要将我们的源打包到一个 JAR 文件中,以及我们的生成文档。这两个 JAR 文件都应该包含在 archives 配置中,这样当我们执行 buildArchives 任务时,所有创建这些 JAR 文件所需的任务都会执行。

我们扩展了之前的构建文件示例,添加了创建两个额外 JAR 文件的代码,并将它们添加到 archives 艺术品配置中。以下代码展示了这一点:

apply plugin: 'java'

// Define project properties.
group = 'com.mrhaki.sample'
version = '2.1'
archivesBaseName = 'sample'

// Create a JAR file with the
// Java source files.
task sourcesJar(type: Jar) {
  classifier = 'sources'

  from sourceSets.main.allJava
}

// Create a JAR file with the output
// of the javadoc task.
task javadocJar(type: Jar) {
  classifier = 'javadoc'

  from javadoc
}

artifacts {
  // Add the new archive tasks
  // to the artifacts configuration.
  archives sourcesJar, javadocJar
}

// Extra task to check the artifacts.
task artifactsInfo << {
  configurations
    .findByName('archives')
    .allArtifacts
    .each { artifact ->
      println artifact.file.name
    }
}

我们现在将执行 buildArchivesartifactsInfo 任务。我们在输出中看到我们的两个新任务 sourcesJarjavadocJar 已执行。生成的艺术品文件是 sample-2.1.jarsample-2.1-sources.jarsample-2.1-javadoc.jar。以下命令展示了这一点:

$ gradle buildArchives artifactsInfo
:compileJava
:processResources
:classes
:jar
:javadoc
:javadocJar
:sourcesJar
:buildArchives
:artifactsInfo
sample-2.1.jar
sample-2.1-sources.jar
sample-2.1-javadoc.jar

BUILD SUCCESSFUL

Total time: 2.945 secs
$

在前面的示例中,我们有一个 Java 项目,并且从同一个源集中,我们想要创建两个不同的归档文件。源集包含一些 API 类和实现类。我们想要有一个包含 API 类的 JAR 文件,以及一个包含实现类的 JAR 文件。以下代码展示了这一点:

apply plugin: 'java'

// Define project properties.
group = 'com.mrhaki.sample'
version = '2.1'
archivesBaseName = 'sample'

// We create a new source set
// api, which contains the
// Java sources. This means
// Gradle will search for the
// directory src/api/java.
sourceSets {
  api
}

task apiJar(type: Jar) {
  appendix = 'api'

  // We use the output of the
  // compilation of the api
  // source set, to be the
  // contents of this JAR file.
  from sourceSets.api.output
}

artifacts {
  // Assign apiJar archive task to the
  // archives configuration.
  archives apiJar
}

// Extra task to check the artifacts.
task artifactsInfo << {
  configurations
    .findByName('archives')
    .allArtifacts
    .each { artifact ->
      println artifact.file.name
    }
}

我们现在将运行 buildArchives 任务,并查看创建包含 api 源集类别的 JAR 文件所需的所有任务是否都已执行:

$ gradle buildArchives artifactsInfo
:compileApiJava
:processApiResources
:apiClasses
:apiJar
:compileJava
:processResources
:classes
:jar
:buildArchives
:artifactsInfo
sample-2.1.jar
sample-api-2.1.jar

BUILD SUCCESSFUL

Total time: 2.
095 secs
$

将工件发布到本地目录

我们现在知道了如何创建一个或多个工件,以及如何使用工件配置来分组它们。在本节中,我们将了解如何将我们的工件复制到本地目录或网络共享。请记住,对于每个工件的配置,Gradle 都会添加一个build<ConfigurationName>任务和一个upload<ConfigurationName>任务。现在是时候更多地了解upload<ConfigurationName>任务,以便我们可以复制我们的工件。在接下来的章节中,我们还将学习如何部署到 Maven 仓库、Ivy 仓库以及 Bintray。

对于每个upload<ConfigurationName>任务,我们必须配置一个仓库定义。仓库定义基本上是我们上传或发布工件时的目的地。在本节中,我们使用本地目录,因此我们使用flatDir方法定义一个仓库。我们指定一个名称和目录,以便 Gradle 知道upload<ConfigurationName>任务的输出需要放在哪里。在我们应用了 Java 插件的 Gradle 项目中,我们已经有archives工件配置和uploadArchives任务。我们必须配置uploadArchives任务并定义需要使用的仓库。在下一个示例构建文件中,我们将使用lib-repo本地目录作为仓库目录:

apply plugin: 'java'

// Define project properties.
group = 'com.mrhaki.sample'
version = '2.1'
archivesBaseName = 'sample'

// Configure the uploadArchives task.
uploadArchives {
  // Define a local directory as the
  // upload repository. The artifacts
  // must be 'published' in this
  // directory.
  repositories {
    flatDir(
      name: 'upload-repository',
      dirs: "${projectDir}/lib-repo")
  }
}

让我们看看执行uploadArchives任务时的输出,并检查lib-repo目录中的文件:

$ gradle uploadArchives
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:uploadArchives

BUILD SUCCESSFUL

Total time: 3.424 secs
$ ls -1 lib-repo
ivy-2.1.xml
ivy-2.1.xml.sha1
sample-2.1.jar
sample-2.1.jar.sha1
$

在我们的lib-repo目录中,对于我们的工件,我们有一个名为ivy-2.1.xml的 Ivy 描述符文件,以及一个名为ivy-2.1.xml.sha1的校验和文件。我们还看到了我们的sample-2.1.jar工件及其sample-2.1.jar.sha1校验和文件。Ivy 描述符文件包含有关我们的工件的基本信息。以下代码展示了这一点:

<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0" >
  <info organisation="com.mrhaki.sample" module="java" revision="2.1" status="integration" publication="20141126060840">
    <description/>
  </info>
  <configurations>
    <conf name="archives" visibility="public" description="Configuration for archive artifacts."/>
    <conf name="compile" visibility="private" description="Compile classpath for source set 'main'."/>
    <conf name="default" visibility="public" description="Configuration for default artifacts." extends="runtime"/>
    <conf name="runtime" visibility="private" description="Runtime classpath for source set 'main'." extends="compile"/>
    <conf name="testCompile" visibility="private" description="Compile classpath for source set 'test'." extends="compile"/>
    <conf name="testRuntime" visibility="private" description="Runtime classpath for source set 'test'." extends="runtime,testCompile"/>
  </configurations>
  <publications>
    <artifact name="sample" type="jar" ext="jar" conf="archives,runtime"/>
  </publications>
</ivy-module>

我们已经在uploadArchives任务配置中配置了仓库。然而,我们也可以通过使用repositories配置块来引用项目中配置的现有仓库定义。这是一个好习惯,因为我们只需要定义一次仓库,就可以在构建文件中的多个任务中重用它。让我们重写之前的示例构建文件,在repositories配置块中定义仓库,并在uploadArchives任务中引用它。以下代码展示了这一点:

apply plugin: 'java'

// Define project properties.
group = 'com.mrhaki.sample'
version = '2.1'
archivesBaseName = 'sample'

// Define upload repository.
repositories {
  flatDir(
    name: 'upload-repository',
    dirs: "${projectDir}/repo")
}

// Configure the uploadArchives task.
uploadArchives {
  // Refer to repository with the
  // name 'upload-repository' as the
  // repository for uploading artifacts.
  repositories.add(
    project.repositories.'upload-repository')
}

排除描述符文件

默认情况下,Ivy 描述符文件会被添加到上传位置。如果我们不希望它被添加,我们可以为Upload任务设置uploadDescriptor属性。

在下面的示例构建文件中,我们在uploadArchives任务中将uploadDescriptor属性设置为false

apply plugin: 'java'

// Define project properties.
group = 'com.mrhaki.sample'
version = '2.1'
archivesBaseName = 'sample'

// Define upload repository.
repositories {
  flatDir(
    name: 'upload-repository',
    dirs: "${projectDir}/lib-repo")
}

uploadArchives {
  // Exclude the descriptor file.
  uploadDescriptor = false

  repositories.add(
    project.repositories.'upload-repository')
}

当我们执行任务并查看lib-repo目录中的文件时,我们看到描述符文件没有被添加。以下代码展示了这一点:

$ gradle uploadArchives
:compileJava
:processResources
:classes
:jar
:uploadArchives

BUILD SUCCESSFUL

Total time: 1.463 secs
$ ls -1 lib-repo
sample-2.1.jar
sam
ple-2.1.jar.sha1
$

签署工件

我们可以使用 Gradle 的签名插件对工件进行数字签名。该插件支持生成 Pretty Good Privacy (PGP) 签名。这种签名格式也是发布到 Maven Central 仓库所必需的。要创建 PGP 签名,我们必须在我们的计算机上安装一些 PGP 工具。这些工具的安装方式因操作系统而异。在类 Unix 系统上,软件可能通过包管理器提供。使用 PGP 软件,我们需要创建一个密钥对,我们可以使用它来签名工件。

要对工件进行签名,我们必须将签名插件应用到我们的项目中。然后我们必须使用 signing 配置块来配置插件。我们需要至少添加有关我们的 PGP 密钥对的信息。我们需要公钥的十六进制表示,包含我们的私钥的秘密密钥环文件的路径,以及用于保护私钥的密码短语。我们将这些信息分配给签名插件配置的 keyIdsecretKeyRingFilepassword 属性。这些值不应包含在 Gradle 构建文件中,因为它们是秘密的,所以最好将它们存储在 gradle.properties 文件中,并对该文件应用安全的文件权限。此外,我们不应将此文件添加到我们的版本控制系统中。

在以下示例 gradle.properties 文件中,我们设置了属性。这些值是示例值,并且对每个用户都不同:

signing.keyId = 8B00165A
signing.secretKeyRingFile = /Users/current/.gnupg/secring.gpg
signing.password = secret

使用配置进行签名

我们已经准备好对工件进行签名。我们需要使用 signing 配置块来配置我们想要签名的工件。我们必须指定包含要签名的工件的工件配置名称。

当我们将 Java 插件应用到我们的项目中时,我们会得到 archives 工件配置。我们希望对这个配置分配的工件进行签名。在下一个示例构建文件中,我们应用了 Java 和签名插件。在 signing 配置块中,我们定义了我们要对属于 archives 配置的工件进行签名:

apply plugin: 'java'
apply plugin: 'signing'

group = 'com.mrhaki.sample'
version = '2.1'
archivesBaseName = 'sample'

// Configure signing plugin.
signing {
  // Define that we want to
  // sign the artifacts belonging
  // to the archives configuration.
  sign configurations.archives
}

uploadArchives {
  repositories {
    flatDir(
      name: 'local-repo',
      dirs: "${projectDir}/repo")
  }
}

签名插件还为我们项目添加了一个新的任务规则——sign<ConfigurationName>。配置的名称是我们定义在 signing 配置块中的名称。我们定义了 archives 配置,因此,在我们的项目中,我们现在可以执行 signArchives 任务。该任务也被添加为 assemble 任务的依赖项;因此,每次我们调用 assemble 任务时,Gradle 都会确保 signArchives 任务也被调用。

在这里,我们运行 uploadArchives 任务以查看哪些文件被放入了仓库目录:

$ gradle uploadArchives
:compileJava
:processResources
:classes
:jar
:signArchives
:uploadArchives

BUILD SUCCESSFUL

Total time: 4.305 secs
$ ls -1 repo
ivy-2.1.xml
ivy-2.1.xml.sha1
sample-2.1.asc
sample-2.1.asc.sha1
sample-2.1.jar
sample-2.1.jar.sha1
$

我们注意到,与签名文件 sample-2.1.asc 一起创建了一个用于签名文件的校验和文件 sample-2.1.asc.sha1

使用存档任务进行签名

要对不属于工件配置的工件进行签名,我们必须对签名插件进行不同的配置。在signing配置块中,我们在上一节中分配了一个配置,但我们也可以使用归档任务。当我们调用sign<TaskName>任务规则时,这个归档任务的输出将被签名。

在下一个示例构建文件中,我们将使用manualZip任务创建一个 ZIP 文件。我们将为manualZip任务配置签名插件,以便这个 ZIP 文件被签名:

apply plugin: 'signing'

version = '1.0'

// New archive task to create
// a ZIP file from some files.
task manualZip(type: Zip) {
  archivesBaseName = 'manual'
  from 'src/docroot'
}

// Configure signing plugin to
// sign the output of the
// manualZip task.
signing {
  sign manualZip
}

// Create new configuration for
// ZIP and signed ZIP artifacts.
configurations {
  manualDistribution
}

// Set artifacts to manualDistribution
// configuration.
artifacts {
  manualDistribution(
    manualZip,
    signManualZip.singleSignature.file)
}

// Configure upload task for
// manualDistribution configuration.
uploadManualDistribution {
  repositories {
    flatDir {
      dirs "${projectDir}/repo"
    }
  }
}
// Add task dependency so signing of
// ZIP file is done before upload.
uploadManualDistribution.dependsOn signManualZip

所有sign<TaskName>任务自动具有对归档任务标识符的任务依赖,通过<TaskName>。因此,我们现在可以简单地调用uploadManualDistribution任务,ZIP 文件将被创建、签名并上传到repo目录。以下代码展示了这一点:

$ gradle uploadManualDistribution
:manualZip
:signManualZip
:uploadManualDistribution

BUILD SUCCESSFUL

Total time: 1.695 secs
$ ls -1 repo
ivy-1.0.xml
ivy-1.0.xml.sha1
manual-1.0.zip
manual-1.0.zip-1.0.asc
manual-1.0.zip-1.0.asc.sha1
manual-1.0.zip.sha1
$

摘要

在前面的章节中,你学习了如何使用外部依赖。在本章中,你学习了如何定义工件配置以分配你自己的工件。这些工件可以是其他项目和其他应用中其他开发者的依赖。

你还学习了在使用 Java 插件时如何创建默认工件。接下来,我们看到了如何从一个项目中创建多个工件。

你随后学习了如何配置一个上传任务,以便你可以将你的工件上传到本地目录。这个目录也可以是其他开发团队可访问的网络共享。

最后,你学习了如何使用签名插件对你的工件进行签名。当你希望向使用工件的人提供额外信心时,这可能很有用。

在接下来的章节中,你将看到如何将你的工件上传到 Maven 仓库、Ivy 仓库和 Bintray。

第五章:发布到 Maven 仓库

在上一章中,你学习了如何使用Upload任务发布你的项目工件。在本章中,你将了解更多关于将工件发布到 Maven 仓库的新功能和仍在开发中的特性。

你将了解 Gradle 中的新发布机制。这个特性目前仍在开发中,这意味着实现可能会在未来发生变化。但到目前为止,这种发布工件的方式将是默认的。

定义发布

我们必须将maven-publish插件添加到我们的项目中,以添加 Gradle 的新发布功能。该插件允许我们定义和部署我们的项目工件以 Maven 格式。这意味着我们的部署项目可以被支持 Maven 格式的其他开发者和项目使用。例如,其他项目可以使用 Gradle 或 Maven 并定义一个对我们发布的工件依赖。

maven-publish插件基于一个通用的publishing插件。publishing插件为我们项目添加了一个新的publishing扩展。我们可以在构建脚本中使用publications配置块来配置我们想要发布的工件和想要部署到的仓库。publications扩展在org.gradle.api.publish包中有PublishingExtension类型。该插件还向项目中添加了通用的生命周期publish任务。其他任务可以作为任务依赖添加到这个任务中;因此,通过单个publish任务,可以发布项目中的所有发布。

maven-publish插件还向项目中添加了额外的任务规则。有一个任务用于为项目中的每个发布生成 Maven POM 文件。插件还添加了一个新的任务规则,用于将每个发布发布到本地 Maven 仓库。最后,基于发布和仓库的组合添加了一个任务规则,用于将发布发布到指定的仓库。

让我们创建一个示例构建文件,并应用maven-publish插件来查看新任务:

apply plugin: 'maven-publish'
apply plugin: 'java'

现在,我们将从命令行调用tasks任务:

$ gradle tasks
...
Publishing tasks
----------------
publish - Publishes all publications produced by this project.
publishToMavenLocal - Publishes all Maven publications produced by this project to the local Maven cache.
...
BUILD SUCCESSFUL

Total time: 4.647 secs

我们可以在输出中看到publishpublishToMavenLocal任务。用于将单个发布发布到仓库的动态任务规则没有显示。

要配置我们的发布,我们必须首先添加一个publishing配置块。在块内部,我们定义publications配置块。在这个块中,我们定义一个发布。发布定义了需要发布的内容。maven-publish插件期望发布具有在org.gradle.api.publish.maven包中找到的MavenPublication类型。除了需要发布的工件之外,我们还可以定义生成 POM 文件的详细信息。

定义发布工件

我们定义的任何出版物都必须在我们的项目中具有唯一的名称。我们可以在publications配置块内添加多个具有自己名称的出版物。要添加工件,我们可以在出版物定义中使用artifact方法。我们还可以使用artifacts属性直接设置所有工件。

我们可以使用以下方式使用artifact方法定义工件:

类型 描述
AbstractArchiveTask 从归档任务中提取了工件的信息。工件是org.gradle.api.artifacts包中PublishArtifact的一个实例。
File 工件的信息是从文件名中提取的。
Map 这是定义工件的另一种方式。该映射必须包含一个source键,它引用一个文件或归档任务。我们可以使用其他属性来进一步配置工件,例如classifierextension

使用归档任务工件

在下面的示例构建文件中,我们定义了一个名为publishJar的新出版物,并将jar归档任务的输出定义为工件:

apply plugin: 'maven-publish'
apply plugin: 'java'

// Configuration block for publishing
// artifacts from the project.
publishing {

  // Define publications with what
  // needs to be published.
  publications {

    // Name of this publication
    // is publishJar.
    publishJar(MavenPublication) {

      // Use output of jar task
      // as the artifact for
      // the publication.
      artifact jar

      // Alternatively we can use
      // a Map notation:
      // artifact source: jar
    }

  }
}

接下来,我们将运行tasks任务,在输出中,我们将能够看到为发布此出版物而新生成的任务:

$ gradle tasks
...
Publishing tasks
----------------
generatePomFileForPublishJarPublication - Generates the Maven POM file for publication 'publishJar'.
publish - Publishes all publications produced by this project.
publishPublishJarPublicationToMavenLocal - Publishes Maven publication 'publishJar' to the local Maven repository.
publishToMavenLocal - Publishes all Maven publications produced by this project to the local Maven cache.
...
BUILD SUCCESSFUL

Total time: 4.215 secs

注意到两个额外的任务,generatePomFileForPublishJarPublicationpublishPublishJarPublicationToMavenLocal。出版物的名称publishJar用于这两个任务。Gradle 使用generatePomFileFor<publicationName>Publication模式为出版物生成 POM。将出版物发布到本地 Maven 仓库的任务模式是publish<publicationName>PublicationToMavenLocal。在本章的后面部分,我们将看到如何添加其他仓库。我们目前还不能调用这些任务,因为我们还需要设置groupversion项目属性,但我们将在这部分关于生成 POM 文件的章节中介绍。现在我们可以专注于在本节中定义出版物的工件。

对于一个出版物,我们不仅限于一个工件;我们可以通过多次调用artifact方法来添加更多工件。或者,我们可以使用artifacts属性分配多个工件。对于单个出版物,每个工件都应该具有唯一的classifierextension属性值。在我们可以调用任何任务之前,Gradle 将检查这一点,因此当工件没有唯一的classifierextensions属性值的组合时,我们会立即收到错误消息。

在下面的示例构建文件中,我们使用artifact方法向我们的出版物添加了两个额外的工件:

apply plugin: 'maven-publish'
apply plugin: 'java'

task sourcesJar(type: Jar) {
  from sourceSets.main.allJava
  classifier = 'sources'
}

task javadocJar(type: Jar) {
  from javadoc
}

publishing {

  publications {

    publishJar(MavenPublication) {

      artifact jar

      artifact sourcesJar

      artifact javadocJar {
        // Each artifact must have
        // a unique classifier.
        // We can set the classifier
        // via the task as in sourcesJar
        // or here in the artifact configuration.
        classifier = 'javadoc'
      }

      // Or with a Map notation we
      // can write:
      // artifact source: javadocJar, classifier: 'javadoc'

    }

  }
}

除了使用artifact方法外,我们还可以使用artifacts属性并分配多个工件。我们分配的每个工件都必须具有唯一的classifierextension属性值的组合。在下一个示例构建文件中,我们将使用与上一个示例相同的工件,但这次我们将它们分配给artifacts属性:

apply plugin: 'maven-publish'
apply plugin: 'java'

task sourcesJar(type: Jar) {
  from sourceSets.main.allJava
  classifier = 'sources'
}

task javadocJar(type: Jar) {
  from javadoc
  classifier = 'javadoc'
}

publishing {

  publications {

    publishJar(MavenPublication) {

      // Use artifacts property to
      // define the artifacts.
      // The classifier for each of
      // these artifacts must be
      // unique.
      artifacts = [
        jar,
        sourcesJar,
        javaDocJar]

    }

  }
}

使用文件工件

除了存档任务外,我们还可以使用文件作为工件。Gradle 尝试从文件名中提取extensionclassifier属性。我们也可以在我们将文件添加为发布工件时自行配置这些属性。

在下面的构建文件示例中,我们使用src/files/READMEsrc/files/COPYRIGHT文件作为发布工件:

apply plugin: 'maven-publish'

publishing {
  publications {
    documentation(MavenPublication) {

      // Use file name as a publication artifact.
      artifact 'src/files/README'

      artifact('src/files/COPYRIGHT') {
        // Each file artifact must have a
        // unique classifier and extension.
        classifier = 'metaInformation'
      }

      // Alternative syntax is with
      // the Map notation:
      // artifact source: 'src/files/README'
      // artifact source: 'src/files/COPYRIGHT',
      //          extension: 'metaInformation'

    }
  }
}

使用软件组件

除了artifact方法和artifacts属性外,我们还可以在publications配置块内部使用from方法。我们将SoftwareComponent指定为 Gradle 的from方法的参数。java插件添加了名为javaSoftwareComponent,它包括jar工件和所有运行时依赖项。war插件添加了作为SoftwareComponentwar工件。SoftwareComponent是 Gradle 构建模型的一部分,它定义了一个依赖于其他代码或作为其他代码依赖项的代码片段。

在下一个示例构建文件中,我们将应用war插件到我们的项目中,这将隐式添加java插件。我们还定义了两个发布,每个都使用来自两个插件的SoftwareComponent。以下代码展示了这一点:

apply plugin: 'maven-publish'
apply plugin: 'war'

publishing {

  publications {

    // First publication with
    // the name javaJar, contains
    // the artifact created by the
    // jar task.
    javaJar(MavenPublication) {
      from components.java
    }

    // Second publication with
    // the name webWar, contains
    // the artifact created by
    // the war task.
    webWar(MavenPublication) {
      from components.web
    }

  }

}

生成 POM 文件

Maven 发布的一个重要部分是 POM 文件。我们已经看到 Gradle 为我们项目添加了generatePom<publicationName>任务。此外,我们可以在发布配置内部定义 POM 文件的一些属性。Gradle 还提供了一个钩子来进一步自定义生成的 POM 文件。

Gradle 在生成的 POM 文件中使用项目的versiongroupname属性。我们创建一个新的示例构建文件,在其中定义项目属性,以便它们包含在 POM 文件中。以下代码展示了这一点:

apply plugin: 'maven-publish'
apply plugin: 'java'

// Defined project properties, that are
// used in the generated POM file.
// The name of the project is by default
// the directory name, but we can
// change it via a settings.gradle file
// and the rootProject.name property.
version = '2.1.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(MavenPublication) {
      from components.java
    }
  }
}

现在我们执行generatePomFileForSamplePublication任务。pom-default.xml文件在build/publications/sample目录下创建。如果我们打开该文件,我们可以看到groupIdartifactIdversion元素填充了从我们的 Gradle 构建文件中获取的值。以下代码展示了这一点:

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" 
    >
  <modelVersion>4.0.0</modelVersion>
  <groupId>book.gradle</groupId>
  <artifactId>sample</artifactId>
  <version>2.1.RELEASE</version>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>4.1.4.RELEASE</version>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

我们可以在发布配置内部覆盖groupIdartifactIdversion的值。我们使用groupIdartifactIdversion属性设置不同于从项目属性中获取的默认值。在下一个示例构建文件中,我们将使用这些方法来设置值:

apply plugin: 'maven-publish'
apply plugin: 'java'

version = '2.1.DEVELOPMENT'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(MavenPublication) {
      groupId = 'book.sample.gradle'
      artifactId ='bookSample'
      version = '2.1'

      from components.java
    }
  }
}

再次执行generatePomFileForSamplePublication任务后,我们可以在生成的 POM 文件中看到新值。以下代码展示了这一点:

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" 
    >
  <modelVersion>4.0.0</modelVersion>
  <groupId>book.sample.gradle</groupId>
  <artifactId>bookSample</artifactId>
  <version>2.1</version>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>4.1.4.RELEASE</version>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

你可能已经注意到,generatePomFile<publicationName>Publication 任务还向生成的 POM 文件中添加了一个 dependencies 元素。我们的项目依赖项被添加到 POM 文件中作为运行时依赖项。这是因为我们在发布配置中使用 from 方法并指定 components.java 值。Java 软件组件不仅将 jar 存档任务作为一个工件,还将项目依赖项转换为 Maven 运行时依赖项。如果我们使用存档任务来定义工件,则不会将 dependencies 元素添加到 POM 文件中。

在下面的示例构建文件中,我们使用 artifact 方法来定义发布:

apply plugin: 'maven-publish'
apply plugin: 'java'

// Defined project properties, that are
// used in the generated POM file.
// The name of the project is by default
// the directory name, but we can
// change it via a settings.gradle file
// and the rootProject.name property.
version = '2.1.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(MavenPublication) {
      artifact jar
    }
  }
}

当我们从命令行运行 generatePomFileForSamplePublication 任务时,会生成 POM 文件。现在 POM 文件的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" 
    >
  <modelVersion>4.0.0</modelVersion>
  <groupId>book.gradle</groupId>
  <artifactId>sample</artifactId>
  <version>2.1.RELEASE</version>
</project>

在下一节中,我们将学习如何使用钩子自定义 POM 文件。然后,例如,我们还可以更改项目依赖项的 Maven 依赖项作用域。

自定义 POM 文件

要向生成的 POM 文件添加一些额外元素,我们必须使用 pom 属性,它是 MavenPublication 的一部分。这返回一个 MavenPom 对象,我们可以从这个对象调用 withXml 方法来向 POM 文件添加额外元素。我们将使用 withXml 方法的闭包来访问 XmlProvider 对象。使用 XmlProvider 对象,我们可以通过 asElement 方法获取 DOM 元素的引用,通过 asNode 方法获取 Groovy 节点对象,或者通过 asString 方法获取 StringBuilder 对象来扩展 POM XML。

在下面的示例构建文件中,我们将 organizationissueManagement 元素添加到生成的 POM 文件中:

apply plugin: 'maven-publish'
apply plugin: 'java'

version = '2.1.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(MavenPublication) {
      from components.java

      pom.withXml {

        asNode()
          .appendNode('organization')
          .with {
            appendNode('name', 'Gradle')
            appendNode('url', 'http://www.gradle.org')
        }

        asNode()
          .appendNode('issueManagement')
          .with {
            appendNode('system', 'Jenkins')
            appendNode('url', 'http://buildserver/')
          }
      }
    }
  }
}

如果我们生成 POM 文件,我们可以在 XML 版本中看到我们新创建的元素。这在上面的代码中显示如下:

<?xml version="1.0" encoding="UTF-8"?>
<project  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" >
  <modelVersion>4.0.0</modelVersion>
  <groupId>book.gradle</groupId>
  <artifactId>sample</artifactId>
  <version>2.1.RELEASE</version>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>4.1.4.RELEASE</version>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
  <organization>
    <name>Gradle</name>
    <url>http://www.gradle.org</url>
  </organization>
  <issueManagement>
    <system>Jenkins</system>
    <url>http://buildserver/</url>
  </issueManagement>
</project>

在上一节中,我们已经了解到,如果我们使用 from 方法并指定 components.java 值,所有项目依赖项都会被添加到生成的 POM 文件中作为运行时依赖项。这不一定总是我们想要的。使用 withXml 方法,我们不仅可以添加新元素,还可以更改值。

让我们在其中添加一个钩子,将依赖项的运行时作用域更改为编译作用域。在下一个构建文件中,我们将实现这一点:

apply plugin: 'maven-publish'
apply plugin: 'java'

version = '2.1.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(MavenPublication) {
      from components.java

      pom.withXml {
        asNode()
          .dependencies
          .dependency
          .findAll { dependency ->
            // Find all with scope runtime.
            // Could be more specific if we would
            // have more dependencies. For example
            // check group, name and version.
            dependency.scope.text() == 'runtime'
          }
          .each { dependency ->
            // Set scope value to compile.
            dependency.scope*.value = 'compile'
          }
      }
    }
  }
}

生成的 POM 文件现在具有以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<project  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" >
  <modelVersion>4.0.0</modelVersion>
  <groupId>book.gradle</groupId>
  <artifactId>sample</artifactId>
  <version>2.1.RELEASE</version>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>4.1.4.RELEASE</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>

另一种解决方案是使用 artifact 方法而不是 from 方法来配置发布。这样,dependencies 不会添加到 POM 文件中,因为 Gradle 无法确定工件的依赖项。使用 withXml 方法,我们可以根据项目依赖项自行添加它。

在下面的示例构建文件中,此解决方案得到实现:

apply plugin: 'maven-publish'
apply plugin: 'java'

version = '2.1.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(MavenPublication) {
      artifact jar

      pom.withXml {
        // Create dependencies element.
        def dependencies =
          asNode()
            .appendNode('dependencies')

        project
          .configurations['compile']
          .allDependencies
          ?.each { dependency ->

            // Add a dependency element with
            // groupId, artifactId, version and scope,
            // to the dependencies element.
            dependencies.appendNode('dependency').with {
              appendNode('groupId', dependency.group)
              appendNode('artifactId', dependency.name)
              appendNode('version', dependency.version)
              appendNode('scope', 'compile')
            }

          }
      }
    }
  }
}

当我们调用 generatePomFileForSamplePublication 任务时,我们得到以下 POM 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" >
  <modelVersion>4.0.0</modelVersion>
  <groupId>book.gradle</groupId>
  <artifactId>sample</artifactId>
  <version>2.1.RELEASE</version>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>4.1.4.RELEASE</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>

定义仓库

我们必须配置一个 Maven 仓库来发布我们的配置发布。我们可以选择本地目录或仓库管理器,如 Artifactory 或 Nexus。Gradle 还添加了对将发布安装到我们的本地 Maven 仓库的支持。

发布到本地 Maven 仓库

Gradle 已经将我们的本地 Maven 仓库作为发布的目的地。对于每个命名的发布,都有一个publish<publicationName>ToMavenLocal任务。Gradle 还创建了publishToMavenLocal任务,该任务将所有发布发布到本地 Maven 仓库。

我们有以下示例构建文件:

apply plugin: 'maven-publish'
apply plugin: 'java'

version = '2.1.DEVELOPMENT'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {

  publications {
    publishJar(MavenPublication) {
      artifactId = 'sample'

      from components.java
    }
  }

}

在命令行中,我们将运行publishToMavenLocal任务,并查看哪些任务被执行:

$ gradle publishToMavenLocal
:generatePomFileForPublishJarPublication
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:publishPublishJarPublicationToMavenLocal
:publishToMavenLocal

BUILD SUCCESSFUL

Total time: 5.135 secs

你可能已经注意到,首先使用jar任务及其任务依赖项创建了发布工件。然后生成 POM 文件,我们的发布通过publishPublishJarPublicationToMavenLocal任务复制到本地 Maven 仓库,该任务是publishToMavenLocal的任务依赖项。

当我们查看本地 Maven 仓库目录时,我们看到我们的项目工件已发布:

/Users/mrhaki/.m2/repository/
book
└── gradle
 └── sample
 ├── 2.1.RELEASE
 │   ├── sample-2.1.RELEASE.jar
 │   └── sample-2.1.RELEASE.pom
 └── maven-metadata-local.xml

发布到 Maven 仓库

如果我们有我们自己的公司 Maven 仓库或我们想要发布发布的目录,那么我们必须将其添加到publishing配置块中。在块内部,我们可以添加包含一个或多个命名仓库的repositories配置块。对于每个发布和仓库的组合,Gradle 创建一个具有publish<publicationName>To<repositoryName>Repository名称模式的任务。

在下一个示例构建文件中,我们将定义一个名为localRepo的简单目录仓库:

apply plugin: 'maven-publish'
apply plugin: 'java'

version = '2.1.DEVELOPMENT'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {

  publications {
    publishJar(MavenPublication) {
      artifactId = 'sample'

      from components.java
    }
  }

  // Add a Maven repository for
  // the publications.
  repositories {
    maven {
      name = 'localRepo'
      url = "$buildDir/localRepo"
    }
  }
}

首先,我们将运行tasks任务,以查看哪个任务被添加到Publishing tasks组:

$ gradle tasks
...
Publishing tasks
----------------
generatePomFileForPublishJarPublication - Generates the Maven POM file for publication 'publishJar'.
publish - Publishes all publications produced by this project.
publishPublishJarPublicationToLocalRepoRepository - Publishes Maven publication 'publishJar' to Maven repository 'localRepo'.
publishPublishJarPublicationToMavenLocal - Publishes Maven publication 'publishJar' to the local Maven repository.
publishToMavenLocal - Publishes all Maven publications produced by this project to the local Maven cache.
...
BUILD SUCCESSFUL

Total time: 4.514 secs

要发布我们的项目工件,我们可以执行publishPublishJarPublicationToLocalRepoRepositorypublish任务。以下输出显示了执行的任务:

$ gradle publish
:generatePomFileForPublishJarPublication
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:publishPublishJarPublicationToLocalRepoRepository
Uploading: book/gradle/sample/2.1.DEVELOPMENT/sample-2.1.DEVELOPMENT.jar to repository remote at file:/Users/mrhaki/Projects/book/sample/build/localRepo/
Transferring 2K from remote
Uploaded 2K
:publish

BUILD SUCCESSFUL

Total time: 5.012 secs

一旦执行任务,我们将在build/localRepo目录中获得以下文件:

build/localRepo/
└── book
 └── gradle
 └── sample
 ├── 2.1.DEVELOPMENT
 │   ├── sample-2.1.DEVELOPMENT.jar
 │   ├── sample-2.1.DEVELOPMENT.jar.md5
 │   ├── sample-2.1.DEVELOPMENT.jar.sha1
 │   ├── sample-2.1.DEVELOPMENT.pom
 │   ├── sample-2.1.DEVELOPMENT.pom.md5
 │   └── sample-2.1.DEVELOPMENT.pom.sha1
 ├── maven-metadata.xml
 ├── maven-metadata.xml.md5
 └── maven-metadata.xml.sha1

发布到 Artifactory

要将我们的发布发布到具有 Maven 布局的 Artifactory 仓库,我们只需在publications.repositories配置块中配置仓库。我们可以设置url属性、一个name和可选的安全凭据。

在下一个示例构建文件中,我们使用一个 Artifactory 仓库来发布发布:

apply plugin: 'maven-publish'
apply plugin: 'java'

version = '2.1.DEVELOPMENT'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {

  publications {
    publishJar(MavenPublication) {
      artifactId = 'sample'

      from components.java
    }
  }

  // Add a Artifactory repository for
  // the publications with Maven layout.
  repositories {
    maven {
      name = 'artifactory'
      url = "http://localhost:8081/artifactory/libs-release-local"

      // Username and password should be
      // saved outside build file in
      // real life, eg. in gradle.properties
      // or passed via command line as 
      // project properties. 
      credentials {
        username = 'user'
        password = 'passw0rd'
      }
    }
  }
}

Gradle 根据发布名称和仓库名称创建一个新的publishPublishJarPublicationToArtifactoryRepository任务。当我们调用任务时,我们可以看到发布已部署到 Artifactory 仓库。以下代码显示了这一点:

$ gradle publishPublishJarPublicationToArtifactoryRepository
:generatePomFileForPublishJarPublication
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:publishPublishJarPublicationToArtifactoryRepository
Uploading: book/gradle/sample/2.1.DEVELOPMENT/sample-2.1.DEVELOPMENT.jar to repository remote at http://localhost:8081/artifactory/libs-release-local
Transferring 2K from remote
Uploaded 2K

BUILD SUCCESSFUL

Total time: 5.012 secs

当我们在网页浏览器中打开 Artifactory 网络应用程序时,我们可以看到我们的项目现在是仓库的一部分,如下面的截图所示:

发布到 Artifactory

发布到 Nexus

另一个仓库管理器是 Nexus。将发布物发布到 Nexus 仓库管理器与发布到 Artifactory 或本地目录并没有太大的区别。我们只需要更改 url 属性以引用仓库,并设置可选的安全凭据。

在以下示例构建文件中,我们使用 Nexus 仓库管理器:

apply plugin: 'maven-publish'
apply plugin: 'java'

version = '2.1.DEVELOPMENT'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {

  publications {
    publishJar(MavenPublication) {
      artifactId = 'sample'

      from components.java
    }
  }

  // Add a Maven repository for
  // the publications.
  repositories {
    maven {
      name = 'nexus'
      url = "http://localhost:8081/nexus/content/repositories/releases"
      credentials {
        username = 'admin'
        password = 'admin123'
      }
    }
  }
}

这次,创建了 publishPublishJarPublicationToNexusRepository 任务。该任务也被添加为 publish 任务的依赖任务。为了完成这个任务,请使用以下代码:

$ gradle publishPublishJarPublicationToNexusRepository
:generatePomFileForPublishJarPublication
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:publishPublishJarPublicationToNexusRepository
Uploading: book/gradle/sample/2.1.DEVELOPMENT/sample-2.1.DEVELOPMENT.jar to repository remote at http://localhost:8081/nexus/content/repositories/releases
Transferring 2K from remote
Uploaded 2K

BUILD SUCCESSFUL

Total time: 5.012 secs

当我们在仓库内部查看 Nexus 网络应用程序时,我们可以看到我们的项目被添加到了仓库中,如下面的截图所示:

发布到 Nexus

摘要

在本章中,你学习了如何使用新的和正在发展的 maven-publish 插件。你看到了如何使用 publications 配置块声明你的发布物。Gradle 将会根据你声明的发布物自动创建新的任务。

你还学习了如何自定义 Gradle 发布任务生成的 POM 文件。

最后,你看到了如何配置 Maven 仓库,以便你可以将你的发布物部署到它们。我们配置了一个本地目录,这也可以是一个网络共享,并展示了如何配置 Artifactory 或 Nexus 仓库管理器。

在下一章中,你将看到如何上传到 Bintray。

第六章:发布到 Bintray

在上一章中,我们学习了如何将我们的工件部署到 Maven 仓库。在这一章中,你将学习我们如何将工件部署到 Bintray 作为 Maven 仓库。我们将了解 Bintray 是什么以及它如何帮助我们发布项目。

我们将了解如何配置 Gradle Bintray 插件来部署我们的工件。

什么是 Bintray?

Bintray 将自己宣传为“服务即分发”。这意味着当我们有想要分发的物品时,例如我们的项目工件,我们可以使用 Bintray。Bintray 提供了一个平台来存储我们想要共享的软件,并使其对其他人可下载。围绕这一点,有许多工具可以提供关于包如何分发和使用的见解。Bintray 还提供了一个 REST API,使其与平台协同工作变得容易。运行 Bintray 的公司是 JFrog,该公司因其仓库产品 Artifactory 而非常知名。

Bintray 的一部分称为 JCenter。JCenter 在 Bintray 平台上托管 Java 依赖项。我们已经了解了 JCenter 作为依赖项的仓库主机。然而,我们也可以使用 JCenter 作为我们自己的依赖项的分发仓库。在这一章中,我们将使用 JCenter 来部署我们的工件。

定义新的仓库

在我们能够使用 Bintray 的 JCenter 之前,我们必须在bintray.com上创建一个 Bintray 账户。其中一种最简单的方法是使用现有的 GitHub 账户登录。

接下来,我们将创建一个新的仓库,我们将在此存储我们的工件。因此,首先我们登录到 Bintray。从我们的用户页面,我们将选择新建仓库选项。在我们的浏览器窗口中,我们可以看到一些需要填写的字段,如下面的屏幕截图所示:

定义新的仓库

我们需要为我们的仓库提供一个名称和可选的描述。我们选择Maven作为仓库类型。Bintray 也可以用于其他类型的依赖项,但对我们来说,Java 代码我们想要使用 Maven。在填写完所有字段后,我们点击创建按钮,Bintray 就会创建一个新的空仓库。在下一个屏幕截图中,我们将看到我们新创建的仓库:

定义新的仓库

定义 Bintray 插件

为了将我们的工件部署到 JCenter,我们使用 Bintray Gradle 插件。此插件为我们项目添加了额外的功能,以便发布我们的工件。

让我们继续使用之前项目的示例构建文件。这个构建文件是为一个包含一些代码的 Java 项目准备的。我们将使用发布插件来定义项目的发布或工件。现在,我们将通过使用buildscript配置块将 Gradle 插件添加到项目中。在下一个示例构建文件中,我们将应用 Bintray 插件到我们的项目中。以下代码展示了这一点:

// Define Bintray plugin.
buildscript {
  repositories {
    jcenter()
  }

  dependencies {
    classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.1
  }
}

// Apply plugin to project.
apply plugin: 'com.jfrog.bintray'

apply plugin: 'maven-publish'
apply plugin: 'java'

version = '1.0.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(MavenPublication) {
      from components.java
    }
  }
}

由于 Gradle 2.1 的发布,我们使用了一种替代语法来在我们的构建脚本中包含外部插件。新的语法适用于部署到 Gradle 插件门户的插件。这个特性是孵化中的,这意味着它将来可能会改变。另外,一个重要的限制是,新的语法在subprojectsallprojects配置块中不受支持。在下面的示例构建文件中,使用了添加插件的新语法:

// Define and apply Bintray plugin.
plugins {
  id 'com.jfrog.bintray' version '1.0'
}

在我们的项目中添加了新的插件后,我们可以运行tasks命令来查看插件添加了哪些任务:

$ gradle tasks
...
Publishing tasks
----------------
bintrayUpload - Publishes artifacts to bintray.com.
...

我们注意到插件为我们项目添加了bintrayUpload任务。

将发布部署到 Bintray

在我们能够运行bintrayUpload任务之前,我们必须在我们的 Gradle 构建文件中添加一些配置。Bintray 插件可以通过bintray配置块进行配置。在这个配置块内部,我们可以看到部署我们的项目发布到 Bintray 所需的所有属性。

首先,我们需要设置用于部署的 Bintray 账户的用户名和 API 密钥。要获取 API 密钥,我们必须首先在我们的网页浏览器中登录到 Bintray。从我们的账户页面,我们点击编辑按钮。接下来,我们将选择API菜单选项以获取我们的 API 密钥。然后,我们可以将密钥复制到剪贴板,以便我们可以在构建脚本中使用它。bintray配置块中的userkey属性包含我们不希望分享的信息。最好将这些属性的值从我们的 Gradle 构建文件外部化。我们可以在项目目录中添加一个gradle.properties文件,包含属性的值。gradle.properties文件也可以添加到我们的 Gradle 用户主目录中,默认情况下是<user_home>/.gradle。或者,我们可以使用命令行选项-P--project-prop来设置值。

需要userkey属性。此外,我们必须设置pkg.repo属性,使用我们在 Bintray 中的仓库名称,并将pkg.name设置为部署的组名。最后,我们需要定义我们需要发布的内容。幸运的是,Bintray 插件支持 Gradle 发布插件,因此我们可以重用我们在构建文件中配置的发布。

在下面的示例构建文件中,我们在bintray配置块中配置了 Bintray 插件:

// Define Bintray plugin.
buildscript {
  repositories {
    jcenter()
  }

  dependencies {
    classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.0'
  }
}

// Apply plugin to project.
apply plugin: 'com.jfrog.bintray'

apply plugin: 'maven-publish'
apply plugin: 'java'

version = '1.0.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(MavenPublication) {
      from components.java
    }
  }
}

bintray {

  // Use externalized project property bintrayUsername.
  user = bintrayUsername

  // Use externalized project property bintrayApiKey.
  key = bintrayApiKey

  // Define publication that needs to be published
  // to Bintray.
  publications = ['sample']

  pkg {
    // Name of repository in Bintray
    repo = 'book-sample'

    // Name for package.
    name = 'sample'
  }

}

在我们能够上传我们的工件之前,我们必须首先使用 Bintray 的网页界面通过sample包创建一个名为book-sample的仓库。我们需要使用我们的账户登录,然后选择新建仓库链接。在下面的截图中,我们可以看到需要填写的字段:

将发布部署到 Bintray

在我们创建了一个新的仓库之后,我们可以看到仓库的概览,如下面的截图所示:

将发布部署到 Bintray

从这个屏幕,我们点击新建软件包按钮。一个仓库可以包含多个软件包。下一个截图显示了我们需要填写以创建新软件包的字段。我们必须设置名称许可证字段和一个版本控制链接。

将出版物部署到 Bintray

一旦我们在我们的仓库中创建了软件包,我们就可以为我们的项目调用bintrayUpload任务。让我们看看调用任务时的输出:

$ gradle bintrayUpload
:generatePomFileForSamplePublication
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:publishSamplePublicationToMavenLocal
:bintrayUpload

BUILD SUCCESSFUL

Total time: 9.125 secs

我们注意到bintrayUpload任务依赖于在文件上传之前必须首先编译和构建我们的工件的任务。我们不必自己定义这个任务依赖。

一切构建成功,我们现在可以打开我们的网络浏览器并转到仓库和软件包页面。在以下截图中,我们看到我们的更新后的软件包网页:

将出版物部署到 Bintray

Bintray 首先将上传的文件放入未发布状态。这样,我们有时间决定我们是否真的想要发布这个版本。我们可以在屏幕上看到消息。我们确信我们想要发布这个版本,所以我们将点击发布链接。现在,文件已发布,以便其他人也可以看到文件。如果我们点击发布版本,我们甚至可以在我们的网络浏览器中看到实际的文件。以下截图显示了我们的已发布工件 JAR 文件和相应的 POM 文件:

将出版物部署到 Bintray

配置 Bintray 插件

我们已经配置了必要的配置属性,以便将我们的项目发布到 Bintray。然而,插件允许更多的配置。我们可以在以下表中看到配置属性:

名称 描述
user 这设置了 Bintray 用户名。
key 这设置了 API 密钥。
configurations 这定义了具有可部署文件的配置列表。
publications 这定义了要部署的出版物列表。
filesSpec 使用CopySpec来定义要发布的任意文件,这些文件不是出版物或配置的一部分。
dryRun 这允许您执行所有任务而不部署它们。
publish 在上传后是否应该发布版本,而不是通过网络浏览器发布。
pkg.repo 这是仓库的名称。
pkg.name 这是软件包的名称。
pkg.userOrg 当仓库属于组织时,这是可选的组织名称。
pkg.desc 这是对软件包的描述。
pkg.websiteUrl 这是属于项目的网站 URL。
pkg.issueTrackerUrl 这是用于项目的缺陷跟踪系统的 URL。
pkg.vcsUrl 这是使用的版本控制系统的 URL。
pkg.licenses 这是此项目的有效许可证列表。
pkg.labels 这是描述项目内容的标签列表。
pkg.publicDownloadNumbers 这显示了已发布文件被下载的次数。
pkg.attributes 这是包的定制属性映射。
pkg.version.name 这是自定义的 Bintray 版本。
pkg.version.desc 这是针对此版本的具体描述。
pkg.version.released 这是发布日期。
pkg.version.vcsTag 这是版本控制系统中此版本的标签。
pkg.version.attributes 这些是此版本包的定制属性。
pkg.version.gpg.sign 将此设置为 true 以使用 GPG 签名。
pkg.version.gpg.passphrase 这是 GPG 签名的密码。
pkg.version.mavenCentralSync.sync 将此设置为 true 以与 Maven Central 同步。
pkg.version.mavenCentralSync.user 这是与 Maven Central 同步的用户令牌。
pkg.version.mavenCentralSync.password 这是与 Maven Central 同步用户的密码。
pkg.version.mavenCentralSync.close 默认情况下,暂存库是关闭的,并且工件被发布到 Maven Central。您可以选择性地关闭此行为(通过将值设置为 0)并手动发布版本。

在下面的示例构建文件中,我们将使用一些这些配置属性:

// Define Bintray plugin.
buildscript {
  repositories {
    jcenter()
  }

  dependencies {
    classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.0'
  }
}

// Apply plugin to project.
apply plugin: 'com.jfrog.bintray'

apply plugin: 'maven-publish'
apply plugin: 'java'

version = '1.0.2.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(MavenPublication) {
      from components.java
    }
  }
}

bintray {
  user = bintrayUsername
  key = bintrayApiKey
  publications = ['sample']

  publish = true

  pkg {
    repo = 'book-sample'
    name = 'full-sample'

    desc = 'Sample package for Gradle book.'

    websiteUrl = 'https://github.com/mrhaki/gradle-dep-book/'
    issueTrackerUrl = 'https://github.com/mrhaki/gradle-dep-book/issues'
    vcsUrl = 'https://github.com/mrhaki/gradle-dep-book.git'

    licenses = ['Apache-2.0']

    labels = ['book', 'sample', 'Gradle']

    publicDownloadNumbers = true

    version {
      desc = 'Fixed some issues.'
      released = new Date()
    }

  }

}

如果我们定义了 vcsUrllicenses 配置属性,那么插件将自动在我们的仓库中创建包,这是很好的。因此,我们不必使用网络浏览器来创建新的包。相反,我们可以使用构建脚本中的配置来自动创建包。此外,请注意,包是自动发布的,与第一个例子中的未发布状态不同。

摘要

在本章中,你学习了如何使用第三方 Bintray 插件。我们了解了 Bintray 和 JCenter。我们看到了如何在 Bintray 上创建仓库,并使用它将我们的项目作为包部署到该仓库。

你学习了可以为 Bintray 部署设置的不同的配置属性。

在下一章中,我们将看到如何上传到 Ivy 仓库。

第七章:发布到 Ivy 仓库

在前一章中,您学习了如何将我们的项目工件发布到 Maven 仓库。在本章中,我们将使用一个 Ivy 仓库进行发布。

就像发布到 Maven 仓库一样,我们将使用 Gradle 中新的和孵化中的发布功能来发布到 Ivy 仓库。

定义发布内容

我们必须将ivy-publish插件添加到我们的项目中,以便能够将我们的工件发布到 Ivy 仓库。该插件允许我们使用 Ivy 格式来描述需要发布的工件。

ivy-publish插件基于通用的publishing插件。publishing插件为我们项目添加了一个新的publishing扩展。我们可以在构建脚本中使用publications配置块来配置我们想要发布的工件和想要部署到的仓库。publications扩展在org.gradle.api.publish包中有PublishingExtension类型。该插件还向项目添加了一个通用的生命周期publish任务。可以将其他任务作为任务依赖添加到这个任务中,因此,通过单个publish任务,可以发布项目的所有发布内容。

ivy-publish插件还为项目添加了一些额外的任务规则。有一个任务用于为项目中的每个发布内容生成 Ivy 描述符文件。插件还基于发布内容和仓库的组合添加了一个任务规则,用于将发布内容发布到指定的仓库。

让我们创建一个示例构建文件,并将ivy-publish插件应用到项目中,以查看新任务:

apply plugin: 'ivy-publish'
apply plugin: 'java'

现在,我们将从命令行调用tasks任务:

$ gradle tasks
...
Publishing tasks
----------------
publish - Publishes all publications produced by this project.
...
BUILD SUCCESSFUL

Total time: 4.589 secs

在输出中,我们看到publish任务。用于将单个发布内容发布到仓库的动态任务规则没有显示。

要配置我们的发布内容,我们首先需要添加一个publishing配置块。在该块内部,我们将定义publications配置块。在这个块中,我们定义一个发布内容。发布内容定义了需要发布的内容。ivy-publish插件期望在org.gradle.api.publish.ivy包中找到一个IvyPublication类型的发布内容。除了需要发布的工件之外,我们还可以定义生成的 Ivy 描述符文件的详细信息。

定义发布工件

我们定义的发布内容必须在我们的项目中具有唯一名称。我们可以在publications配置块内部添加多个具有自己名称的发布内容。要添加工件,我们可以在发布内容定义中使用artifact方法。我们还可以使用artifacts属性直接设置所有工件。

我们可以使用以下表格中描述的方式,使用artifact方法定义工件:

类型 描述
AbstractArchiveTask 工件信息是从归档任务中提取的。工件是org.gradle.api.artifacts包中的PublishArtifact实例。
File 工件的信息是从文件名中提取的。
Map 这是一种定义工件的其他方式。该映射必须包含一个source键,它引用一个文件或归档任务,以及其他我们可以使用的属性,例如classifierextension,以进一步配置工件。

使用归档任务工件

在以下示例构建文件中,我们定义了一个名为publishJar的新出版物,并定义了jar归档任务的输出作为工件:

apply plugin: 'ivy-publish'
apply plugin: 'java'

// Configuration block for publishing
// artifacts from the project.
publishing {

  // Define publications with what
  // needs to be published.
  publications {

    // Name of this publication
    // is publishJar.
    publishJar(IvyPublication) {

      // Use output of jar task
      // as the artifact for
      // the publication.
      artifact jar

      // Alternatively we can use
      // a Map notation:
      // artifact source: jar
    }

  }
}

接下来,我们运行tasks任务,并在输出中我们看到为发布此出版物生成的新任务:

$ gradle tasks
...
Publishing tasks
----------------
generateDescriptorFileForPublishJarPublication - Generates the Ivy Module Descriptor XML file for publication 'publishJar'.
publish - Publishes all publications produced by this project.
...
BUILD SUCCESSFUL

Total time: 4.215 secs

注意额外的任务,generateDescriptorFileForPublishJarPublication。这个任务的名称用于publishJar出版物。Gradle 使用以下模式为generateDescriptorFileFor<publicationName>Publication出版物生成一个任务以生成 Ivy 描述符 XML 文件。我们目前还不能调用这个任务,因为我们还需要设置groupversion项目属性,但我们将在这部分关于生成 Ivy 描述符文件的章节中看到这一点。现在,我们将专注于在本节中定义出版物的工件。

我们不局限于为出版物使用一个工件;我们可以通过多次调用artifact方法来添加更多。我们甚至可以使用artifacts属性来分配多个工件。对于单个出版物,每个工件都必须具有唯一的classifierextension属性值。在我们可以调用任何任务之前,Gradle 会检查这一点,所以当工件没有唯一的classifierextensions属性值的组合时,我们会立即收到错误信息。

在以下示例构建文件中,我们使用artifact方法向我们的出版物添加了两个额外的工件:

apply plugin: 'ivy-publish'
apply plugin: 'java'

task sourcesJar(type: Jar) {
  from sourceSets.main.allJava
  classifier = 'sources'
}

task javadocJar(type: Jar) {
  from javadoc
}

publishing {

  publications {

    publishJar(IvyPublication) {

      artifact jar

      // Add output of sourcesJar task
      // as an artifacts. In the task
      // the classifier is already
      // set to sources.
      artifact sourcesJar

      artifact javadocJar {
        // Each artifact must have
        // a unique classifier.
        // We can set the classifier
        // via the task as in sourcesJar
        // or here in the artifact configuration.
        classifier = 'javadoc'
      }

      // Or with a Map notation we
      // can write:
      // artifact source: javadocJar, classifier: 'javadoc'

    }

  }
}

除了使用artifact方法之外,我们还可以使用artifacts属性并分配多个工件。我们分配的每个工件都必须具有唯一的classifierextension属性值的组合。在下一个示例构建文件中,我们将使用与上一个示例相同的工件,但这次我们将它们分配给artifacts属性:

apply plugin: 'ivy-publish'
apply plugin: 'java'

task sourcesJar(type: Jar) {
  from sourceSets.main.allJava
  classifier = 'sources'
}

task javadocJar(type: Jar) {
  from javadoc
  classifier = 'javadoc'
}

publishing {

  publications {

    publishJar(IvyPublication) {

      // Use artifacts property to
      // define the artifacts.
      // The classifier for each of
      // these artifacts must be
      // unique.
      artifacts = [
        jar,
        sourcesJar,
        javaDocJar]

    }

  }
}

使用文件工件

除了归档任务之外,我们还可以使用文件作为工件。Gradle 会尝试从文件名中提取extensionclassifier属性。当我们添加文件作为发布工件时,我们也可以自己配置这些属性。

在以下示例构建文件中,我们使用src/files/READMEsrc/files/COPYRIGHT文件作为发布工件:

apply plugin: 'ivy-publish'

publishing {
  publications {
    documentation(IvyPublication) {

      // Use file name as a publication artifact.
      artifact 'src/files/README'

      artifact('src/files/COPYRIGHT') {
        // Each file artifact must have a
        // unique classifier and extension.
        classifier = 'metaInformation'
      }

      // Alternative syntax is with
      // the Map notation:
      // artifact source: 'src/files/README'
      // artifact source: 'src/files/COPYRIGHT',
      //          extension: 'metaInformation'

    }
  }
}

使用软件组件

除了artifact方法和artifacts属性外,我们还可以在publications配置块中使用from方法。我们指定一个 Gradle SoftwareComponent对象作为from方法的参数。java插件添加了一个名为javaSoftwareComponent对象,它包括jar归档任务和所有运行时依赖项。war插件添加了war归档任务作为SoftwareComponent对象。

在下一个示例构建文件中,我们将war插件应用到我们的项目中。war插件扩展了java插件,因此我们也将隐式地应用到我们的项目中java插件。我们还将定义两个发布,每个都使用来自两个插件的SoftwareComponent对象:

apply plugin: 'ivy-publish'
apply plugin: 'war'

publishing {

  publications {

    // First publication with
    // the name javaJar, contains
    // the artifact created by the
    // jar task.
    javaJar(IvyPublication) {
      from components.java
    }

    // Second publication with
    // the name webWar, contains
    // the artifact created by
    // the war task.
    webWar(IvyPublication) {
      from components.web
    }

  }

}

生成 Ivy 描述符文件

Ivy 发布的的一个重要部分是描述符文件。我们已经看到 Gradle 为我们项目添加了一个generateDescriptorFile<publicationName>任务。此外,我们可以在发布配置中定义描述符文件的一些属性。Gradle 还提供了一个钩子来进一步自定义生成的描述符文件。

Gradle 使用项目的versiongroupnamestatus属性在生成的 Ivy 描述符文件中的info元素。我们将创建一个新的示例构建文件,在其中定义项目属性,因此它们将被包含在文件中:

apply plugin: 'ivy-publish'
apply plugin: 'java'

// Defined project properties, that are
// used in the generated descriptor file.
// The name of the project is by default
// the directory name, but we can
// change it via a settings.gradle file
// and the rootProject.name property.
version = '2.1.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(IvyPublication) {
      from components.java
    }
  }
}

现在,我们执行generateDescriptorFileForSamplePublication任务。在build/publications/sample目录中创建了一个ivy.xml文件。如果我们打开文件,我们可以看到info元素属性被填充了来自我们的 Gradle 构建文件中的值。下面的代码显示了这一点:

<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0">
  <info organisation="book.gradle" module="sample" revision="2.1.RELEASE" status="integration" publication="20150424051601"/>
  <configurations>
    <conf name="default" visibility="public" extends="runtime"/>
    <conf name="runtime" visibility="public"/>
  </configurations>
  <publications>
    <artifact name="sample" type="jar" ext="jar" conf="runtime"/>
  </publications>
  <dependencies>
    <dependency org="org.springframework" name="spring-context" rev="4.1.4.RELEASE" conf="runtime-&gt;default"/>
  </dependencies>
</ivy-module>

我们可以在发布配置中覆盖organisationmodulerevisionstatusbranch的值。我们需要在IvyPublication的配置块中设置属性。statusbranch属性需要通过descriptor属性设置。通过descriptor属性,我们还可以向 Ivy 描述符文件中的info元素添加新的子元素。在下一个示例构建文件中,我们将使用这些方法来设置值:

apply plugin: 'ivy-publish'
apply plugin: 'java'

version = '2.1.DEVELOPMENT'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(IvyPublication) {
      organisation = 'book.sample.gradle'
      module ='bookSample'
      version = '2.1'
      descriptor.status = 'published'
      descriptor.branch = 'n/a'

      // Add extra element as child
      // for info.
      descriptor.extraInfo '', 'ivyauthor', 'Hubert Klein Ikkink'

      from components.java
    }
  }
}

我们再次执行generateDescriptorFileForSamplePublication任务,如下面的代码所示,我们可以在生成的 Ivy 描述符文件中看到新的值:

<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0">
  <info organisation="book.sample.gradle" module="bookSample" branch="n/a" revision="2.1.DEVELOPMENT" status="published" publication="20150424053039">
    <ns:ivyauthor xmlns:ns="">Hubert Klein Ikkink</ns:ivyauthor>
  </info>
  <configurations>
    <conf name="default" visibility="public" extends="runtime"/>
    <conf name="runtime" visibility="public"/>
  </configurations>
  <publications>
    <artifact name="bookSample" type="jar" ext="jar" conf="runtime"/>
  </publications>
  <dependencies>
    <dependency org="org.springframework" name="spring-context" rev="4.1.4.RELEASE" conf="runtime-&gt;default"/>
  </dependencies>
</ivy-module>

我们项目的依赖项被添加到生成的描述符文件中的依赖项中。这是因为我们在发布配置中使用from方法并指定components.java值。Java 软件组件不仅添加了jar归档任务作为归档,还将项目依赖项转换为描述符文件中的依赖项。如果我们使用归档任务来定义归档,则不会添加dependencies元素。

在下面的示例构建文件中,我们使用artifact方法来定义发布:

apply plugin: 'ivy-publish'
apply plugin: 'java'

// Defined project properties, that are
// used in the generated descriptor file.
// The name of the project is by default
// the directory name, but we can
// change it via a settings.gradle file
// and the rootProject.name property.
version = '2.1.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(IvyPublication) {
      artifact jar
    }
  }
}

当我们从命令行运行generateDescriptorFileForSamplePublication任务时,Ivy 描述符文件被生成。现在文件的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0">
  <info organisation="book.gradle" module="sample" revision="2.1.RELEASE" status="integration" publication="20150424053351"/>
  <configurations/>
  <publications>
    <artifact name="sample" type="jar" ext="jar"/>
  </publications>
  <dependencies/>
</ivy-module>

在下一节中,你将学习如何使用descriptor属性的withXml方法来自定义描述符。然后,例如,我们还可以更改项目依赖项的依赖范围。

自定义描述符文件

要向生成的文件中添加额外元素,我们必须使用IvyPublication的一部分descriptor属性。这返回一个IvyModuleDescriptorSpec对象,我们将从这个对象调用withXml方法来向描述符文件添加额外元素。我们使用一个闭包与withXml方法来访问一个XmlProvider对象。有了XmlProvider对象,我们可以通过asElement方法获取一个 DOM 元素的引用,通过asNode方法获取一个 Groovy 节点对象,或者通过asString方法获取一个StringBuilder对象来扩展描述符 XML。

在以下示例构建文件中,我们将descriptionissueMangement元素添加到生成的描述符文件中:

apply plugin: 'ivy-publish'
apply plugin: 'java'

version = '2.1.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(IvyPublication) {
      from components.java

      // Customize generated descriptor XML.
      descriptor.withXml {

        asNode()
          .appendNode('description', 
                'Sample Gradle project')

        asNode()
          .appendNode('issueManagement')
          .with {
            appendNode('system', 'Jenkins')
            appendNode('url', 'http://buildserver/')
          }
      }
    }
  }
}

如果我们生成 Ivy 描述符文件,我们可以在 XML 版本中看到我们新创建的元素:

<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0">
  <info organisation="book.gradle" module="sample" revision="2.1.RELEASE" status="integration" publication="20150424053914"/>
  <configurations>
    <conf name="default" visibility="public" extends="runtime"/>
    <conf name="runtime" visibility="public"/>
  </configurations>
  <publications>
    <artifact name="sample" type="jar" ext="jar" conf="runtime"/>
  </publications>
  <dependencies>
    <dependency org="org.springframework" name="spring-context" rev="4.1.4.RELEASE" conf="runtime-&gt;default"/>
  </dependencies>
  <description>Sample Gradle project</description>
  <issueManagement>
    <system>Jenkins</system>
    <url>http://buildserver/</url>
  </issueManagement>
</ivy-module>

在上一节中,你已经了解到,如果我们使用带有components.java值的from方法,所有项目依赖项都会被添加到生成的描述符文件中作为运行时依赖项。这可能不是我们总是想要的。使用withXml方法,我们不仅可以添加新元素,还可以更改值。

让我们在 info 元素中更改模块属性的地方添加一个钩子。在下一个构建文件中,我们将实现这一点:

apply plugin: 'ivy-publish'
apply plugin: 'java'

version = '2.1.RELEASE'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {
  publications {
    sample(IvyPublication) {
      from components.java

      descriptor.withXml {
        // Replace value for module attribute
        // in info element.
        new StringBuilder(
          asString()
            .replaceAll(
              /module="sample"/,
              'module="ivyChapter"'))
      }
    }
  }
}

生成的描述符文件现在有以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0">
  <info organisation="book.gradle" module="ivyChapter" revision="2.1.RELEASE" status="integration" publication="20150424055754"/>
  <configurations>
    <conf name="default" visibility="public" extends="runtime"/>
    <conf name="runtime" visibility="public"/>
  </configurations>
  <publications>
    <artifact name="sample" type="jar" ext="jar" conf="runtime"/>
  </publications>
  <dependencies>
    <dependency org="org.springframework" name="spring-context" rev="4.1.4.RELEASE" conf="runtime-&gt;default"/>
  </dependencies>
</ivy-module>

定义仓库

我们必须配置一个 Ivy 仓库来发布我们的配置出版物。我们可以选择本地目录或仓库管理器,如 Artifactory 或 Nexus。

发布到本地目录

如果我们有一个想要发布我们出版物的目录,我们必须将其添加到publishing配置块中。在块内部,我们添加一个包含一个或多个命名仓库的repositories配置块。对于每个出版和仓库的组合,Gradle 都会创建一个名为publish<publicationName>To<repositoryName>Repository的任务。

在下一个示例构建文件中,我们定义一个名为localRepo的简单目录仓库:

apply plugin: 'ivy-publish'
apply plugin: 'java'

version = '2.1.DEVELOPMENT'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {

  publications {
    publishJar(IvyPublication) {
      module = 'sample'

      from components.java
    }
  }

  // Add a local director as repository
  // for the publications.
  repositories {
    ivy {
      name = 'localRepo'
      url = "$buildDir/localRepo"
    }
  }
}

首先,我们运行tasks任务来查看哪些任务被添加到Publishing tasks组:

$ gradle tasks
...
Publishing tasks
----------------
generateDescriptorFileForPublishJarPublication - Generates the Ivy Module Descriptor XML file for publication 'publishJar'.
publish - Publishes all publications produced by this project.
publishPublishJarPublicationToLocalRepoRepository - Publishes Ivy publication 'publishJar' to Ivy repository 'localRepo'.
...
BUILD SUCCESSFUL

Total time: 11.514 secs

要发布我们的项目工件,我们可以执行publishPublishJarPublicationToLocalRepoRepositorypublish任务。以下输出显示了执行的任务:

$ gradle publish
:generateDescriptorFileForPublishJarPublication
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:publishPublishJarPublicationToLocalRepoRepository
:publish

BUILD SUCCESSFUL

Total time: 6.383 secs

一旦任务运行完成,我们将在build/localRepo目录中获得以下文件:

build/localRepo/
└── book.gradle
 └── sample
 └── 2.1.DEVELOPMENT
 ├── ivy-2.1.DEVELOPMENT.xml
 ├── ivy-2.1.DEVELOPMENT.xml.sha1
 ├── sample-2.1.DEVELOPMENT.jar
 └── sample-2.1.DEVELOPMENT.jar.sha1

发布到 Artifactory

要将我们的发布内容发布到 Artifactory 存储库,我们只需在publications.repositories配置块中配置存储库。我们可以设置url属性、一个name以及可选的安全凭据。

在下一个示例构建文件中,我们将使用 Artifactory 存储库来发布发布内容:

apply plugin: 'ivy-publish'
apply plugin: 'java'

version = '2.1.DEVELOPMENT'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {

  publications {
    publishJar(IvyPublication) {
      module = 'sample'

      from components.java
    }
  }

  // Add a Artifactory repository for
  // the publications with Maven layout.
  repositories {
    ivy {
      name = 'artifactory'
      url = "http://localhost:8081/artifactory/libs-release-local"

      // Username and password should be
      // saved outside build file in
      // real life, eg. in gradle.properties.
      credentials {
        username = 'user'
        password = 'passw0rd'
      }
    }
  }
}

Gradle 根据发布名称和存储库名称创建了一个新的任务publishPublishJarPublicationToArtifactoryRepository。当我们调用该任务时,我们可以看到发布已部署到 Artifactory 存储库,如下面的代码所示:

$ gradle publishPublishJarPublicationToArtifactoryRepository
:generateDescriptorFileForPublishJarPublication
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:publishPublishJarPublicationToArtifactoryRepository
Upload http://localhost:8081/artifactory/libs-release-local/book.gradle/sample/2.1.DEVELOPMENT/sample-2.1.DEVELOPMENT.jar
Upload http://localhost:8081/artifactory/libs-release-local/book.gradle/sample/2.1.DEVELOPMENT/sample-2.1.DEVELOPMENT.jar.sha1
Upload http://localhost:8081/artifactory/libs-release-local/book.gradle/sample/2.1.DEVELOPMENT/ivy-2.1.DEVELOPMENT.xml
Upload http://localhost:8081/artifactory/libs-release-local/book.gradle/sample/2.1.DEVELOPMENT/ivy-2.1.DEVELOPMENT.xml.sha1

BUILD SUCCESSFUL

Total time: 12.214 secs

当我们在 Web 浏览器中打开 Artifactory Web 应用程序时,我们可以看到我们的项目现在是存储库的一部分,如下面的截图所示:

发布到 Artifactory

发布到 Nexus

另一个存储库管理器是 Nexus。将发布到 Nexus 存储库管理器与发布到 Artifactory 或本地目录没有太大区别。我们只需更改url属性以引用存储库并设置正确可选的安全凭据。

在以下示例构建文件中,我们使用 Nexus 存储库管理器:

apply plugin: 'ivy-publish'
apply plugin: 'java'

version = '2.1.DEVELOPMENT'
group = 'book.gradle'

repositories {
  jcenter()
}

dependencies {
  compile 'org.springframework:spring-context:4.1.4.RELEASE'
}

publishing {

  publications {
    publishJar(IvyPublication) {
      module = 'sample'

      from components.java
    }
  }

  // Add a Nexus repository for
  // the publications.
  repositories {
    ivy {
      name = 'nexus'
      url = "http://localhost:8081/nexus/content/repositories/releases"
      credentials {
        username = 'admin'
        password = 'admin123'
      }
    }
  }
}

这次,创建了publishPublishJarPublicationToNexusRepository任务。该任务也被添加为publish任务的依赖任务。以下代码展示了这一点:

$ gradle publishPublishJarPublicationToNexusRepository
:generateDescriptorFileForPublishJarPublication
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:publishPublishJarPublicationToNexusRepository
Upload http://localhost:8081/nexus/content/repositories/releases/book.gradle/sample/2.1.DEVELOPMENT/sample-2.1.DEVELOPMENT.jar
Upload http://localhost:8081/nexus/content/repositories/releases/book.gradle/sample/2.1.DEVELOPMENT/sample-2.1.DEVELOPMENT.jar.sha1
Upload http://localhost:8081/nexus/content/repositories/releases/book.gradle/sample/2.1.DEVELOPMENT/ivy-2.1.DEVELOPMENT.xml
Upload http://localhost:8081/nexus/content/repositories/releases/book.gradle/sample/2.1.DEVELOPMENT/ivy-2.1.DEVELOPMENT.xml.sha1

BUILD SUCCESSFUL

Total time: 5.746 secs

当我们查看存储库中的 Nexus Web 应用程序时,我们可以看到我们的项目已添加到存储库中,如下面的截图所示:

发布到 Nexus

摘要

在本章中,您学习了如何使用新的孵化ivy-publish插件。您看到了如何使用publications配置块声明我们的发布内容。Gradle 将自动创建基于我们声明的发布内容的新任务。

您还学习了如何自定义 Gradle 发布任务生成的 Ivy 描述符文件。

最后,您看到了我们如何配置存储库以部署我们的发布内容。我们使用了file协议的本地目录,并使用了 Artifactory 和 Nexus 存储库管理器。

在本书中,我们看到了我们如何在项目中定义所需的依赖项。您学习了如何自定义依赖项解析以及如何定义存储依赖项的存储库。

然后,您学习了如何将我们的项目作为他人的依赖项进行部署。我们看到了如何将发布到 Maven 存储库,包括 Bintray 和 Ivy 存储库。您现在有了使用 Gradle 在 Java 项目中管理依赖项的知识。

posted @ 2025-09-10 14:11  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报