Maven最全避坑指南

在各类人工智能AI助手出来后,我发现写技术性文章的欲望变得越来越低。我经常会问自己:这个问题AI能解决吗?AI总结得比我全面吗?AI的回答会更清晰简洁、通俗易懂吗?

如果上面三个问题的回答都是肯定的,那自然没有写文章的必要性了。但只要有一个回答是否定的,那我还是想写一写,即使在AI已经如此发达的时代。

若能助君,烦请三连。

问题描述

最近在对一个老项目进行改造,项目是传统的Spring boot架构,项目中有一个父模块,父模块里有两个子模块,结构类似如下:

parent
├── children-1
│   └── children-1.pom
├── children-2
│   └── children-2.pom
└── parent.pom

这个结构很经典,也很常用,但是我在父模块引入一个依赖后,发现子模块并不能直接把这个依赖用到的类给引进来。

好神奇啊,我仔细看了一下父模块的pom依赖,依赖是这么写的:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 父模块基本信息 -->
    <artifactId>parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <!-- 父模块不参与打包,所以设置为pom-->
    <packaging>pom</packaging>
    <name>parent</name>
    <description>Parent module for Spring Boot project</description>

    <!-- 继承公司的父依赖(关键) -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>common-parent</artifactId>
        <version>2.7.18</version> <!-- 根据项目实际版本调整 -->
        <relativePath/>
    </parent>

    <!-- 子模块声明 -->
    <modules>
        <module>children-1</module>
        <module>children-2</module>
    </modules>

    <!-- 依赖管理(统一管理版本) -->
    <dependencyManagement>
        <dependencies>
            <!-- 我引入的新依赖,这里以 MapStruct 依赖(示例) -->
            <dependency>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct</artifactId>
                <version>1.5.3.Final</version> <!-- 稳定版本 -->
            </dependency>
            <dependency>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.5.3.Final</version>
                <scope>provided</scope>
            </dependency>

            <!-- 其他通用依赖 -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.24</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- 公共依赖(子模块会自动继承,无需重复声明) -->
    <dependencies>
        <!-- 如需所有子模块都强制依赖某个包,可在此处声明 -->
        <!-- 例如:日志框架 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
    </dependencies>

  <!--编译插件-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

我引入的依赖在这里:
新增依赖

定义任务

我这次加依赖,只是在父模块的dependencyManagement来直接引入依赖,如下:
在这里插入图片描述

死去的回忆开始攻击我,dependencyManagement只是为了做统一的依赖版本管理,而不是真正的引入依赖,就此入手开改。

解决方案

我在子模块里找到了dependencies,并在里面真正的加入了依赖,不过我没有加版本,因为版本已经在父模块里做了声明,点击依赖左上角,也能跳到父模块的依赖声明部分:

在这里插入图片描述

这时候,代码里终于不报错了,嘻嘻嘻。

我准备收工时,打包工程,另一个问题出现了:

在这里插入图片描述

这个玩意,咋来的呢?

之前的父模块里的模块里虽然有children-1这个模块,但是children-1这个module又以其他版本的Spring boot parent作为他的父亲,类似这样:

在这里插入图片描述

出于直觉,这不对啊,parent里都有你这个模块了,你这里又继承了新的parent,逆子!!!就把它改成这样:

在这里插入图片描述

结果改完,打包时就爆了上面的错❌。

难道真不能随便改前人的代码?

理论上来说,子模块属于一个公共的模块,不需要启动类;子模块的pom在被我修改后,除了继承的父模块不一样之外,其他的都一样,那问题还是出在父亲身上了。再仔细一看,父模块这里:

在这里插入图片描述

好家伙,这编译插件直接写

一般来说,父模块的编译插件也只是做一个版本管理,而不是直接依赖,例如:

    <build>
        <pluginManagement>
            <plugins>
                <!-- Spring Boot Maven插件 -->
                <plugin>
                  <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <!-- 可选:配置全局属性 -->
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                            </exclude>
                        </excludes>
                    </configuration>
                    <!-- 注意:不在父模块激活插件,让子模块自行决定 -->
                </plugin>

                <!-- 其他常用插件 -->
            </plugins>
        </pluginManagement>
    </build>

所以,我猜前人应该是改不了父工程的情况下,直接就改了子模块的父依赖,好一个去父留子

但是,这不好,实际上,一个工程里,应该只有一个父依赖,这样才能保证所有依赖的统一管理

思考再三,有三种解决方案:

  1. 方案一

直接改父模块的插件配置为pluginManagement,然后,在需要的子模块里做引入。

如果在父模块能改的情况下,这是最好的。

  1. 方案二

在子模块中排除 Spring Boot Maven 插件

在子模块的pom.xml里添加如下配置,以此排除 Spring Boot Maven 插件:

<build>
  <plugins>
    <!-- 禁用父模块传递的 spring-boot-maven-plugin -->
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <version>${spring-boot.version}</version> <!-- 与父模块版本一致 -->
      <configuration>
        <skip>true</skip> <!-- 关键:跳过插件执行 -->
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>repackage</goal> <!-- 父模块可能配置了 repackage 目标 -->
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
  1. 方案三(推荐)

禁用子模块的 spring-boot-maven-plugin 的主类检查


<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <mainClass>dummy.DummyMainClass</mainClass>
                <skip>true</skip> <!-- 跳过启动类查找 -->
            </configuration>
        </plugin>
    </plugins>
</build>

总结

在复杂的分布式系统中,maven的依赖管理实在是会让人头大,maven的特性实际上也不多,我认为最重要的就两个:依赖传递和依赖冲突

依赖传递:简化与复杂性的双重来源

依赖传递是 Maven 最强大的特性之一,它允许项目仅声明直接依赖,而间接依赖(即直接依赖的依赖)会被自动解析并引入。这一机制带来了显著的优势:

  • 减少重复声明:无需在每个模块中重复声明基础库依赖
  • 版本一致性:同一依赖在整个项目中保持相同版本
  • 构建效率:依赖树扁平化,减少磁盘空间占用

示例:
假设项目依赖spring-boot-starter-web,而 starter 又依赖spring-web和spring-webmvc。通过依赖传递,只需声明:


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

Maven 会自动解析并引入所有间接依赖,包括spring-web、spring-webmvc及其深层依赖。

潜在问题:

  • 版本冲突:当多个直接依赖引入同一库的不同版本时
  • 依赖膨胀:传递性依赖可能引入不必要的库
  • 兼容性风险:间接依赖的版本可能与项目不兼容

依赖冲突:复杂性的根源与解决方案

可以说,本文源自依赖冲突。

依赖冲突是依赖传递带来的主要挑战,常见的冲突场景包括:

路径优先原则(Path Depth):Maven会 默认选择路径最短的依赖


项目 -> A:1.0 -> B:1.0
项目 -> C:1.0 ->D:1.0 ->  B:2.0

此时 B 的版本将被解析为 1.0(路径更短)

声明顺序原则(Declaration Order):当路径长度相同时,POM 中先声明的依赖优先


<dependencies>
    <dependency> <!-- 优先选择A:1.0 -->
        <groupId>com.example</groupId>
        <artifactId>A</artifactId>
        <version>1.0</version>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>B</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

传递性依赖冲突:间接依赖的版本冲突更为隐蔽



项目 -> A:1.0 -> C:1.0
项目 -> B:1.0 -> C:2.0

此时需要显式干预 C 的版本

最佳实践

在分布式系统中使用 Maven 管理依赖,面对服务众多、独立部署、依赖关系复杂等特点,遵循以下最佳实践至关重要,以确保构建的可重复性、依赖一致性、效率和安全性:

  1. 结构化项目(多模块 vs 多仓库)

    • 多模块项目 (Maven Multi-Module):
      • 适用场景: 紧密相关的服务(例如,属于同一个业务域,经常需要协同开发、同时发布)。
      • 优点: 共享父 POM 配置,强制依赖版本一致性,单命令构建所有相关模块,简化依赖管理。
      • 最佳实践:
        • 定义一个清晰的父 POM (<packaging>pom</packaging>)。
        • 在父 POM 中使用 <modules> 列出所有子模块。
        • 核心: 在父 POM 的 <dependencyManagement><pluginManagement> 中集中定义所有依赖项和插件的版本。子模块只声明依赖项(不指定版本)和插件(通常只需<groupId><artifactId>)。
        • 父 POM 定义公共属性(Java版本、编码、公共依赖版本属性等)。
        • 子模块继承父 POM。
    • 独立仓库 (Separate Repositories):
      • 适用场景: 服务之间耦合度低,独立开发和部署节奏差异大,分属不同团队或代码库。
      • 优点: 服务完全解耦,部署更灵活,技术栈选择更自由(虽然 Maven 限制了语言)。
      • 最佳实践:
        • 每个服务是独立的 Maven 项目,拥有自己的 POM。
        • 关键: 建立并严格遵守一个公司/项目范围的 BOM (Bill of Materials)
        • 所有服务必须导入这个 BOM,并在其 <dependencyManagement> 部分使用 <scope>import</scope>
        • 在服务 POM 的 <dependencies> 中声明依赖时禁止指定版本号,版本由 BOM 控制。
  2. 集中化依赖版本管理 (BOM / Dependency Management)

    • 这是分布式系统 Maven 管理的核心!
    • 创建 BOM 项目:
      • 一个独立的 Maven 项目,<packaging>pom</packaging>
      • 在其 <dependencyManagement> 部分,精确定义所有允许使用的第三方库及其精确版本
      • 也可以定义公共插件的版本。
    • 服务项目使用 BOM:
      <dependencyManagement>
          <dependencies>
              <dependency>
                  <groupId>com.yourcompany</groupId>
                  <artifactId>platform-bom</artifactId>
                  <version>1.0.0</version>
                  <type>pom</type>
                  <scope>import</scope> <!-- 关键!导入依赖管理 -->
              </dependency>
          </dependencies>
      </dependencyManagement>
      
    • 服务声明依赖 (不指定版本):
      <dependencies>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
              <!-- 版本由 platform-bom 控制 -->
          </dependency>
      </dependencies>
      
    • 优势:
      • 强制一致性: 所有服务使用相同版本的库,极大减少兼容性问题。
      • 单点升级: 升级库版本只需修改 BOM 并发布新版本,各服务更新 BOM 引用即可。
      • 可见性: 明确列出所有允许的依赖及其版本。
      • 减少冲突: 显著降低传递依赖冲突的可能性。
  3. 明确定义和严格控制依赖范围 (<scope>)

    • compile (默认): 编译、测试、运行都需要。谨慎添加,确保确实是运行时必需。
    • provided 容器或环境会提供(如 Servlet API, Tomcat 内嵌库)。避免打包到 WAR/JAR 中造成冲突或冗余。
    • runtime 编译不需要,但运行和测试需要(如 JDBC 驱动)。
    • test 仅用于测试(JUnit, Mockito 等)。绝对不要让测试依赖泄漏到运行时。
    • import 仅用于 <dependencyManagement> 中导入 BOMs。
    • 最佳实践: 尽可能使用最严格的作用域。testprovided 是你的好朋友,能有效瘦身和避免冲突。
  4. 使用属性 (<properties>) 管理版本号和其他配置

    • 在父 POM 或 BOM 中,为常用依赖版本、插件版本、Java 版本、编码等定义属性。
    • 示例:
      <properties>
          <java.version>17</java.version>
          <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
          <spring-boot.version>3.1.0</spring-boot.version>
          <lombok.version>1.18.28</lombok.version>
      </properties>
      
    • 在依赖或插件配置中引用:
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
          <version>${spring-boot.version}</version> <!-- 引用属性 -->
      </dependency>
      
    • 优势: 集中管理,一处修改,全局生效;提高 POM 可读性。
  5. 管理传递依赖和冲突

    • 理解传递依赖: Maven 会自动引入依赖的依赖。使用 mvn dependency:tree 命令经常查看依赖树。
    • 冲突解决: Maven 遵循“最近定义优先”原则。结构化项目(父POM/BOM)本身已极大减少冲突。
    • 排除依赖 (<exclusions>):
      • 当传递依赖导致问题(版本错误、冲突、不需要)时使用。
      • 在声明依赖的地方添加 <exclusions>
      • 谨慎使用: 排除可能破坏功能。优先考虑在 BOM 中统一管理源头依赖的版本来避免冲突。
    • <optional>true</optional> 标记非强制的传递依赖(例如库支持多种数据库,但用户只需一种)。依赖方需要显式声明才能使用。
  6. 插件管理 (<pluginManagement>)

    • 在父 POM 或 BOM 的 <build><pluginManagement> 中定义公共插件的版本和核心配置。
    • 子模块只需声明 <plugin>(通常只需 groupIdartifactId),版本和公共配置自动继承。
    • 确保关键插件(如编译器、打包插件、Spring Boot Maven Plugin)版本和基础配置一致。
  7. 仓库配置 (Repositories)

    • 使用 Nexus/Artifactory: 强烈推荐搭建并使用企业级私有 Maven 仓库(如 Nexus Repository Manager 或 JFrog Artifactory)。
    • 配置镜像:settings.xml (全局或用户级) 中配置私有仓库为镜像 (<mirror>),加速下载并控制来源。
    • 仓库声明: 对于私有仓库或特定第三方仓库,在父 POM 或 BOM 的 <repositories><pluginRepositories> 中声明,确保所有构建都能找到依赖。避免在每个子模块重复声明。
    • 快照 vs 发布: 区分快照仓库(用于开发中不稳定的版本)和发布仓库(用于稳定版本)。在 POM 中正确使用 -SNAPSHOT 后缀。
  8. 持续集成 (CI) 集成

    • 在 CI 流程中,确保每次构建都使用干净的本地仓库或利用 CI 工具的缓存机制(但要小心缓存污染)。
    • mvn dependency:tree 作为 CI 步骤的一部分输出或分析,监控依赖变化。
    • 使用 maven-dependency-pluginanalyzepurge-local-repository 目标(谨慎)进行更深入检查或清理。
    • 使用 versions-maven-plugin 定期检查依赖更新。
  9. 安全扫描

    • 将 OWASP Dependency-Check Maven 插件 (org.owasp:dependency-check-maven) 集成到构建中。
    • 配置它在 CI 流程中运行,扫描依赖中的已知漏洞(CVE)并生成报告。
    • 设置构建失败策略(例如发现严重漏洞时失败)。
  10. 文档化与沟通

    • 清晰记录 BOM 的维护流程、版本升级策略。
    • 沟通依赖变更(尤其是 BOM 升级或关键库升级)对各个服务的影响。
    • 在服务 POM 或 README 中说明其关键依赖和与 BOM 的关系。

总结关键点:

  • 强制一致性是王道: BOM 或严格的父 POM <dependencyManagement> 是分布式系统依赖管理的基石。
  • 作用域要精准:test, provided 这样的作用域是控制依赖影响范围的关键工具。
  • 属性集中管理: 利用 <properties> 提升可维护性。
  • 私有仓库不可少: Nexus/Artifactory 是企业级实践的标配。
  • 依赖树要常看: mvn dependency:tree 是理解依赖关系的瑞士军刀。
  • 安全扫描要集成: 依赖漏洞扫描必须自动化。
  • CI 是保障: 在持续集成流程中固化依赖管理实践(干净构建、依赖树、安全检查)。

遵循这些最佳实践,可以在分布式系统的复杂环境中,利用 Maven 构建出稳定、可重复、安全且易于维护的组件。核心在于通过 BOM 和集中化管理实现对众多服务依赖版本的强有力控制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值