Skip to content

Maven ​

Maven 是 Java 工程里最常见的构建与依赖管理工具。它不是单纯的“下载 jar 包工具”,而是把项目坐标、依赖版本、源码目录、测试、打包、发布和插件执行统一放进一套可重复的工程模型里。一个项目只要给出 pom.xml,团队成员、CI 服务器和发布流水线就能用同一套命令得到一致的构建结果。

Maven 的核心价值可以概括为四件事:用 pom.xml 描述项目,用仓库保存构件,用生命周期组织构建步骤,用插件执行具体工作。理解这四件事之后,mvn clean package、dependencyManagement、scope、多模块聚合、私服发布和依赖冲突排查就会落到同一条主线上。

视频讲解 ​

配套视频系列:Maven 深入教程:从 POM 到依赖治理与发布部署。

视频更适合先建立整体图景,文章更适合回查命令、配置和排错细节。建议先按视频顺序理解 Maven 的构建链路,再回到本文对照 pom.xml、依赖树、生命周期、插件绑定和多模块构建。

分P主题对应文章重点
P01Maven 到底在管理什么项目模型、坐标、仓库、生命周期和插件的总览
P02POM 与 effective POMPOM 结构、父 POM、继承、最终生效配置
P03生命周期、phase 与 plugin生命周期阶段如何触发插件目标
P04依赖解析、传递依赖与冲突仓库解析、传递依赖、版本冲突处理
P05scope 与 classpath 边界编译、测试、运行、打包时的依赖可见性
P06仓库、本地缓存与 SNAPSHOT本地仓库、远程仓库、私服、快照版本
P07BOM、parent 与版本治理团队级版本收敛和依赖管理
P08多模块与 Reactor 构建聚合、继承、模块顺序和局部构建
P09Profile、资源过滤与环境差异构建参数和环境差异的边界
P10测试、质量检查与构建门禁单元测试、验证阶段和质量约束
P11发布、部署与制品管理install、deploy、私服和制品生命周期
P12Maven 排查方法论effective POM、依赖树、插件日志和常见故障

Maven 在 Java 工程中的位置

解决的问题 ​

没有 Maven 时,一个 Java 项目通常需要手工完成这些事情:下载第三方 jar 包、维护编译命令、区分测试代码和业务代码、把资源文件复制到输出目录、运行测试、组装最终 jar 或 war、把公共模块交给其他项目使用。项目越大,越容易出现“本地能跑、CI 不能跑”“A 同学下载了新版本依赖、B 同学还是旧版本”“公共模块复制来复制去”的问题。

Maven 把这些不稳定的手工动作收敛成约定和配置。项目采用固定目录结构,依赖通过坐标声明,构建过程按生命周期推进,真正的编译、测试、打包由插件完成。开发者不需要记住一长串 javac、jar、复制资源的命令,只要维护项目模型,再用目标阶段触发构建。

典型工程里,Maven 负责以下边界:

问题Maven 的处理方式
项目叫什么、产物是什么使用 groupId、artifactId、version、packaging 定义项目坐标
第三方库从哪里来从本地仓库、私服仓库、中央仓库解析依赖
编译、测试、打包如何执行生命周期阶段绑定插件目标
多个模块如何一起构建父 POM 管版本,聚合 POM 管构建顺序
版本冲突如何定位用依赖树、有效 POM 和插件日志还原最终结果

从 0 建立项目 ​

一个最小 Maven 项目只需要一个 pom.xml 和标准源码目录:

text
demo-order/
├── pom.xml
└── src/
    ├── main/
    │   ├── java/
    │   │   └── com/example/order/OrderApplication.java
    │   └── resources/
    │       └── application.yml
    └── test/
        └── java/
            └── com/example/order/OrderApplicationTest.java

src/main/java 放业务代码,src/main/resources 放运行时资源,src/test/java 放测试代码,target/ 是构建输出目录。这个目录结构本身就是约定,Maven 插件会默认从这些位置读取源码、资源和测试文件。

可以用 Archetype 创建普通 Java 项目:

bash
mvn archetype:generate \
  -DgroupId=com.example \
  -DartifactId=demo-order \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DinteractiveMode=false

Spring Boot 项目更常见的入口是 Spring Initializr。它会直接生成 Maven 结构、启动类、测试类和适配 Spring Boot 的 pom.xml。无论从哪个入口创建,后续判断项目是否是 Maven 工程,只看根目录是否有 pom.xml。

安装与验证 ​

Maven 运行依赖 JDK。安装前先确认 Java 可用:

bash
java -version
javac -version

常见安装方式:

bash
# macOS
brew install maven

# SDKMAN
sdk install maven

# Ubuntu / Debian
sudo apt update
sudo apt install maven

验证安装:

bash
mvn -version

输出中需要关注三类信息:Maven 版本、Java 版本、用户主目录。用户主目录会影响默认本地仓库路径,一般是 ~/.m2/repository。

POM 的核心结构 ​

pom.xml 是 Maven 的项目对象模型。它不是脚本,而是项目声明:这个项目是谁、依赖谁、如何构建、产物如何发布。

xml
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo-order</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <junit.version>5.10.2</junit.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>
</project>

坐标由 groupId、artifactId、version 组成。groupId 通常表示组织或业务域,artifactId 表示模块名,version 表示产物版本。其他项目引用当前模块时,本质上也是引用这个坐标。

packaging 决定产物形态。普通库和服务常用 jar,传统 Servlet 应用可能使用 war,父工程或版本管理工程常用 pom。当 packaging 不同时,Maven 默认绑定的插件目标也会不同。

settings 与 pom 的边界 ​

pom.xml 描述项目本身,应提交到代码仓库。settings.xml 描述当前开发者或构建环境的私有配置,一般放在 ~/.m2/settings.xml,不应提交到业务代码仓库。

文件作用典型内容
pom.xml项目模型项目坐标、依赖、插件、模块、发布地址
settings.xml本机或 CI 环境配置本地仓库路径、镜像、私服账号、激活 profile

国内网络或企业内网环境通常会配置镜像和私服认证:

xml
<settings>
    <mirrors>
        <mirror>
            <id>company-public</id>
            <mirrorOf>*</mirrorOf>
            <url>https://maven.example.com/repository/public/</url>
        </mirror>
    </mirrors>

    <servers>
        <server>
            <id>company-releases</id>
            <username>${env.MAVEN_REPO_USER}</username>
            <password>${env.MAVEN_REPO_PASSWORD}</password>
        </server>
    </servers>
</settings>

server.id 要和 pom.xml 中 distributionManagement 或仓库配置里的 id 对上。这样项目里只保存发布地址,账号密码留在开发机或 CI 密钥里。

仓库与依赖解析 ​

Maven 仓库保存的是构件,常见文件包括 .pom、.jar、源码包、Javadoc 包和校验文件。依赖解析时,Maven 先看本地仓库;本地没有,再按远程仓库或镜像配置下载;下载成功后缓存到本地仓库,后续构建直接复用。

Maven 依赖解析链路

依赖声明只写直接依赖,但实际进入项目 classpath 的还包括传递依赖。例如订单服务直接依赖 spring-web,spring-web 又依赖 spring-core、spring-beans,这些间接依赖也会被解析进来。传递依赖提高了复用效率,也带来了版本冲突。

bash
# 查看依赖树
mvn dependency:tree

# 只看某个 groupId 或 artifactId
mvn dependency:tree -Dincludes=org.springframework

# 分析声明但未使用、使用但未声明的依赖
mvn dependency:analyze

依赖冲突时,Maven 主要按“路径最近优先”选择版本。路径长度相同,再按声明顺序选择先出现的依赖。工程中更稳定的做法不是依赖这个隐式结果,而是在 dependencyManagement 中统一声明版本。

xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.2.6</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

dependencyManagement 只管理版本,不会自动引入依赖。真正使用某个库,仍然要在 dependencies 里声明。

scope 与 classpath ​

scope 决定依赖在哪些阶段可见。判断 scope 时不要只问“会不会下载”,而要问“编译、测试、运行、打包时是否进入 classpath”。

scope编译测试运行典型场景
compile是是是业务代码直接使用的库,默认值
provided是是否Servlet API、Lombok、由运行容器提供的库
runtime否是是JDBC 驱动、运行时实现
test否是否JUnit、Mockito、测试工具
import否否否在 dependencyManagement 中导入 BOM
system是是取决于手工路径绑定本机文件,不适合团队项目

常见误区是把 Lombok、Servlet API 这类编译期需要但运行期不应打进包的依赖写成 compile,导致最终产物混入不需要的库。另一个误区是把 JDBC 驱动写成 provided,本地编译没问题,运行时连接数据库才报 ClassNotFoundException。

生命周期与插件 ​

Maven 命令不是直接调用“编译器”或“打包器”,而是请求执行某个生命周期阶段。执行 mvn package 时,Maven 会从 validate 一路执行到 package,中间包括资源处理、编译、测试编译、测试和打包。

Maven 生命周期与插件执行

常用阶段:

命令会做什么适用场景
mvn validate检查项目模型是否完整快速验证 POM 是否能被解析
mvn compile处理资源并编译主代码本地确认语法和主依赖
mvn test编译并执行单元测试提交前验证
mvn package测试后生成 jar 或 war本地打包、容器镜像构建前
mvn verify运行更多校验集成测试、质量门禁
mvn install安装产物到本地仓库本地联调多项目
mvn deploy发布产物到远程仓库CI 发布公共库

插件是生命周期真正做事的执行者。maven-compiler-plugin 负责编译,maven-surefire-plugin 负责单元测试,maven-jar-plugin 负责普通 jar,maven-war-plugin 负责 war,maven-deploy-plugin 负责发布。生命周期提供顺序,插件提供能力。

常见编译配置:

xml
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.13.0</version>
            <configuration>
                <release>17</release>
                <parameters>true</parameters>
            </configuration>
        </plugin>
    </plugins>
</build>

release 比同时写 source 和 target 更适合控制 JDK API 兼容性。项目统一 JDK 版本时,建议把版本放到 properties 中,再由插件引用。

常用命令 ​

本地开发时,先掌握少量高频命令即可:

bash
# 清理输出目录
mvn clean

# 编译主代码
mvn compile

# 运行测试
mvn test

# 打包,默认会执行测试
mvn clean package

# 跳过测试执行,但仍编译测试代码
mvn clean package -DskipTests

# 跳过测试编译和测试执行
mvn clean package -Dmaven.test.skip=true

# 将当前模块安装到本地仓库,供其他本地项目引用
mvn clean install

# 查看最终生效的 POM
mvn help:effective-pom

# 查看当前项目最终解析出的属性
mvn help:effective-settings

跳过测试要谨慎区分。-DskipTests 只跳过测试运行,测试代码仍会编译;-Dmaven.test.skip=true 连测试代码编译也跳过,更容易掩盖测试依赖和测试源码问题。

多模块项目 ​

当订单、支付、库存、通知等模块需要在一个仓库里协作时,单个 Maven 项目会演变成多模块项目。多模块项目通常有一个根 POM,负责聚合子模块、统一版本和插件配置。

Maven 多模块组织方式

目录示例:

text
shop-platform/
├── pom.xml
├── shop-common/
│   └── pom.xml
├── order-service/
│   └── pom.xml
├── payment-service/
│   └── pom.xml
└── inventory-service/
    └── pom.xml

根 POM:

xml
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example.shop</groupId>
    <artifactId>shop-platform</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>shop-common</module>
        <module>order-service</module>
        <module>payment-service</module>
        <module>inventory-service</module>
    </modules>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.example.shop</groupId>
                <artifactId>shop-common</artifactId>
                <version>${project.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

子模块通过 parent 继承公共配置:

xml
<parent>
    <groupId>com.example.shop</groupId>
    <artifactId>shop-platform</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</parent>

<artifactId>order-service</artifactId>

<dependencies>
    <dependency>
        <groupId>com.example.shop</groupId>
        <artifactId>shop-common</artifactId>
    </dependency>
</dependencies>

多模块有两个容易混淆的概念:继承和聚合。parent 是继承关系,子模块继承父 POM 的属性、依赖管理和插件管理;modules 是聚合关系,根项目一次构建多个模块,并按模块依赖关系决定构建顺序。两者常常写在同一个根 POM 中,但含义不同。

局部构建常用命令:

bash
# 只构建 order-service 以及它依赖的模块
mvn clean install -pl order-service -am

# 从某个模块开始继续构建
mvn clean install -rf :order-service

# 构建多个模块
mvn clean install -pl order-service,payment-service -am

-pl 选择模块,-am 同时构建所需依赖模块,-rf 用于失败后从指定模块恢复构建。

版本管理与 BOM ​

企业项目里最容易失控的是版本。一个服务直接写几十个依赖版本,多个服务各写一套版本,很快就会出现 Spring、Jackson、Netty、日志框架版本不一致的问题。

稳定做法是把版本集中到父 POM 或 BOM:

xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.example.platform</groupId>
            <artifactId>platform-dependencies</artifactId>
            <version>1.4.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

BOM 的作用是给一组依赖提供统一版本表。业务模块只声明要用什么,不重复声明版本:

xml
<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

对于业务平台,建议分三层管理版本:语言和构建插件版本放父 POM,通用三方库版本放 BOM,业务模块只声明真实使用的依赖。这样升级 Spring Boot 或日志组件时,改动集中,回滚也清晰。

Profile 与环境差异 ​

Profile 用于切换少量构建差异,例如不同仓库、不同打包参数、不同资源过滤配置。它不适合承载大量业务环境逻辑。数据库地址、密钥、运行时开关更适合交给应用配置中心、环境变量或部署平台。

xml
<profiles>
    <profile>
        <id>dev</id>
        <properties>
            <skip.integration.tests>true</skip.integration.tests>
        </properties>
    </profile>
    <profile>
        <id>release</id>
        <properties>
            <skip.integration.tests>false</skip.integration.tests>
        </properties>
    </profile>
</profiles>

使用方式:

bash
mvn clean verify -Pdev
mvn clean deploy -Prelease

Profile 越多,构建结果越难判断。一个项目如果需要通过 Profile 切换大量源码、依赖和插件,通常说明模块边界或部署模型需要重新整理。

私服与发布 ​

公共三方依赖一般来自 Maven Central 或企业镜像,内部公共库应发布到企业私服。私服的价值不只是加速下载,还包括统一缓存、权限控制、制品留存、漏洞治理和离线构建。

发布配置通常放在项目 POM:

xml
<distributionManagement>
    <repository>
        <id>company-releases</id>
        <url>https://maven.example.com/repository/releases/</url>
    </repository>
    <snapshotRepository>
        <id>company-snapshots</id>
        <url>https://maven.example.com/repository/snapshots/</url>
    </snapshotRepository>
</distributionManagement>

账号密码放在 settings.xml 的 servers 中。执行发布:

bash
mvn clean deploy

SNAPSHOT 版本表示开发中的可变版本,适合联调;正式版本不应重复覆盖。公共库发布前应确保源码包、Javadoc 包、测试和版本号策略清晰,否则下游项目很难追踪问题。

Spring Boot 项目中的 Maven ​

Spring Boot Maven 项目通常通过父 POM 或 BOM 继承大量版本管理能力:

xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.6</version>
    <relativePath/>
</parent>

使用 Spring Boot 父 POM 后,常见 starter 不需要重复写版本:

xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

可执行 jar 由 Spring Boot 插件重新打包:

xml
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

普通 maven-jar-plugin 打出的 jar 通常只包含项目自己的 class,不能直接运行完整 Spring Boot 应用;spring-boot-maven-plugin 会把依赖和启动器结构一起组织进可执行包。

常见排错路径 ​

Maven 问题不要只看最后一行错误。有效排查顺序是:先确认命令阶段,再看项目模型,再看依赖树,再看插件配置,最后定位仓库或网络。

Maven 排错路径

依赖下载失败 ​

先判断是坐标不存在、仓库不可访问,还是认证失败:

bash
mvn -U dependency:resolve
mvn help:effective-settings

-U 会强制检查远程更新。若私服需要账号,检查 settings.xml 中 server.id 是否和仓库 id 一致。若本地仓库缓存了失败结果,可以删除对应目录后重新解析。

版本冲突或运行时报错 ​

编译能过但运行时报 NoSuchMethodError、ClassNotFoundException、ClassCastException,常见原因是实际进入运行 classpath 的版本和预期不一致。

bash
mvn dependency:tree -Dverbose
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core

定位后优先在 dependencyManagement 统一版本,必要时用 exclusions 排除不该传递进来的依赖:

xml
<dependency>
    <groupId>com.example</groupId>
    <artifactId>legacy-client</artifactId>
    <version>1.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

插件版本或编译 JDK 不一致 ​

本地能编译,CI 报 JDK API 不存在,通常是 JDK 版本或编译插件配置不一致。先看:

bash
mvn -version
mvn help:effective-pom

项目应明确编译目标,例如使用 maven.compiler.release。团队项目还可以结合 Maven Wrapper、CI 镜像和工具链配置固定构建环境。

测试跳过后仍然失败 ​

如果使用 -DskipTests 后仍然失败,说明失败可能发生在测试编译阶段。测试源码依赖缺失、生成测试代码失败、测试资源过滤失败都会在这个阶段暴露。只有 -Dmaven.test.skip=true 才会跳过测试编译,但这不适合作为长期构建参数。

多模块找不到内部依赖 ​

本地只进入某个子目录执行构建时,Maven 可能找不到兄弟模块的最新产物。优先回到根目录执行:

bash
mvn clean install -pl order-service -am

如果仍然失败,检查子模块 parent 坐标、根 POM modules 声明、内部依赖版本是否和 ${project.version} 保持一致。

选型与使用边界 ​

Maven 适合约定清晰、依赖管理稳定、CI 发布流程标准化的 Java 项目。企业后端服务、公共 SDK、多模块平台、Spring Boot 应用都很适合使用 Maven。它的优势是模型稳定、生态成熟、团队协作成本低。

Gradle 更适合构建逻辑高度定制、需要更强增量构建和脚本表达能力的项目,例如大型 Android 工程或复杂多语言构建。普通 Java 后端项目如果没有明显的构建性能或脚本扩展需求,Maven 的可读性和统一性通常更容易维护。

能力验证清单 ​

学完 Maven 后,应能独立完成这些事情:

  • 从空目录创建一个 Maven Java 项目,并解释标准目录结构的作用。
  • 读懂 pom.xml 中项目坐标、依赖、插件、父 POM、模块声明的含义。
  • 使用 mvn clean package、mvn test、mvn install 完成本地构建和联调。
  • 通过 dependency:tree 找到版本冲突来源,并用 dependencyManagement 或 exclusions 收敛依赖。
  • 区分 pom.xml 和 settings.xml,正确配置镜像、私服账号和发布仓库。
  • 维护一个多模块项目,知道 parent 继承和 modules 聚合的区别。
  • 按错误类型选择排查入口,而不是反复删除整个本地仓库。

总结 ​

Maven 的学习重点不是记住所有标签,而是建立工程模型。pom.xml 定义项目,仓库提供构件,生命周期安排顺序,插件执行动作,依赖树决定最终 classpath。沿着这条线看,一个 Maven 项目从 0 到发布就是:声明坐标和依赖,按标准目录组织代码,执行生命周期阶段,生成构件,必要时发布到仓库,再用依赖树和有效 POM 排查偏差。

别急,先让缓存热一下。