版本管理:语义化版本、版本目录、依赖锁定

版本管理这件事,说实话,我早年吃过不少亏。那时候项目里依赖乱七八糟,今天能编译,明天就炸了。后来我才意识到,版本管理不是「记个版本号」那么简单,它是一套工程规范。

今天咱们就聊聊三个核心概念:语义化版本版本目录(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. 三者如何配合?

语义化版本、版本目录、依赖锁定,这三者不是孤立的。它们是一个完整的版本管理方案:

  • 语义化版本:告诉你「这个版本改了什么」。
  • 版本目录:帮你「集中管理版本号」。
  • 依赖锁定:确保「构建结果可复现」。

我一般的工作流是这样的:

  1. libs.versions.toml 里定义所有依赖的版本号。
  2. build.gradle.kts 里引用这些依赖。
  3. 启用依赖锁定,生成锁文件。
  4. 需要升级依赖时,先改 libs.versions.toml 里的版本号。
  5. 运行测试,确认没问题。
  6. 更新锁文件,提交代码。

这样一套流程下来,依赖管理基本不会出乱子。

5. 一张图看懂版本管理

下面这张图展示了语义化版本、版本目录、依赖锁定之间的关系:

版本管理三大核心 语义化版本 主版本号.次版本号.修订号 不兼容变更 → 主版本 新增功能 → 次版本 问题修复 → 修订号 版本目录 libs.versions.toml 集中定义版本号 所有模块统一引用 升级只需改一处 依赖锁定 *.lockfile 固定所有依赖版本 构建结果可复现 避免传递依赖变化 三者配合工作流 1. 语义化版本定义依赖的版本号规则 2. 版本目录集中管理这些版本号 3. 依赖锁定确保构建时使用精确版本

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