自定义gradle 插件,配合 buildSrc 形式的组件库版本管理,
用于实现多 project 项目共享一套版本管理信息
前言
随着组件化越来越常见,module数量越来越多,依赖管理的混乱问题大家想必是都遇到过甚至正在经历着。
对于依赖管理的优化从手动到 ext ,到 buildSrc,越来越优雅,越高效和高级,确实解决了依赖管理方面的问题。但是这些都是解决同一个 project 项目下的依赖管理问题的。
当一个团队有多个 project 时,各 project 之间是感知不到对方的 buildSrc 配置信息的,也就是说每个 project 都是要建立一个 buildSrc,并配置相同的依赖信息。
本文主要基于 buildSrc 形式,介绍一种自定 gradle 插件,使 buildSrc 实现多项目间可以共享一套版本依赖管理信息。
默认你已了解 buildSrc 实现依赖管理。
对于 buildSrc 的介绍有很多文章,搜一下即可,这里推荐一篇
Kotlin + buildSrc:更好的管理Gadle依赖! - 腾讯云开发者社区-腾讯云
依赖管理的方式
下面简单介绍一下依赖管理:
手动管理
moduleA 的 gradle 中:
implementation "com.android.support:appcompat-v7:27.0.2"
implementation "com.haodf.lib:toast:1.2.3"
moduleB 的 gradle 中:
implementation "com.android.support:appcompat-v7:27.0.2"
implementation "com.haodf.lib:toast:1.2.3"
可以看到,相同的代码重复的写在 gradle 中。一旦 toast 有更新(这个其实很常见,不同的人维护不同的组件库,经常有功能升级,bug修改),就得把所有引用的地方都手动 由 1.2.3 更新到 1.2.4。久而久之,忘了这个 module ,漏了那个 module,于是一个项目里对toast的引用竟然有三四个版本同时存在。
ext管理
即形如下面这种:
ext {
versions = [
toast: "27.0.2",
]
libs = [
toast: "com.haodf.lib:${versions.toast}",
]
}
//使用
implementation libs.toast
这种就是没有代码提示
于是 buildSrc 出现了:
buildSrc
形如:
代码:
object Librarys {
//toast
const val toast= "com.haodf.lib:${Versions.toast}"
使用:
implementation Librarys.toast
这里的调用是有代码提示的,并且可以像代码调用一样点击查看都有哪些地方在引用了这个库。
多项目共享依赖信息的方案
那么,如果我们团队有三个 project 项目,该怎么解决依赖管理问题呢?
多项目是很常见的,即使只服务于一个app,也会有包含app模块的主project,然后是其他的工具类组件库模块,业务模块等等。每一个都是以project形式进行开发的,他们都以 aar的形式被发布、依赖,开发时也基本都需要依赖其他的 aar 库。
这时候,我们需要为 projectA、projectB、projectC 分别建立一个 buildSrc 模块,这是必须的,因为buildSrc 必须显示的在项目目录下创建。然后在每一个 buildSrc 模块下创建依赖管理的类 dependencies.kt 文件。然后内容是一模一样的。
这即是问题所在。
一是代码重复,本来就是被维护的信息,却要同时维护多份同样的信息。
二是当 toast 组件有更新时,必须到这个 project 中去对三个dependencies.kt进行源码级的修改。
我们是否能对依赖相关的代码只维护一份呢,当更新时是否能只修改一处源码呢?
原理
我们想到通过自定义 gradle 插件来实现这个功能:
在 buildSrc 模块被自动编译以供 gradle 调用前,自动的去某个地方把版本依赖相关的源码下载到 buildSrc 中。这样如果有版本更新,就会把最新的依赖相关的代码下载到 buildSrc,然后再编译 buildSrc,,就实现了自动更新。
这个能保存依赖相关代码的某个地方,可以是某个url,下载某个rar文件,也可以是某个maven管理的库。但是从开发的角度,最好的就是maven仓库了,同时,我们在维护这份依赖信息时最好也能和正常敲代码一样,敲的是什么,最后更新到本地项目中的就是什么。
最终确定依赖相关的信息直接写在 gradle 插件里,每次有依赖信息更新,就发布新的gradle插件。
自定义 gradle插件
同样这个也有很多文章介绍,就不过多介绍如何自定义了,只讲一下依赖管理相关的具体的代码
下面是 gradle插件的主要结构:
app是一个空的项目,plugin-universion模块是自定义gradle的代码。
因为 gradle使用 groovy语言编写的,而我们实际项目中是用kotlin开发的,所以创建了两个源码目录:
src/main/groovy:用来编写插件代码
src/main/java:用来像在实际项目中编写代码一样,编写依赖管理相关的代码。
首先,我们在src\main\java\com\haodf\universion\Dependencies.kt文件中编写kotlin代码:
注意这是在自定义gradle插件项目中编写的kotlin代码
object Versions {
const val activity= "1.4.10"
const val toast= "1.3.9"
}
object Librarys {
const val activity= "com.xx.lib:activity:${Versions.activity}"
const val toast= "com.xx.lib:toast:${Versions.toast}"
}
最终,以上的代码将以源码的形式被全部复制到具体项目的 buildSrc 模块下的 Dependencies.kt中。
提取源码
怎么样在gradle运行时拿到上面这堆源码呢?在gradle插件运行时,上面的代码早就被编译了。所以我们想到在自定义gradle项目编译前,能不能把Dependencies.kt中的源码内容转成一个类的变量的值,这样在gradle代码运行时,就能读取到了。
所以,在src/main/groovy 代码目录下新建 Code.groovy类:
public class Code {
public String code = """ 这里使用了三引号,保留此处的字符串的格式 """
}
接下来,就靠 gradle任务来把Dependencies.kt内的kotlin代码内容赋值到 code变量里了。
在 plugin-universion 的gradle中直接编码:
File f = new File( "plugin-universion", "src/main/java/com/haodf/universion/Dependencies.kt")
if (!f.exists()) {
f = new File( "src/main/java/com/haodf/universion/Dependencies.kt")
}
String codeStr = f.readLines().join("\n")
String c1 ="package com.haodf.universion\n" +
"\n" +
"public class Code {\n" +
" public String code = \"\"\""
String c2 = "\"\"\"\n" +
"}"
f = new File( "plugin-universion", "src/main/groovy/com/haodf/universion/Code.groovy")
if (!f.exists()) {
f = new File( "src/main/groovy/com/haodf/universion/Code.groovy")
}
println codeStr
println("----------------ok--------------------")
f.write(c1+codeStr.replace("\$", "\\\$")+c2)
f = new File( "plugin-universion", "src/main/groovy/com/haodf/universion/Version.groovy")
if (!f.exists()) {
f = new File( "src/main/groovy/com/haodf/universion/Version.groovy")
}
f.write("package com.haodf.universion\n" +
"\n" +
"public class Version {\n" +
" public String version = \"" +
project.ext.gradle_version +
"\"\n" +
"\n" +
"}")
其实逻辑很简单,全部是文件操作。config阶段,gradle读取Dependencies.kt文件内容,然后按代码格式组装字符串,将字符串内容覆盖写入 Code.groovy文件中。
所以,在编译之前,Code.groovy的内容就会变成:
注意哦,看起来的 kotlin 代码是code变量的值
public class Code {
public String code = """/**
* Created by zhaoruixuan1 on 2023/3/24
* CopyRight (c) haodf.com
* 功能:统一依赖管理
*/
object Versions {
const val activity = "1.4.10"
const val toast = "1.3.9"
}
object Librarys {
const val activity = "com.xx.lib:activity:\${Versions.activity}"
const val toast = "com.xx.lib:toast:\${Versions.toast}"
}"""
}
同时,也覆写了Version.groovy文件,保存了当前的gradle插件版本。用来在项目运行时判断是否需要执行更新操作。
然后就是自定义gradle插件的主要代码:
UniversionPlugin.groovy
public class UniVersionPlugin implements Plugin<Project> {
String version = new Version().version
String info = "val name = \"kotlin\""
UniVersionExtension uniVersionExtension = new UniVersionExtension()
@Override
void apply(Project target) {
println "universion plugin running"
target.extensions.add("universion", UniVersionExtension)//配置的插件扩展
//默认项:实际项目的buildSrc模块下,保存版本号的文件
uniVersionExtension.versionFile = "src/main/java/version"
//默认项:实际项目中,承载依赖代码的kotlin类
uniVersionExtension.codeFile = "src/main/java/Dependencies.kt"
uniVersionExtension.ignoreError = false
target.afterEvaluate {
update(target)
}
}
private void update(Project target) {
try {
//读取自定义的扩展配置信息
UniVersionExtension extension = target.extensions.getByName("universion")
if (extension != null) {
if (extension.versionFile != null && extension.versionFile != "") {
uniVersionExtension.versionFile = extension.versionFile
}
if (extension.codeFile != null && extension.codeFile != "") {
uniVersionExtension.codeFile = extension.codeFile
}
if (extension.ignoreError != null) {
uniVersionExtension.ignoreError = extension.ignoreError
}
}
println("versionFile = " + uniVersionExtension.versionFile + " codeFile = " + uniVersionExtension.codeFile + " ignoreError = " + uniVersionExtension.ignoreError)
File versionNumFile = target.file(uniVersionExtension.versionFile)
versionNumFile.setReadable(true)
List<String> lines = versionNumFile.readLines("utf-8")
String moduleVersion = ""
if (lines != null && lines.size() > 0) {
moduleVersion = lines.get(0)
}
if (moduleVersion == "" || moduleVersion == null) {
println "universion: 未找到目前 universion 版本,请确认src/main/java/version文件存在"
}
println("universion: 项目版本:" + moduleVersion + " 最新版本:" + version)
if (moduleVersion.equals(version)) {
println "universion 已是最新版"
return
}
println "universion: 开始更新"
//依然是文件操作
File versionInfoFile = target.file(uniVersionExtension.codeFile)
versionNumFile.write(version)
versionInfoFile.setWritable(true)
versionInfoFile.write(new Code().code)
println "universion:更新成功"
} catch (Exception e) {
if (uniVersionExtension.ignoreError) {
println("universion: 更新失败\n error:" + e.message+"\n已设置忽略此异常,可能导致统一版本管理信息未更新")
} else {
throw new RuntimeException("universion: 更新失败\n " + e.message + "\n如果想忽略此异常,请在gradle中配置universion { ignoreError = true }")
}
}
}
}
插件运行时,把 Code.code的值写入到具体项目的Dependencies.kt文件中
插件的扩展:
public class UniVersionExtension {
String versionFile;//指定具体项目中,保存插件版本号的文件
String codeFile; //指定具体项目中,保存依赖管理代码的文件
boolean ignoreError;//指定当此插件更新失败时,是否忽略,继续往下编译
}
最后,发布gradle插件即可。
注意:
本插件为 buildSrc 模块开发,以使 buildSrc 模块被用来进行版本管理时,在多 project 项目开发时有更好更高效的使用体验。
但是本插件并不局限于 buildSrc,任何 project 均可引用。
使用gradle插件
在具体的project中(默认是配合 buildSrc使用):
新建 buildSrc 模块后,建立空的包目录,buildSrc/src/main/java/。
然后新建一个空的代码类 比如名 Dependencies.kt,无需编写实际代码。
再新建一个文本文件,比如名 version.
引用插件:
在 buildSrc 模块的 gradle 中:
dependencies 下增加:
dependencies {
......
classpath "com.haodf.universion:universion:+"
}
gradle 文件顶部增加:
apply plugin: 'com.haodf.universion'
至此,项目引用部分完成,点击同步或编译,之前在插件中编写的版本管理的代码会以源码格式写入 Dependencies.kt 文件,
插件版本号会写入 version 文件中, 用于对比下次编译是否需要执行更新操作。
扩展:
插件提供三个参数,以对代码、版本文件、更新操作进行自定义:
比如你的想project中新建的buildSrc模块下,你想取的保存依赖信息的代码文件是YiLai.kt
你想保存版本号的文件是 gradle.txt
那么扩展配置如下:
universion {
versionFile = "src/main/java/gradle.txt" //修改记录插件版本号的文件
codeFile= "src/main/java/YiLai.kt" //修改源码文件
ignoreError = false //当此插件进行版本更新失败时,是否忽略,继续往下编译。
}
插件的不足
版本信息的源码只在 gradle 插件项目中维护一份即可。
N 个 project 依然需要 N 个 buildSrc 模块,
当有组件版本号变更时,只需修改 gradle 插件项目一处。
另外在 N 个项目中,依然需要对插件的引用处修改插件版本号
同步后,新代码就自动更新到对应的 buildSrc 下的类文件中了。
存在的缺憾:
1、多项目已共享一份版本信息源码,但是更新时依然需要手动修改 gradle 版本号:
classpath "com.haodf.universion:universion:1.2.3"
2、使用 + 替换具体的版本号可以实现多 project 自动更新统一版本信息
classpath "com.haodf.universion:universion:+"
组件管理者只管发布新版本,任何一个project在下一次编译前都会自动更新下来最新的依赖版本
但是这又引入了 + 引用的风险。虽然目前看对此插件的 classpath 依赖没什么风险