第二章:Maven实战:多模块项目搭建、依赖冲突解决与多环境配置
好,咱们直接进入正题。Maven 这东西,很多同学觉得就是个「包下载工具」。其实不然。你想想看,一个真正能上线的企业级项目,少说十几个模块,依赖关系错综复杂,环境配置五花八门。这时候,Maven 的实战能力才真正体现出来。
我个人习惯,把 Maven 的实战能力拆成三块:多模块怎么搭、依赖冲突怎么治、多环境怎么配。今天咱们一个一个啃。
2.1 多模块项目搭建:从单打独斗到团队协作
我记得刚带团队那会儿,有个同事把所有代码都塞在一个模块里。编译一次十分钟,改个接口还得全量打包。嗯,这就是典型的「单体地狱」。
多模块项目的核心思想很简单:分而治之。一个父工程(pom 打包方式)管理一群子模块,子模块之间可以互相依赖。
2.1.1 父工程的创建
父工程不写代码,只做两件事:统一版本、声明子模块。
<!-- parent/pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.blueocean</groupId>
<artifactId>my-project-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging> <!-- 关键:pom 打包 -->
<modules>
<module>my-common</module>
<module>my-service</module>
<module>my-web</module>
</modules>
<!-- 统一版本管理 -->
<properties>
<spring.version>5.3.20</spring.version>
<jackson.version>2.13.3</jackson.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
dependencyManagement,不放 dependencies。子模块按需引入,版本由父工程统一锁定。这样既灵活又可控。
2.1.2 子模块的配置
子模块只需要声明自己的 parent 是谁,以及自己独有的依赖。
<!-- my-service/pom.xml -->
<project>
<parent>
<groupId>com.blueocean</groupId>
<artifactId>my-project-parent</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>my-service</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.blueocean</groupId>
<artifactId>my-common</artifactId>
<version>${project.version}</version> <!-- 兄弟模块依赖 -->
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<!-- 版本由父工程管理,这里不用写 -->
</dependency>
</dependencies>
</project>
这里有个坑:relativePath 默认是 ../pom.xml,如果你的目录结构不是标准父子关系,一定要显式指定。我曾经因为忘记写这个,导致 CI 构建时找不到父 POM,排查了半天。
2.2 依赖冲突解决:三招制敌
依赖冲突,说白了就是「同一个 jar 包,不同版本,Maven 该听谁的?」。我见过最夸张的一次,项目里同时引入了 3 个不同版本的 Guava,启动时直接 NoSuchMethodError。
Maven 解决冲突有三大原则,咱们一个一个说。
2.2.1 第一声明优先原则
这是 Maven 的默认行为。在依赖树中,谁先声明,谁说了算。
<dependencies>
<!-- 先声明的 A 依赖了 logback 1.2.3 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>module-a</artifactId>
<version>1.0</version>
</dependency>
<!-- 后声明的 B 依赖了 logback 1.3.0 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>module-b</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
<!-- 最终生效的是 logback 1.2.3 -->
2.2.2 依赖排除(Exclusions)
这是我最常用的方式。当你明确知道某个传递依赖有问题,直接把它踢出去。
<dependency>
<groupId>com.example</groupId>
<artifactId>module-a</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
我曾经在一个项目中,因为 Spring 和 Apache HttpClient 都依赖了 commons-logging,导致日志冲突。排除掉一个后,世界清净了。
2.2.3 版本锁定(Dependency Management)
这是最优雅的方式。在父工程的 dependencyManagement 里,显式声明你想要的版本。这样不管传递依赖怎么折腾,最终版本都由你说了算。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
</dependencies>
</dependencyManagement>
dependencyManagement 里锁定。子模块只写 groupId 和 artifactId。这样版本一目了然,冲突自然消失。
2.3 Profile 多环境配置:一套代码,到处运行
开发、测试、生产,环境不同,配置不同。数据库地址、日志级别、第三方接口地址……总不能每次都手动改吧?
Profile 就是干这个的。它允许你在构建时,根据激活的环境,替换不同的配置。
2.3.1 定义 Profile
<profiles>
<!-- 开发环境 -->
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<db.url>jdbc:mysql://localhost:3306/dev_db</db.url>
<log.level>DEBUG</log.level>
</properties>
</profile>
<!-- 生产环境 -->
<profile>
<id>prod</id>
<properties>
<db.url>jdbc:mysql://prod-server:3306/prod_db</db.url>
<log.level>WARN</log.level>
</properties>
</profile>
</profiles>
2.3.2 使用资源过滤
配合 Maven 的资源过滤功能,让配置文件里的占位符被替换成真实值。
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering> <!-- 开启过滤 -->
</resource>
</resources>
</build>
然后在 application.properties 里写占位符:
db.url=${db.url}
log.level=${log.level}
2.3.3 激活 Profile
构建时通过 -P 参数指定:
# 开发环境
mvn clean package -Pdev
# 生产环境
mvn clean package -Pprod
settings.xml 里的 <servers> 或者外部环境变量来注入。安全第一。
2.4 避坑指南:我曾经踩过的坑
- 依赖冲突排查:用
mvn dependency:tree命令,能看到完整的依赖树。哪个版本被覆盖了,一目了然。 - Profile 不生效:检查一下
activeByDefault是不是设成了true。如果多个 Profile 同时激活,后声明的会覆盖先声明的。 - 子模块版本不一致:所有子模块的版本号,最好统一用
${project.version}引用父工程的版本。否则升级时容易漏掉。
好了,这一章的内容就这些。多模块、依赖冲突、多环境配置,这三板斧用好了,你的 Maven 实战能力基本就到位了。下一章咱们聊聊 Gradle,看看它和 Maven 到底有什么不同。