4、依赖传递性与版本管理

依赖传递性,说白了就是“你依赖了一根线,它背后还连着一团毛线”。我刚接触 Gradle 时,最怕的就是莫名其妙的版本冲突。项目明明只声明了三个依赖,一跑起来报错说 jar 包版本不对——查了半天,原来是被某个间接依赖给“带歪”了。

4.1 传递性依赖机制

Gradle 默认会帮你把依赖的依赖也拉进来。比如我声明依赖了 Spring Boot,Gradle 就会自动把 Spring Core、Jackson、Tomcat 等一整套全部拽下来。这通常是好事,省得你手动一个个加。但问题在于,如果两个不同的一级依赖都依赖了同一个库,却要求不同版本,冲突就来了。

举个例子,在 build.gradle 里写:

dependencies {
    implementation 'com.example:library-a:1.0'
    implementation 'com.example:library-b:2.0'
}

如果 library-a 的 pom 里声明了 com.example:common:1.0,而 library-b 声明的却是 com.example:common:2.0,Gradle 会采用哪个版本?

Gradle 的策略是:最短路径优先。如果路径长度相同,则看哪个版本在最上面声明(类似 Maven 的最近者优先)。但如果你显式声明了 strictly,规则就会被覆盖。

我个人习惯在团队项目里用 gradle dependencies 命令先扫一遍依赖树,看看有没有被偷偷升级的 jar。这招在接手老项目时特别管用。

下面这张图展示了传递性依赖的核心流程:

传递性依赖解析流程 项目 A(我的应用) 一级依赖 B 一级依赖 C 传递依赖 D(冲突点) 版本冲突:Gradle 最短路径优先 使用 force / strictly 干预结果 B 依赖 D:1.0 C 依赖 D:2.0

4.2 版本声明方式

你可以在 build.gradle 里用几种不同的方式声明版本。除了常规的 version,还有 force 和 strictly。它们的区别我整理了一张表:

声明方式 含义 影响范围 覆盖能力
version 常规版本声明 当前项目 无强制力,可能被传递依赖覆盖
force 强制使用指定版本 整个依赖图 会覆盖几乎所有传递依赖版本
strictly 严格锁定版本,失败报错 整个依赖图 + 校验机制 无可协商余地,版本不符直接构建失败

具体写法是这样的:

dependencies {
    implementation('com.example:common:2.0') {
        // 方式一:普通版本声明(默认就是version)
        version {
            strictly '2.0'   // 等价于直接写版本号 + strictly
        }
    }

    // 方式二:force 写法(已不推荐,建议用 strictly 替代)
    implementation('com.example:common:1.5') {
        version {
            strictly '1.5'   // 别用 force,用 strictly 更明确
        }
    }
}

你注意到了吗?我上面特意写了 force 写法,但我个人强烈建议你 不要再写 force。Gradle 在 4.x 之后已经用 strictly 来承载“强制版本”的职责。你要是还在用 force,不仅语义模糊,未来版本可能会被移除。

记住:strictly 是 Gradle 官方推荐的强制版本方式。它会在依赖解析时执行严格校验,如果冲突无法解决,构建直接失败,而不是静默采用某个不可控版本。

4.3 强制版本与 strictly

用 strictly 时,你还可以搭配 require 和 prefer 来做更精细的控制。比如:

dependencies {
    implementation('com.example:common') {
        version {
            strictly '[1.5, 2.0)'  // 符合语义化版本范围
            prefer '1.8'           // 在这个范围内优先使用 1.8
        }
    }
}

我曾经在一个支付项目里遇到过这样的坑:一个底层日志库偷偷从 1.2 升到了 2.0,导致 JSON 序列化行为变了,线上出 bug。查了两天才发现是某个新引入的 SDK 依赖里写了 strictly '2.0',直接盖过了我们项目的 1.2 版本。

从那以后,我会在项目根目录的 build.gradle 里显式声明所有核心库的 strictly 版本,防止外部库偷偷升级:

dependencies {
    constraints {
        implementation('com.example:logging-api') {
            version {
                strictly '1.2.0'
                // 如果有安全补丁需要升级,我们先在这里改,而不是被传递依赖带偏
            }
        }
    }
}

小心:strictly 是一把双刃剑。你一旦用了它,Gradle 会强制所有传递依赖都要遵循这个版本。如果有一个别的依赖无论如何都要求 2.0,构建就会报错。这时候你得用 dependencies 命令去分析冲突根因,要么升级你约束的版本,要么排除冲突的传递依赖。

我曾经在微服务拆分的项目里因为滥用 strictly,导致整个 CI 频繁失败。后来总结的经验是:只对核心库(如序列化、数据库驱动)用 strictly,工具类的依赖没必要锁死。

最后分享一个小技巧:如果你只是想阻止某个传递依赖被拉进来,不一定用 strictly。你可以直接用 exclude 或者 transitive = false。比如:

implementation('com.example:library-b') {
    transitive = false  // 禁用所有传递依赖
}

但说实话,我不建议全局禁用传递性,那会让项目变得臃肿——你反而要手动补一堆 jar。更好的做法是单独排除有问题的那个依赖。嗯,这里要记住:版本管理的核心不是锁死,而是可复现、可追踪。推荐你把项目的 gradle.lockfile 纳入版本控制,这样每次构建的依赖版本都是确定的。