剑客
关注科技互联网

Nebula: Netflix 开源的 Gradle 插件集合

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 项目构建的不同阶段,提供不同的功能。给出了使用 Nebula 的插件的项目的 Gradle 脚本。Nebula 的插件都发布到 Gradle 插件仓库中,因此需要在脚本中添加插件仓库地址"https://plugins.gradle.org/m2/"。要使用 Nebula 的插件,只需要在脚本中的 buildscript 中添加对相应插件的依赖,再通过 apply plugin 来应用插件。在应用了插件之后,可以在脚本中进行相应的配置,并通过 Gradle 命令行来运行相关的任务。中给出了示例的 Gradle 脚本。

清单 1. 使用 Nebula 的 Gradle 脚本

group 'com.midgetontoes'
version '1.0-SNAPSHOT'

buildscript {
   ext {
  springBootVersion = '1.3.5.RELEASE'
   }
   repositories {
  mavenLocal()
  mavenCentral()
  jcenter()
  maven {
  url "https://plugins.gradle.org/m2/"
  }
   }
   dependencies {
  classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  classpath "com.netflix.nebula:nebula-project-plugin:3.2.0"
  classpath "gradle.plugin.com.netflix.nebula:gradle-ospackage-plugin:3.6.1"
  classpath "com.netflix.nebula:nebula-publishing-plugin:4.8.0"
  classpath "com.netflix.nebula:nebula-release-plugin:4.0.1"
  classpath "com.netflix.nebula:gradle-resolution-rules-plugin:1.8.0"
   }
}

apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'application'
apply plugin: 'spring-boot'
apply plugin: 'nebula.project'
apply plugin: 'nebula.resolution-rules'
apply plugin: 'nebula.dependency-lock'
apply plugin: 'nebula.ospackage-daemon'
apply plugin: 'nebula.maven-publish'
apply plugin: 'nebula.javadoc-jar'
apply plugin: 'nebula.source-jar'
apply plugin: 'nebula.nebula-release'

sourceCompatibility = 1.8

mainClassName = 'com.midgetontoes.nebulasample.Application'

repositories {
   mavenLocal()
   mavenCentral()
   jcenter()
}

dependencies {
resolutionRules files('local-rules.json')
resolutionRules 'com.netflix.nebula:gradle-resolution-rules:latest.release'

   compile('org.springframework.boot:spring-boot-starter-web')
   compile('com.google.guava:guava:19.0')
// compile('io.netty:netty-all:4.1.4.Final')
   testCompile('org.springframework.boot:spring-boot-starter-test')
}

下面对 Nebula 提供的常用插件进行具体的介绍。

依赖版本锁定

在进行项目构建时一个很重要的要求是构建的可重复性。也就是说,代码仓库中任意时刻的代码都应该是可重复构建的。只有这样才可以保证代码的稳定性和质量。不论是最近的代码,还是几个星期之前、几个月之前甚至是几年之前的代码,都应该满足这样的条件。

可重复构建所面临的挑战之一来自于项目所依赖的第三方库。随着项目的演化,这些第三方库的版本可能升级。之前的项目版本也许只能与特定版本的第三方库协同工作。Gradle 项目直接在 Gradle 文件中声明所依赖的第三方库的版本。除了直接声明的第三方库版本之外,有些依赖是通过传递关系引入的。这些传递依赖的版本是不受应用本身控制的,而由所依赖的库自己来管理。因此第三方库自身的依赖的版本更新,也可能造成应用的构建失败。当项目的传递依赖关系很复杂时,很可能会出现传递依赖冲突的情况。

Nebula 提供的 nebula.dependency-lock 插件的作用是生成一个包含了全部依赖的具体版本的锁定文件。这个文件由代码仓库进行管理。当这个文件存在时,该插件会确保 Gradle 只会使用正确版本的依赖。实际上,使用过 Ruby 中的 Gem 管理工具 Bundler 的开发人员会发现,这种版本锁定功能与 Bundler 生成的 Gemfile.lock 是一样的。当每次版本发布时,在构建成功之后,应该通过该插件生成锁定文件,并提交到代码仓库。

nebula.dependency-lock 插件支持两类不同的锁定文件,分别是项目锁定文件和全局锁定文件。当 Gradle 项目中包含多个子项目时,每个子项目可以有自己的锁定文件。当全局锁定文件存在时,子项目中的锁定文件不起作用。子项目锁定文件的名称默认为 dependencies.lock,全局锁定文件的名称默认为 global.lock。插件提供的任务如所示。

表 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 来指定不同依赖的版本号。中给出了作为示例的 Maven BOM 文件。

清单 2. 示例 Maven BOM 文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     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>sample</groupId>
  <artifactId>sample-bom</artifactId>
  <version>1.0</version>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.1.1</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

当 Maven BOM 文件发布到 Maven 仓库之后,可以作为依赖推荐插件的规则来源,如所示。

清单 3. 使用 Maven BOM 文件指定推荐版本

dependencyRecommendations {
  mavenBom module: 'sample:sample-bom:1.0'
}

第二种方式是通过属性文件来指定版本。属性文件中的键是依赖的全名,值是对应的版本号,如所示。

清单 4. 使用属性文件指定推荐版本

dependencyRecommendations {
  propertiesFile file: 'recommendations.properties'
}

第三种方式是通过由 dependency-lock 插件生成的依赖版本锁定文件来指定版本号,如所示。该锁定文件在生成之后,通常会被提交到代码仓库中。项目可以直接使用锁定文件来推荐版本号。

清单 5. 使用依赖版本锁定文件推荐版本

dependencyRecommendations {
  dependencyLock module: 'sample:dependencies:1.0'
}

第四种方式是在 Gradle 文件中直接使用 java.util.Map 接口对象来提供推荐的版本号,如所示。

清单 6. 使用 Map 接口对象推荐版本

dependencyRecommendations {
  map recommendations: [
  'com.google.guava:guava': '18.0',
  'org.slf4j:slf4j-api': '1.7.21'
  ]
}

最后一种方式是通过完全自定义的代码来声明推荐的版本号。在 Gradle 脚本中通过 add 方法来添加规则。add 方法需要根据依赖的组织名和名称,返回其对应的推荐版本号。比如可以把推荐的版本号保存在数据库之中,然后在 add 方法中进行数据库查询并返回版本号。在中,对所有的依赖都返回推荐的版本号 1.0。

清单 7. 使用 add 方法推荐版本

dependencyRecommendations {
  add { org, name -> '1.0' }
}

在使用了版本推荐插件之后,Gradle 对依赖版本的选择过程发生了变化。优先级最高的是强制应用的依赖版本号,其次是显式指定了版本号的普通依赖,接着是通过插件推荐的依赖版本号,最后则是由直接依赖引入的传递依赖。当传递依赖的版本号与插件推荐的版本号发生冲突时,可以应用不同的冲突解决策略。默认的策略是 ConflictResolved,即通过 Gradle 自己的机制来选择合适的版本号,即优先考虑传递依赖中的版本号,再考虑插件所推荐的版本号;另外一种策略是 OverrideTransitives,即选择插件推荐的版本号,而完全忽略传递依赖中的版本号。中给出了使用 OverrideTransitives 策略的示例。

清单 8. 使用 OverrideTransitives 策略

dependencyRecommendations {
 strategy OverrideTransitives
 map recommendations: ['commons-logging:commons-logging': '1.0']
}

依赖解析规则

Gradle 本身已经提供了强大的依赖解析功能,可以满足各种特殊的依赖解析需求。在使用第三方提供的库时,不可避免的会遇到一些特殊情况,造成正常的依赖解析方式无法满足需求。这一方面是由于第三方库本身的原因,如库可能修改了在 Maven 仓库中的组织名和名称,但是并没有修改其内部的 Java 包名;有的库可能把所依赖的其他库打包在自己的 jar 包中。这两种情况都会造成解析时出现重复名称的 Java 类。另外一方面是额外的依赖限制。比如某些库可能只兼容特定版本的其他库。这样的版本依赖关系需要显式声明。还有一个常见需求是限制库使用的最低版本。这些需求都可以通过 Gradle 脚本来实现。但是当有多个项目时,这些特殊的依赖解析规则会在不同的 Gradle 脚本中重复,并没有很好的方式来复用。

Nebula 中的 nebula.resolution-rules 插件提供了一种更好的方式来管理和复用这些依赖解析规则。通过该插件可以把依赖解析规则记录在 JSON 文件中,从而可以更好的复用。可以为这些 JSON 文件创建专门的 Maven 项目并进行版本管理。公司和组织可以管理和维护自己的依赖解析规则。Netflix 自己维护一个公开的代码仓库来包含常见库的依赖解析规则。

在中,通过 nebula.resolution-rules 插件的 resolutionRules 声明了两种依赖解析规则,第一种来自项目文件 local-rules.json,第二种来自 Netflix 提供的通用解析规则。

清单 9. 通过 resolutionRules 声明依赖解析规则

resolutionRules files('local-rules.json')
resolutionRules 'com.netflix.nebula:gradle-resolution-rules:latest.release'

给出了 local-rules.json 文件的内容,其中通过 deny 规则声明了不能使用 io.netty:netty-all 依赖,因为该包中有其所使用的其他依赖,很容易造成类名重复。

清单 10. 依赖解析规则示例

{
 "deny": [
    {
       "module": "io.netty:netty-all",
       "reason": "不应该使用包含了其他依赖的库",
       "author" : "admin@example.org",
       "date" : "2016-08-04T20:21:20.368Z"
    }
 ]
}

除了中给出的 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 的最佳实践。

参考资源 (resources)

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址