Gradle 作为 Apache Maven 的有力竞争者,在 Java 项目的构建领域逐渐流行起来。很多开源项目,如 Spring 框架、Hibernate、Elasticsearch 和 RxJava 等都使用 Gradle 进行构建。Gradle 也是 Android Studio 中 Android 项目的标准构建方式。越来越多的开发人员开始使用 Gradle 构建自己的 Java 项目。在开始使用 Gradle 时经常会面临的一个问题是从何处开始。Maven 中可以使用 Archetype 来作为项目的模板,Gradle 并没有提供类似的机制。本文要介绍的 Nebula 是由 Netflix 开发的 Gradle 项目构建框架,其目的是为 Gradle 项目提供一个良好的起点,把一些常见的任务添加到构建过程中,从而简化 Gradle 项目的构建配置。
基本配置
本文通过一个基于 Spring Boot 的 Java Web 示例应用来介绍 Nebula 的使用。Nebula 的核心是一系列由 Netflix 开发和维护的 Gradle 插件。这些插件覆盖 Gradle 项目构建的不同阶段,提供不同的功能。代码清单 1 给出了使用 Nebula 的插件的项目的 Gradle 脚本。Nebula 的插件都发布到 Gradle 插件仓库中,因此需要在脚本中添加插件仓库地址"https://plugins.gradle.org/m2/"。要使用 Nebula 的插件,只需要在脚本中的 buildscript 中添加对相应插件的依赖,再通过 apply plugin 来应用插件。在应用了插件之后,可以在脚本中进行相应的配置,并通过 Gradle 命令行来运行相关的任务。代码清单 1 中给出了示例的 Gradle 脚本。
清单 1. 使用 Nebula 的 Gradle 脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
下面对 Nebula 提供的常用插件进行具体的介绍。
依赖版本锁定
在进行项目构建时一个很重要的要求是构建的可重复性。也就是说,代码仓库中任意时刻的代码都应该是可重复构建的。只有这样才可以保证代码的稳定性和质量。不论是最近的代码,还是几个星期之前、几个月之前甚至是几年之前的代码,都应该满足这样的条件。
可重复构建所面临的挑战之一来自于项目所依赖的第三方库。随着项目的演化,这些第三方库的版本可能升级。之前的项目版本也许只能与特定版本的第三方库协同工作。Gradle 项目直接在 Gradle 文件中声明所依赖的第三方库的版本。除了直接声明的第三方库版本之外,有些依赖是通过传递关系引入的。这些传递依赖的版本是不受应用本身控制的,而由所依赖的库自己来管理。因此第三方库自身的依赖的版本更新,也可能造成应用的构建失败。当项目的传递依赖关系很复杂时,很可能会出现传递依赖冲突的情况。
Nebula 提供的 nebula.dependency-lock 插件的作用是生成一个包含了全部依赖的具体版本的锁定文件。这个文件由代码仓库进行管理。当这个文件存在时,该插件会确保 Gradle 只会使用正确版本的依赖。实际上,使用过 Ruby 中的 Gem 管理工具 Bundler 的开发人员会发现,这种版本锁定功能与 Bundler 生成的 Gemfile.lock 是一样的。当每次版本发布时,在构建成功之后,应该通过该插件生成锁定文件,并提交到代码仓库。
nebula.dependency-lock 插件支持两类不同的锁定文件,分别是项目锁定文件和全局锁定文件。当 Gradle 项目中包含多个子项目时,每个子项目可以有自己的锁定文件。当全局锁定文件存在时,子项目中的锁定文件不起作用。子项目锁定文件的名称默认为 dependencies.lock,全局锁定文件的名称默认为 global.lock。插件提供的任务如表 1 所示。
表 1. nebula.dependency-lock 插件提供的任务
任务 | 描述 |
---|---|
generateLock / generateGlobalLock | 生成锁定文件。generateLock 生成子项目的锁定文件, generateGlobalLock 生成全局锁定文件。锁定文件生成在项目的 build 目录中。 |
updateLock / updateGlobalLock | 更新子项目/全局锁定文件。 |
saveLock / saveGlobalLock | 把生成的锁定文件复制到项目目录中。 |
deleteLock / deleteGlobalLock | 删除子项目/全局锁定文件。 |
commitLock | 把锁定文件提交到代码仓库。 |
该插件提供了一些额外的参数来对任务的行为进行配置。比如,在 updateLock 时可以通过 dependencyLock.updateDependencies 来指定需要更新的依赖的名称。
依赖版本推荐
在 Gradle 项目中添加第三方依赖时都需要指定版本号。在 Gradle 脚本文件中直接引用版本号可能造成依赖版本升级时的维护困难。一般的做法是把版本号提取到项目属性中,从而可以在统一的地方管理所有依赖的版本信息。当项目较多时,这样的管理方式也会变得很繁琐。因为有些通用的库会在多个项目中使用,而当需要升级这些通用库的版本时,会需要修改多个项目的 Gradle 文件。另外一个常见的需求是解决多个依赖库的版本兼容问题。有些第三方库,如 Spring 框架,包含很多个子项目,当引用这些依赖时,需要确保这些依赖的版本一致,否则可能出现兼容性问题。
这些与依赖的版本号相关的问题,都可以通过 Nebula 提供的依赖推荐插件来解决。在使用了依赖推荐插件之后,没有声明版本的第三方依赖的版本号由插件来决定。
依赖推荐插件支持五种方式来声明所推荐的依赖的版本。第一种方式是通过 Maven BOM 文件。Maven 的 BOM 文件中直接定义了依赖的版本信息。比如,Spring Boot 项目就提供了相应的 BOM 文件,可以作为创建 Spring Boot 项目的父 POM 文件。Maven BOM 文件中通过 dependencyManagement 来指定不同依赖的版本号。代码清单 2 中给出了作为示例的 Maven BOM 文件。
清单 2. 示例 Maven BOM 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
当 Maven BOM 文件发布到 Maven 仓库之后,可以作为依赖推荐插件的规则来源,如代码清单 3 所示。
清单 3. 使用 Maven BOM 文件指定推荐版本
1 2 3 |
|
第二种方式是通过属性文件来指定版本。属性文件中的键是依赖的全名,值是对应的版本号,如代码清单 4 所示。
清单 4. 使用属性文件指定推荐版本
1 2 3 |
|
第三种方式是通过由 dependency-lock 插件生成的依赖版本锁定文件来指定版本号,如代码清单 5 所示。该锁定文件在生成之后,通常会被提交到代码仓库中。项目可以直接使用锁定文件来推荐版本号。
清单 5. 使用依赖版本锁定文件推荐版本
1 2 3 |
|
第四种方式是在 Gradle 文件中直接使用 java.util.Map 接口对象来提供推荐的版本号,如代码清单 6 所示。
清单 6. 使用 Map 接口对象推荐版本
1 2 3 4 5 6 |
|
最后一种方式是通过完全自定义的代码来声明推荐的版本号。在 Gradle 脚本中通过 add 方法来添加规则。add 方法需要根据依赖的组织名和名称,返回其对应的推荐版本号。比如可以把推荐的版本号保存在数据库之中,然后在 add 方法中进行数据库查询并返回版本号。在代码清单 7 中,对所有的依赖都返回推荐的版本号 1.0。
清单 7. 使用 add 方法推荐版本
1 2 3 |
|
在使用了版本推荐插件之后,Gradle 对依赖版本的选择过程发生了变化。优先级最高的是强制应用的依赖版本号,其次是显式指定了版本号的普通依赖,接着是通过插件推荐的依赖版本号,最后则是由直接依赖引入的传递依赖。当传递依赖的版本号与插件推荐的版本号发生冲突时,可以应用不同的冲突解决策略。默认的策略是 ConflictResolved,即通过 Gradle 自己的机制来选择合适的版本号,即优先考虑传递依赖中的版本号,再考虑插件所推荐的版本号;另外一种策略是 OverrideTransitives,即选择插件推荐的版本号,而完全忽略传递依赖中的版本号。代码清单 8 中给出了使用 OverrideTransitives 策略的示例。
清单 8. 使用 OverrideTransitives 策略
1 2 3 4 |
|
依赖解析规则
Gradle 本身已经提供了强大的依赖解析功能,可以满足各种特殊的依赖解析需求。在使用第三方提供的库时,不可避免的会遇到一些特殊情况,造成正常的依赖解析方式无法满足需求。这一方面是由于第三方库本身的原因,如库可能修改了在 Maven 仓库中的组织名和名称,但是并没有修改其内部的 Java 包名;有的库可能把所依赖的其他库打包在自己的 jar 包中。这两种情况都会造成解析时出现重复名称的 Java 类。另外一方面是额外的依赖限制。比如某些库可能只兼容特定版本的其他库。这样的版本依赖关系需要显式声明。还有一个常见需求是限制库使用的最低版本。这些需求都可以通过 Gradle 脚本来实现。但是当有多个项目时,这些特殊的依赖解析规则会在不同的 Gradle 脚本中重复,并没有很好的方式来复用。
Nebula 中的 nebula.resolution-rules 插件提供了一种更好的方式来管理和复用这些依赖解析规则。通过该插件可以把依赖解析规则记录在 JSON 文件中,从而可以更好的复用。可以为这些 JSON 文件创建专门的 Maven 项目并进行版本管理。公司和组织可以管理和维护自己的依赖解析规则。Netflix 自己维护一个公开的代码仓库来包含常见库的依赖解析规则。
在代码清单 9 中,通过 nebula.resolution-rules 插件的 resolutionRules 声明了两种依赖解析规则,第一种来自项目文件 local-rules.json,第二种来自 Netflix 提供的通用解析规则。
清单 9. 通过 resolutionRules 声明依赖解析规则
1 2 |
|
代码清单 10 给出了 local-rules.json 文件的内容,其中通过 deny 规则声明了不能使用 io.netty:netty-all 依赖,因为该包中有其所使用的其他依赖,很容易造成类名重复。
清单 10. 依赖解析规则示例
1 2 3 4 5 6 7 8 9 10 |
|
除了代码清单 10 中给出的 deny 规则之外,nebula.resolution-rules 插件还支持其他不同的规则:
- replace:当两个依赖同时出现时,用其中一个替换掉另外一个。
- substitute:类似于 replace,不同的是只要旧的依赖出现,则替换成新的依赖。
- deny:当出现指定的依赖时,会使得 Gradle 构建失败。
- reject:指定的依赖不会出现在动态版本的计算过程中。但如果项目显式的包含这个依赖,则该依赖仍然会被加入。
- align:要求一组依赖的使用相同的版本。
版本发布
当需要发布一个项目的新版本时,通常需要执行一系列的动作,包括对代码仓库的处理,构建当前版本并发布到 Maven 仓库等。nebula-release-plugin 插件的作用是自动化执行这些操作。该插件使用符合语义版本号规则的版本,即 major.minor.patch-<prerelease>+<metadata>的格式。该插件提供了如下的任务:
- snapshot:发布的版本号为<major>.<minor>.<patch>-SNAPSHOT,如 1.0.0-SNAPSHOT。
- devSnapshot:发布的版本号为<major>.<minor>.<patch>-dev.#+<hash>,如 0.1.0-dev.1+b8dd0f3。该任务与 snapshot 的差别在于生成的版本号中包含当前 Git commit 的 hash。
- candidate:发布的版本号为<major>.<minor>.<patch>-rc.#,表示版本发布的候选,可能存在多个候选,如 1.0.0-rc.1,1.0.0-rc.2 等。该任务会创建与版本号相同的 Git 标签。
- final:发布的版本号为<major>.<minor>.<patch>,如 1.0.0。该任务会创建与版本号相同的 Git 标签。
该插件会在当前的构建成功之后才进行相应的 Git 操作,适合于在持续集成服务器中运行。
其他插件
除了上述的插件之后,Nebula 还提供了其他有用的插件。
gradle-aggregate-javadocs-plugin 插件用来把多个子项目的 javadoc 合并成单一的文档。这对于包含多个子项目的项目来说是非常实用的。在添加了该插件之后,可以通过 aggregateJavadocs 任务来生成合并之后的 javadoc。
gradle-override-plugin 插件允许在命令行直接覆写项目构建中的属性值。有两种方式可以覆写属性值,一种是以"OVERRIDE_."开头的环境变量,另外一种是以"override."作为前缀的系统属性。比如在 Gradle 命令行可以通过"-Doverride.sampleProp=value"来覆写"sampleProp"的值为"value"。
gradle-contacts-plugin 插件允许 Gradle 项目添加开发人员的相关信息。这些信息对于开源项目来说尤其重要。通过该插件可以声明开发人员的基本信息、联系方式和角色等。这些信息会被其他插件所使用,如出现在生成的 jar 包的清单文件中,所发布的 Maven 项目的 POM 文件中。
gradle-metrics-plugin 插件用来收集构建过程中的各种数据并推送到数据存储中,以方便相关的数据分析。数据可以被推送到 Elasticsearch 或 Splunk 中。
gradle-ospackage-plugin 插件用来生成可以在操作系统上直接运行的包,支持 RedHat 和 Debian。
小结
Gradle 作为流行的构建工具,已经被越来越多的项目所采用。在使用 Gradle 的过程中,会发现有些通用的任务需要在 Gradle 脚本中不断的重复。Nebula 的意义在于把这些通用的任务整理成单独的开源插件,使得可以被其他项目所复用。本文对 Nebula 所提供的依赖版本锁定、依赖版本推荐、依赖解析规则、版本发布和其他插件进行了详细的介绍,具体说明了这些插件在实际项目中的用法。在 Gradle 项目中使用这些插件可以极大的减少相关的工作量,并应用来自 Netflix 的最佳实践。