在各类人工智能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>
所以,我猜前人应该是改不了父工程的情况下,直接就改了子模块的父依赖,好一个去父留子。
但是,这不好,实际上,一个工程里,应该只有一个父依赖,这样才能保证所有依赖的统一管理。
思考再三,有三种解决方案:
- 方案一
直接改父模块的插件配置为pluginManagement,然后,在需要的子模块里做引入。
如果在父模块能改的情况下,这是最好的。
- 方案二
在子模块中排除 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>
- 方案三(推荐)
禁用子模块的 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 管理依赖,面对服务众多、独立部署、依赖关系复杂等特点,遵循以下最佳实践至关重要,以确保构建的可重复性、依赖一致性、效率和安全性:
-
结构化项目(多模块 vs 多仓库)
- 多模块项目 (Maven Multi-Module):
- 适用场景: 紧密相关的服务(例如,属于同一个业务域,经常需要协同开发、同时发布)。
- 优点: 共享父 POM 配置,强制依赖版本一致性,单命令构建所有相关模块,简化依赖管理。
- 最佳实践:
- 定义一个清晰的父 POM (
<packaging>pom</packaging>)。 - 在父 POM 中使用
<modules>列出所有子模块。 - 核心: 在父 POM 的
<dependencyManagement>和<pluginManagement>中集中定义所有依赖项和插件的版本。子模块只声明依赖项(不指定版本)和插件(通常只需<groupId>和<artifactId>)。 - 父 POM 定义公共属性(Java版本、编码、公共依赖版本属性等)。
- 子模块继承父 POM。
- 定义一个清晰的父 POM (
- 独立仓库 (Separate Repositories):
- 适用场景: 服务之间耦合度低,独立开发和部署节奏差异大,分属不同团队或代码库。
- 优点: 服务完全解耦,部署更灵活,技术栈选择更自由(虽然 Maven 限制了语言)。
- 最佳实践:
- 每个服务是独立的 Maven 项目,拥有自己的 POM。
- 关键: 建立并严格遵守一个公司/项目范围的 BOM (Bill of Materials)。
- 所有服务必须导入这个 BOM,并在其
<dependencyManagement>部分使用<scope>import</scope>。 - 在服务 POM 的
<dependencies>中声明依赖时禁止指定版本号,版本由 BOM 控制。
- 多模块项目 (Maven Multi-Module):
-
集中化依赖版本管理 (BOM / Dependency Management)
- 这是分布式系统 Maven 管理的核心!
- 创建 BOM 项目:
- 一个独立的 Maven 项目,
<packaging>pom</packaging>。 - 在其
<dependencyManagement>部分,精确定义所有允许使用的第三方库及其精确版本。 - 也可以定义公共插件的版本。
- 一个独立的 Maven 项目,
- 服务项目使用 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 引用即可。
- 可见性: 明确列出所有允许的依赖及其版本。
- 减少冲突: 显著降低传递依赖冲突的可能性。
-
明确定义和严格控制依赖范围 (
<scope>)compile(默认): 编译、测试、运行都需要。谨慎添加,确保确实是运行时必需。provided: 容器或环境会提供(如 Servlet API, Tomcat 内嵌库)。避免打包到 WAR/JAR 中造成冲突或冗余。runtime: 编译不需要,但运行和测试需要(如 JDBC 驱动)。test: 仅用于测试(JUnit, Mockito 等)。绝对不要让测试依赖泄漏到运行时。import: 仅用于<dependencyManagement>中导入 BOMs。- 最佳实践: 尽可能使用最严格的作用域。
test和provided是你的好朋友,能有效瘦身和避免冲突。
-
使用属性 (
<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 可读性。
-
管理传递依赖和冲突
- 理解传递依赖: Maven 会自动引入依赖的依赖。使用
mvn dependency:tree命令经常查看依赖树。 - 冲突解决: Maven 遵循“最近定义优先”原则。结构化项目(父POM/BOM)本身已极大减少冲突。
- 排除依赖 (
<exclusions>):- 当传递依赖导致问题(版本错误、冲突、不需要)时使用。
- 在声明依赖的地方添加
<exclusions>。 - 谨慎使用: 排除可能破坏功能。优先考虑在 BOM 中统一管理源头依赖的版本来避免冲突。
<optional>true</optional>: 标记非强制的传递依赖(例如库支持多种数据库,但用户只需一种)。依赖方需要显式声明才能使用。
- 理解传递依赖: Maven 会自动引入依赖的依赖。使用
-
插件管理 (
<pluginManagement>)- 在父 POM 或 BOM 的
<build><pluginManagement>中定义公共插件的版本和核心配置。 - 子模块只需声明
<plugin>(通常只需groupId和artifactId),版本和公共配置自动继承。 - 确保关键插件(如编译器、打包插件、Spring Boot Maven Plugin)版本和基础配置一致。
- 在父 POM 或 BOM 的
-
仓库配置 (Repositories)
- 使用 Nexus/Artifactory: 强烈推荐搭建并使用企业级私有 Maven 仓库(如 Nexus Repository Manager 或 JFrog Artifactory)。
- 配置镜像: 在
settings.xml(全局或用户级) 中配置私有仓库为镜像 (<mirror>),加速下载并控制来源。 - 仓库声明: 对于私有仓库或特定第三方仓库,在父 POM 或 BOM 的
<repositories>和<pluginRepositories>中声明,确保所有构建都能找到依赖。避免在每个子模块重复声明。 - 快照 vs 发布: 区分快照仓库(用于开发中不稳定的版本)和发布仓库(用于稳定版本)。在 POM 中正确使用
-SNAPSHOT后缀。
-
持续集成 (CI) 集成
- 在 CI 流程中,确保每次构建都使用干净的本地仓库或利用 CI 工具的缓存机制(但要小心缓存污染)。
- 将
mvn dependency:tree作为 CI 步骤的一部分输出或分析,监控依赖变化。 - 使用
maven-dependency-plugin的analyze或purge-local-repository目标(谨慎)进行更深入检查或清理。 - 使用
versions-maven-plugin定期检查依赖更新。
-
安全扫描
- 将 OWASP Dependency-Check Maven 插件 (
org.owasp:dependency-check-maven) 集成到构建中。 - 配置它在 CI 流程中运行,扫描依赖中的已知漏洞(CVE)并生成报告。
- 设置构建失败策略(例如发现严重漏洞时失败)。
- 将 OWASP Dependency-Check Maven 插件 (
-
文档化与沟通
- 清晰记录 BOM 的维护流程、版本升级策略。
- 沟通依赖变更(尤其是 BOM 升级或关键库升级)对各个服务的影响。
- 在服务 POM 或 README 中说明其关键依赖和与 BOM 的关系。
总结关键点:
- 强制一致性是王道: BOM 或严格的父 POM
<dependencyManagement>是分布式系统依赖管理的基石。 - 作用域要精准: 像
test,provided这样的作用域是控制依赖影响范围的关键工具。 - 属性集中管理: 利用
<properties>提升可维护性。 - 私有仓库不可少: Nexus/Artifactory 是企业级实践的标配。
- 依赖树要常看:
mvn dependency:tree是理解依赖关系的瑞士军刀。 - 安全扫描要集成: 依赖漏洞扫描必须自动化。
- CI 是保障: 在持续集成流程中固化依赖管理实践(干净构建、依赖树、安全检查)。
遵循这些最佳实践,可以在分布式系统的复杂环境中,利用 Maven 构建出稳定、可重复、安全且易于维护的组件。核心在于通过 BOM 和集中化管理实现对众多服务依赖版本的强有力控制。

551

被折叠的 条评论
为什么被折叠?



