3. Maven/Gradle版本管理:项目版本号规范与依赖锁定

版本管理这件事,说大不大,说小不小。我见过太多团队因为版本号乱写,导致线上事故。今天咱们就聊聊这个看似简单、实则坑多的环节。

3.1 语义化版本号:别再乱起名了

先问个问题:你见过 1.0.0-final2.3.1-beta23.0.0.RELEASE 这种版本号吗?我见过,而且不止一次。说实话,这种命名方式在小型项目里还能凑合,一旦上了规模,就是灾难。

语义化版本(SemVer)的规则其实很简单:主版本号.次版本号.修订号

位置 含义 什么时候加
主版本号 不兼容的API变更 你改了接口,别人得改代码
次版本号 向下兼容的功能新增 加了新功能,但不影响老代码
修订号 向下兼容的问题修正 修了个bug,没加新功能

举个例子:2.5.1 表示主版本2,次版本5,修订号1。如果下一个版本修了个bug,就变成 2.5.2。如果加了新功能,就变成 2.6.0。如果改了不兼容的API,就变成 3.0.0

核心原则:版本号不是随便写的,它是在告诉使用者「这个版本改了啥」。

我个人习惯在预发布版本后面加后缀,比如 1.0.0-alpha.11.0.0-beta.21.0.0-rc.1。这样团队一看就知道当前版本处于什么阶段。

注意:不要用 1.0.0-SNAPSHOT 这种版本号上生产环境。SNAPSHOT 意味着「随时可能变」,你想想看,生产环境用这种版本,出了问题你找谁去?

3.2 Maven的pom.xml版本管理

Maven的版本管理,说白了就是靠 pom.xml 里的几个标签。我刚开始用Maven时,也踩过不少坑。

3.2.1 统一版本号管理

最基础的做法是用 <properties> 定义版本号:

<properties>
    <spring.version>5.3.20</spring.version>
    <jackson.version>2.13.3</jackson.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
</dependencies>

这样做的好处很明显:改一个地方,所有依赖都跟着变。我曾经在一个项目里看到十几个地方都写了同一个版本号,升级的时候改漏了一个,结果线上出了兼容性问题。嗯,从那以后我再也不敢偷懒了。

3.2.2 依赖管理(dependencyManagement)

如果是多模块项目,我建议用 <dependencyManagement>。这个标签不会直接引入依赖,而是统一声明版本号:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>common-lib</artifactId>
            <version>2.1.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

子模块引用时就不用写版本号了:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>common-lib</artifactId>
</dependency>

小技巧:mvn versions:display-dependency-updates 命令可以检查依赖是否有新版本。我每周跑一次,看看哪些依赖该升级了。

3.3 Gradle的版本目录与依赖锁定

Gradle在版本管理上比Maven灵活不少。我个人更喜欢Gradle的方式,尤其是它的版本目录(Version Catalog)。

3.3.1 版本目录(Version Catalog)

gradle/libs.versions.toml 文件中定义:

[versions]
spring = "5.3.20"
jackson = "2.13.3"

[libraries]
spring-core = { module = "org.springframework:spring-core", version.ref = "spring" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }

然后在 build.gradle 里引用:

dependencies {
    implementation libs.spring.core
    implementation libs.jackson.databind
}

这样做的好处是版本号集中管理,而且IDE还能自动补全。我刚开始用的时候觉得多此一举,后来发现大型项目里这个功能真香。

3.3.2 依赖锁定(Gradle Locking)

依赖锁定是个好东西。它能把当前用的所有依赖版本都记下来,防止别人不小心升级了某个依赖。

开启方式很简单:

dependencyLocking {
    lockAllConfigurations()
}

然后运行 gradle dependencies --write-locks,Gradle会生成一个 gradle/dependency-locks/*.lockfile 文件。这个文件要提交到Git里。

为什么需要锁定?你想想看,如果团队里有人加了新依赖,或者某个间接依赖自动升级了,没有锁定的话,每个人的构建结果可能都不一样。我曾经遇到过因为间接依赖版本不一致,导致测试环境没问题、生产环境出bug的情况。

3.4 版本管理的避坑指南

最后分享几个我踩过的坑:

  • 不要用范围版本号:比如 [1.0,2.0) 这种写法,看起来灵活,实际上每次构建都可能拿到不同的版本。我见过一个项目因为这个,测试通过了,上线却挂了。
  • 锁定间接依赖:Maven用 maven-enforcer-plugin,Gradle用依赖锁定。间接依赖的版本冲突是线上事故的常见原因。
  • 版本号要写全:不要写 1.0 这种简写,要写 1.0.0。不然Maven/Gradle会怎么解析,你根本猜不到。
  • 定期升级依赖:我建议每个月花半天时间,检查一下依赖有没有安全漏洞。用 OWASP Dependency-Check 或者 GitHub Dependabot 都行。

我的习惯:每次升级依赖后,跑一遍完整的测试套件。如果测试不通过,就回滚版本,查清楚原因再升级。别图省事跳过测试,否则你会在生产环境上「补课」。

版本管理这件事,说白了就是「约定」+「工具」。约定好版本号的规则,用工具把规则落地。做到这两点,版本管理就不会成为你的痛点。


公众号:蓝海资料掘金营,微信deep3321