一文彻底搞懂 Maven 依赖——从 <dependency> 到依赖冲突,带你看懂 Maven 的“江湖规矩”

Java教程 2025-11-14

一、前言:为什么要研究依赖?

写 Java 项目,谁没被 Maven “支配”过呢?

你加了个 Spring Boot Starter,结果一堆库跟着进来;
别人告诉你“scope 写错了”;
编译正常但运行报错,或者 jar 包体积暴涨到 200MB。

这一切背后,其实都是 Maven 依赖系统 在发挥作用。

要真正掌握 Maven,就得先搞清楚:


二、依赖的本质:三段坐标

Maven 的核心设计哲学之一是“声明式依赖”。
你不需要手动下载 jar,只要写出三个坐标:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
    <version>3.3.2version>
dependency>

这三个坐标就像一个图书馆的“索书号”:

  • groupId:组织名(相当于出版社)
  • artifactId:模块名(相当于书名)
  • version:版本号(相当于第几版)
元素含义
组织或公司标识
模块名称
版本号
依赖作用范围(compile、provided、runtime...)
是否为可选依赖
排除指定传递依赖

三、Maven 的依赖来源

Maven 在解析依赖时,会按照以下顺序查找 jar 包:

  1. 本地仓库~/.m2/repository
    → 最近一次构建下载过的包会被缓存到这里。
  2. 远程中央仓库https://repo.maven.apache.org/maven2/
    → Maven 官方中央仓库。
  3. 私有仓库(公司 Nexus / Artifactory)
    → 企业内部维护的依赖镜像。

Maven 会自动从上往下找,找不到就报错:


四、依赖范围(Scope)详解

Scope 是 Maven 的依赖生命周期规则,定义了依赖在哪些阶段可用、是否参与打包、是否传递。

Scope编译时可见测试时可见运行时可见打包带上可传递典型场景
compile默认值,大多数库
provided容器已提供(Servlet、Lombok)
runtimeJDBC Driver、Logback
testJUnit、Mockito
system手动指定 jar
import仅用于依赖管理

五、每种 Scope 的典型示例

1️⃣ compile —— 默认的依赖方式

<dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-lang3artifactId>
    <version>3.14.0version>
dependency>

特点:

  • 编译、运行、测试全阶段可用;
  • 可传递;
  • 打包会带上。

适合:核心依赖(比如 Spring Context、Apache Commons)。


2️⃣ provided —— 编译要用,运行别带

<dependency>
    <groupId>javax.servletgroupId>
    <artifactId>javax.servlet-apiartifactId>
    <version>4.0.1version>
    <scope>providedscope>
dependency>

适合:由容器(Tomcat、Jetty)或环境提供的类库。
打包带上会冲突。


3️⃣ runtime —— 运行时才需要的依赖

<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-jartifactId>
    <version>9.1.0version>
    <scope>runtimescope>
dependency>

特点:

  • 编译不需要(用接口即可);
  • 运行时才加载;
  • 打包会带上。

适合:数据库驱动、日志实现等。


4️⃣ test —— 仅在测试阶段使用

<dependency>
    <groupId>org.junit.jupitergroupId>
    <artifactId>junit-jupiterartifactId>
    <version>5.11.0version>
    <scope>testscope>
dependency>

不会参与最终打包,测试用完即止。


5️⃣ system —— 手动指定路径

<dependency>
    <groupId>com.companygroupId>
    <artifactId>internal-libartifactId>
    <version>1.0version>
    <scope>systemscope>
    <systemPath>${project.basedir}/lib/internal-lib.jarsystemPath>
dependency>

️ 注意:

  • 不推荐使用;
  • 不可传递;
  • 会破坏构建的可移植性。

6️⃣ import —— 依赖版本管理用

用于在 dependencyManagement 中引入 BOM(Bill of Materials)

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.bootgroupId>
      <artifactId>spring-boot-dependenciesartifactId>
      <version>3.3.2version>
      <type>pomtype>
      <scope>importscope>
    dependency>
  dependencies>
dependencyManagement>

它不会引入依赖本身,只是导入一组“版本约定”。


六、依赖传递机制:Maven 的“层层借书”

假设:

  • A → 依赖 B
  • B → 依赖 C

则 A 间接依赖了 C(称为传递依赖)。

Maven 的传递规则如下:

A 的 ScopeB 的 ScopeC 是否传递说明
compilecompile默认传递
compileprovided不传递
providedcompile不传递
test任意不传递
runtimecompile/runtime传递

简单理解:


️ 七、依赖冲突与解决策略

当两个不同版本的相同依赖出现时:

  • 最近路径优先(Nearest Definition Wins)
    → Maven 会选择依赖树中路径最短的版本。

例:

ABcommons-lang3:3.12.0  
ACcommons-lang3:3.14.0

A 直接依赖 C 的路径更短,则取 3.14.0。

如果两者路径一样长:

  • 则选择 声明顺序靠前 的依赖。

查看依赖树命令:

mvn dependency:tree

可查看传递依赖及冲突来源。


强制指定版本:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.apache.commonsgroupId>
      <artifactId>commons-lang3artifactId>
      <version>3.14.0version>
    dependency>
  dependencies>
dependencyManagement>

dependencyManagement 只定义版本,不自动引入依赖。


八、依赖排除(Exclusion)

有时候我们不想要某个传递依赖:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-tomcatartifactId>
        exclusion>
    exclusions>
dependency>

比如:自己要用 Undertow 或 Jetty,而不想要 Tomcat。


九、最佳实践总结

场景Scope 建议原因
普通库依赖compile默认
容器内置库(Servlet、JSP)provided环境已提供
运行时驱动(JDBC、日志实现)runtime只运行时用
测试框架test不参与打包
编译工具(Lombok、MapStruct)provided编译期生效
公司内部 jarsystem(慎用)构建可移植性差
统一管理版本import(BOM)方便升级维护

记忆口诀:

像玩 RPG 游戏一样,你给每个依赖分配“职业技能”,
打包、传递、运行都明明白白,不再踩坑!

十、 —— 控制“依赖传递”的另一种方式

现在我们聊聊另一个常被忽略的兄弟:

用于告诉 Maven:

例子:

<dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-simpleartifactId>
    <version>2.0.9version>
    <optional>trueoptional>
dependency>

这意味着:

  • 当前模块能用 slf4j-simple
  • 但依赖此模块的下游项目不会自动拿到它;
  • 如果想用,必须手动声明。

使用场景

场景是否适合
SDK、框架模块 非常推荐
Spring Boot Starter 常用
应用层️ 一般不用
工具类库 不推荐

optional vs provided

特征trueprovided
控制对象依赖传递生命周期
编译期可见
运行期可见(环境提供)
传递性 不传递 不传递
场景模块设计、SDKWeb 环境、容器依赖

通俗地说:

  • scope 决定“何时使用”;
  • optional 决定“要不要传下去”。

十一、依赖冲突与解决规则

Maven 在面对同一个依赖的多个版本时,遵循两条核心规则:

  1. 最近路径优先(Nearest Definition Wins)
    —— 谁离当前模块更近,用谁。
  2. 先声明优先(First Declaration Wins)
    —— 同层级冲突时,谁先写谁赢。

可通过以下命令查看依赖树:

mvn dependency:tree

十二、全景图:Maven 依赖生命周期与传递机制(附图)

cc.png


十四、总结与金句彩蛋

元素控制内容核心作用
生命周期控制在哪些阶段可见
传递性决定是否下游继承
精准排除清理依赖树

一句话记忆


尾声:让依赖管理优雅如诗

每次写 ,都像在雕琢项目的骨架。
当你真正理解 scopeoptional 与传递关系的微妙平衡,
你就离“构建大师”更近一步了。