Gradle | 依赖管理最佳实践

写在最前

笔者最近接受了 躺平不是等死),换了一份新工作。

这部分内容,也是从前东家的 实际情况 出发的,可惜无法亲手去推广落地了。

在前段时间,我发布过一篇拙见:三思系列:组件化场景下module依赖优雅实践方案, 该文在组件化背景下,探索了一种方案,可以同时满足 减少编译数量以减少编译时间便捷的修改依赖树以灵活改动任意层级的Module内容具体内容可以阅读前文,不再赘述

除却Module依赖,还有 库包依赖 ,本文着重于探索 库包依赖项 的管理方式,而且是狭义上的 仓库下的库包

斗胆 称之为 最佳实践

问题背景和必要知识

首先确定一件事情:

implementation fileTree(dir: 'libs', include: ['*.jar'])

此类方式,引入的库包不属于 仓库 范畴,仅讨论基于Maven仓库的范畴,赘述一句,仓库按照习惯又可以分为两种类型:

  • Local:特指Maven的MavenLocal仓库,或者Gradle的Cache,MavenLocal和Gradle的Cache本质是一致
  • Remote:通过Uri指定的特定位置的仓库,最为常见的是MavenCentral和JCenter仓库。当然,可以将本机的目录指定为 "远程仓库" 位置。

当然,这并不影响本文的讨论


众所周知,使用Gradle确定仓库的库包需要三个因素:

  • GroupId
  • ArtifactId
  • version ,'+'号通配符表达 最新 的含义

for example:

androidx.core:core-ktx:1.3.2
  • GroupId 为 "androidx.core"
  • ArtifactId 为 "core-ktx"
  • version 为 "1.3.2"

问题背景

以Android为例,商业项目中,一个Project仅存在一个Module 的情况应该 非常少见 了, 往往一个Project下会存在多个Module,而且存在一定的依赖关系。

如果没有合适的管理手段,那么每个Module均声明自身的依赖项,当发生版本变更时:

  • 修改过于零碎
  • 同一个依赖项在不同Module下可能出现版本差异,这也是上一点所带来的后果

举个更典型的例子,以 后端项目为例微服务 的概念大家一定不陌生.

即使未曾深入了解,也知道后端将整个服务体系进行了拆分,用多个子系统项目(微服务)共同 支撑完整的服务体系。 以此达到 降低复杂度根据业务特性使用不同框架根据业务权重定制运维策略 等目的

而微服务之间通过RPC进行通信,而此处势必牵涉一个最大的 痛点Service方法签名和DTO数据保持一致,否则会带来 方法不存在 或者 数据遗失、解析错误 等问题。

传统做法及其优劣

比较早期的做法,是在Gradle构建时的运行环境中,创建或者利用Project级别的集合对象,将依赖项信息全部写入其中,各个Module使用时,达成了统一。

大家对这种做法很熟悉,不再用代码举例。 往往需要用到Extension扩展,为了方便描述,我们将:存储依赖项信息的Project级别集合 称为 Ext.deps

优点

  • 统一管理入口。一次修改,全Project生效

缺点

  • 无法进行代码提示
  • 一般无法兼容于构建工具的 新版本提示
  • 仅针对单Project,无法应对多Project,后端的微服务往往是多Project

改良版

利用Gradle 可以apply 远程构建脚本 (xxx.gradle) 的特性,进行方案改进。

将 "构建 Ext.deps 信息" 的 脚本,存储于网络特定位置,以解决多Project难以管理的问题。

一般需要对脚本文件按照版本命名,并保有所有版本的脚本。

这样可以避免:项目回溯版本功能时,出现额外问题。

利用Gradle留的后门

Gradle编译项目是很有意思的事情,我们知道:在成功加载完Gradle项目后,会 编译Gradle脚本 并生成各类Gradle任务,实际情况会更加复杂,为了方便,我们将之称为 Task编译

既然存在编译过程,Gradle团队索性留了一个后门:

如果根项目下存在"buildSrc", gradle 认为这是在Task编译过程中需要编译的内容,这些内容可能包含了:

  • Gradle插件内容
  • 插件设置内容
  • 等等

并且其编译结果对于该项目下的Gradle内容透明

这并不是一个新的特性,它至少已经有五年的历史了

Gradle官方指导文档 ,官方文档对其使用方式做了概要的描述。

勘误

因为buildSrc机制已经不是一个新特性了,故而利用这个机制去 管理Gradle依赖信息 已经是一个老话题了。

可能是巧合,该做法出现在开发者视野中时,刚好是 gradle开始对 kotlin-dsl 进行支持,同样不是新特性,大约是三年前的Gradle-4.10

而开始流行的做法又恰好对新特性进行了尝鲜,并且在讲解视频中留下了一些坑,于是这一做法的着重点,便被吸引到了 如何正确使用kotlin管理Gradle项目的依赖项这一话题上。

这一做法和kotlin、kts脚本并无实质关联

做法

在buildSrc目录下,按照标准sourceSet结构建立目录,并新增类文件例如:

buildSrc/src/main/java/Deps.java

public class Deps {
    public static String junit = "junit:junit:4.13.0";
}

sync后,类会被编译,我们可以在项目下的Gradle脚本中,只用使用,例如:

dependencies {
    //...
    
    testImplementation Deps.junit
//    'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

并且可以享有 代码提示跳转javaDoc弹窗 功能


而可查询到的常见做法,往往是使用kotlin类,那么就需要让buildSrc 在编译时支持kotlin ,那么自然需要 添加插件

在buildSrc下新建 build.gradle 并添加插件:

apply plugin: "kotlin"

buildscript {
    ext.kotlin_version = "1.4.21"
    repositories {
        jcenter()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
repositories {
    jcenter()
}

即可,此时添加的kotlin类即可被编译。

buildSrc/src/main/java/KDeps.kt

object KDeps {
    @JvmStatic
    val ext_junit = "androidx.test.ext:junit:1.1.2"
}

使用示例:

dependencies {
    testImplementation Deps.junit
//    'junit:junit:4.+'
    androidTestImplementation KDeps.ext_junit
    //'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

同样可以享有 代码提示跳转javaDoc弹窗 功能

而网传的 kts脚本 以及添加 kotlin-dsl支持,其实在这个需求中,并无真正的有效用途,只不过是应用了kts脚本后, 本身就需要编译kotlin内容,所以 默认使用了kotlin编译插件


言归正传,使用这种管理方式后,我们解决了无代码提示的弊端,再次 利用机器解放生产力

但是,我们没有解决服务端例子中的问题

将依赖信息打包发布

我想你已经深刻意识到了buildSrc机制的本质是啥:

利用Gradle 编译 buildSrc内容,产物供 后续的 该项目的 Gradle编译过程 使用

那么你一定可以想到,buildSrc可以申明自身的依赖!

于是,我们对常用库包进行分析后,选取对象并确定版本后,即可编写一个Library,

  • 将库包信息写成常量
  • 对Library建立版本机制
  • 发布Library并在buildSrc中使用

这是最简单的做法,即可在多个Project下,以最小的人力成本管理依赖并满足 一致性需求

进阶

Library依赖 Gradle后,可以编写 Gradle-Task内容配置 的过程代码,封装 依赖添加依赖检查 等内容。

举个简单的例子:

object KDeps {
    //    @JvmStatic
    const val ext_junit = "androidx.test.ext:junit:1.1.2"
}
public class Deps {
    public static String junit = "junit:junit:4.13.0";

    public static void applyAll(Project project) {
        project.getDependencies().add(
                "testImplementation", junit
        );
        project.getDependencies().add(
                "androidTestImplementation",KDeps.ext_junit
        );
    }
}

buildSrc/build.gradle

apply plugin: "kotlin"

buildscript {
    ext.kotlin_version = "1.4.21"
    repositories {
        jcenter()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//        implementation 'com.android.tools.build:gradle:4.1.1'
        //gradle sdk
        gradleApi()
    }
}
repositories {
    jcenter()
}

在app 的build.gradle中,可以这样使用:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    //略
}

dependencies {
    //略
    
    //修改为直接在 afterEvaluate 后调用函数设置
//    testImplementation Deps.junit
//    androidTestImplementation KDeps.ext_junit
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

afterEvaluate {
    Deps.applyAll(project)
}

当然,我们在这个过程中还可以使用各类编程技巧。

此时,我们已经拥有了无限可能,根据项目的实际需求 ,自行拓展吧。