第二章: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 -->
注意:这个规则只对同一层级的依赖有效。如果 A 和 B 是传递依赖,那规则就变了。所以别太依赖这个,容易出问题。

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 里锁定。子模块只写 groupIdartifactId。这样版本一目了然,冲突自然消失。

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
我的经验:生产环境的密码等敏感信息,不要直接写在 POM 里。我一般配合 settings.xml 里的 <servers> 或者外部环境变量来注入。安全第一。

2.4 避坑指南:我曾经踩过的坑

  • 依赖冲突排查:mvn dependency:tree 命令,能看到完整的依赖树。哪个版本被覆盖了,一目了然。
  • Profile 不生效:检查一下 activeByDefault 是不是设成了 true。如果多个 Profile 同时激活,后声明的会覆盖先声明的。
  • 子模块版本不一致:所有子模块的版本号,最好统一用 ${project.version} 引用父工程的版本。否则升级时容易漏掉。

好了,这一章的内容就这些。多模块、依赖冲突、多环境配置,这三板斧用好了,你的 Maven 实战能力基本就到位了。下一章咱们聊聊 Gradle,看看它和 Maven 到底有什么不同。