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)
}
当然,我们在这个过程中还可以使用各类编程技巧。
此时,我们已经拥有了无限可能,根据项目的实际需求
,自行拓展吧。