版本管理:语义化版本、版本目录、依赖锁定
版本管理这件事,说实话,我早年吃过不少亏。那时候项目里依赖乱七八糟,今天能编译,明天就炸了。后来我才意识到,版本管理不是「记个版本号」那么简单,它是一套工程规范。
今天咱们就聊聊三个核心概念:语义化版本、版本目录(Version Catalog)、依赖锁定。这三者配合好了,你的项目依赖管理会非常清爽。
1. 语义化版本:版本号里藏着什么信息?
语义化版本(SemVer)是一种版本号规范。格式很简单:主版本号.次版本号.修订号。
举个例子:2.5.1。
- 主版本号:不兼容的API变更。比如从1.x升级到2.x,你的代码大概率要改。
- 次版本号:向下兼容的新功能。比如从2.5升级到2.6,加了个新API,旧代码不受影响。
- 修订号:向下兼容的问题修复。比如从2.5.1升级到2.5.2,修了个bug,你什么都不用动。
我个人习惯在项目初期用 0.x.x 表示不稳定阶段。等API稳定了,再切到 1.0.0。
核心原则:版本号不是随便写的。每次发布,都要问自己:这次改动是否兼容?是否新增功能?是否修了bug?然后对应更新版本号。
我在项目中遇到过一个问题:某个库从 3.2.0 升级到 3.3.0,按理说次版本号升级应该兼容。结果它悄悄改了一个内部API的行为,导致我的代码在运行时崩溃。嗯,这就是「语义化版本」被破坏的例子。所以,不要完全信任版本号,关键依赖还是要做集成测试。
2. 版本目录(Version Catalog):统一管理依赖版本
以前在Gradle里管理依赖,每个模块都要写一遍版本号。比如:
// 每个build.gradle.kts里都写一遍
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
这样做的坏处很明显:版本号散落在各处,升级时要改N个文件。而且不同模块可能用了不同版本,冲突起来很头疼。
Gradle 7.0 之后推出了 Version Catalog,说白了就是集中管理版本号。你只需要在 gradle/libs.versions.toml 里定义一次,所有模块都能引用。
来看看这个文件长什么样:
[versions]
okhttp = "4.11.0"
kotlin = "1.9.0"
[libraries]
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
然后在模块的 build.gradle.kts 里这样引用:
dependencies {
implementation(libs.okhttp.core)
implementation(libs.okhttp.logging)
implementation(libs.kotlin.stdlib)
}
你看,版本号只在一个地方定义。升级时改 libs.versions.toml 一个文件就够了。而且Gradle会自动检查版本冲突,非常省心。
我的建议:新项目直接上Version Catalog。老项目迁移也不难,花半小时把版本号抽出来,以后维护会轻松很多。
3. 依赖锁定:让构建可复现
依赖锁定,说白了就是「固定住所有依赖的精确版本」。为什么需要这个?
你想想看,你的 build.gradle.kts 里写的是 implementation("com.squareup.okhttp3:okhttp:4.11.0")。但 okhttp 自己也有依赖,比如 okio。这些传递依赖的版本是不确定的。今天构建时Gradle解析到 okio 3.3.0,明天可能就变成 3.4.0 了。
这就导致一个问题:同一个代码,不同时间构建,结果可能不同。这在生产环境是很危险的。
Gradle提供了依赖锁定机制。你可以在 build.gradle.kts 里这样配置:
dependencyLocking {
lockAllConfigurations()
}
然后运行 ./gradlew dependencies --write-locks,Gradle会生成一个 gradle/dependency-locks/*.lockfile 文件。里面记录了所有依赖的精确版本:
# This is a Gradle generated file for dependency locking.
com.squareup.okhttp3:okhttp:4.11.0=compileClasspath
com.squareup.okio:okio:3.3.0=compileClasspath
org.jetbrains.kotlin:kotlin-stdlib:1.9.0=compileClasspath
把这个文件提交到Git仓库。以后每次构建,Gradle都会严格按照这个文件里的版本去下载依赖。除非你主动更新锁文件,否则版本不会变。
我曾经踩过的坑:有一次线上出了bug,本地却复现不了。查了半天,发现是传递依赖版本不一致导致的。后来我强制所有项目都用依赖锁定,再也没出过这种问题。
4. 三者如何配合?
语义化版本、版本目录、依赖锁定,这三者不是孤立的。它们是一个完整的版本管理方案:
- 语义化版本:告诉你「这个版本改了什么」。
- 版本目录:帮你「集中管理版本号」。
- 依赖锁定:确保「构建结果可复现」。
我一般的工作流是这样的:
- 在
libs.versions.toml里定义所有依赖的版本号。 - 在
build.gradle.kts里引用这些依赖。 - 启用依赖锁定,生成锁文件。
- 需要升级依赖时,先改
libs.versions.toml里的版本号。 - 运行测试,确认没问题。
- 更新锁文件,提交代码。
这样一套流程下来,依赖管理基本不会出乱子。
5. 一张图看懂版本管理
下面这张图展示了语义化版本、版本目录、依赖锁定之间的关系:
6. 一些实用建议
最后,分享几个我在实战中总结的经验:
- 版本号不要用
+或latest。这种写法会让构建结果不可控。我见过有人用implementation("com.squareup.okhttp3:okhttp:4.+"),结果某天自动升级到5.x,项目直接编译不过。 - 锁文件一定要提交到Git。很多新手把
*.lockfile加到.gitignore里,这是大错特错。锁文件不提交,依赖锁定就失去了意义。 - 定期升级依赖。不要一直用旧版本。我一般每两周检查一次依赖更新,用
./gradlew dependencyUpdates插件来辅助。 - 升级后跑全量测试。语义化版本只是约定,不是保证。升级后一定要跑测试,尤其是集成测试。
一个小技巧:在 libs.versions.toml 里,可以用 [versions] 区块定义版本别名。比如 okhttp = "4.11.0",然后在 [libraries] 里用 version.ref = "okhttp" 引用。这样升级时一目了然。
好了,版本管理这块就聊到这儿。语义化版本、版本目录、依赖锁定,这三样东西用好了,你的项目依赖管理会非常稳健。下次咱们聊聊别的实战话题。
公众号:蓝海资料掘金营,微信deep3321