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

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

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

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

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


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

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.3.2</version>
</dependency>

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

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

三、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.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.14.0</version>
</dependency>

特点:

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

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


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

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

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


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

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>9.1.0</version>
    <scope>runtime</scope>
</dependency>

特点:

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

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


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

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>

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


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

<dependency>
    <groupId>com.company</groupId>
    <artifactId>internal-lib</artifactId>
    <version>1.0</version>
    <scope>system</scope>
    <systemPath>${project.basedir}/lib/internal-lib.jar</systemPath>
</dependency>

️ 注意:

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

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

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

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>3.3.2</version>
      <type>pom</type>
      <scope>import</scope>
    </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.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.14.0</version>
    </dependency>
  </dependencies>
</dependencyManagement>

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


八、依赖排除(Exclusion)

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

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


九、最佳实践总结

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

记忆口诀:

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

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

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

<optional> 用于告诉 Maven:

例子:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>2.0.9</version>
    <optional>true</optional>
</dependency>

这意味着:

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

使用场景

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

optional vs provided

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

通俗地说:

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

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

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

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

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

mvn dependency:tree

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

cc.png


十四、总结与金句彩蛋

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

一句话记忆


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

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

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]