Maven 入门与依赖管理
项目管理与构建自动化 - 从零到实践
用 POM 驱动依赖、生命周期与交付引言
君子生非异也,善假于物也
—— 荀子
学习目标
- 理解 Maven 的定位与核心概念(POM、仓库、生命周期)
- 掌握依赖声明与解析流程,告别手动管理 JAR
- 配置本地/镜像仓库,加速国内构建体验
- 在 IDEA 中创建与配置 Maven 项目并实践依赖使用
完成本节后,你将能够:
- 用
pom.xml管理依赖与构建,确保可复现与一致性 - 熟练配置本地仓库与国内镜像,解决依赖下载缓慢问题
- 通过
mvn生命周期完成清理、编译、测试、打包的标准流程
学习建议:
- 边读边做:在 IDEA 中新建一个
quickstart项目并跟随示例操作 - 遇到依赖问题时,先检查
settings.xml镜像与本地仓库路径是否正确
旧开发痛点
- 手动管理 JAR:下载、拷贝、调整依赖关系,耗时且易错
- 环境不一致:不同环境依赖版本不同,构建结果不可复现
- 流程不统一:缺少统一的构建流程与测试执行,交付质量不稳定
这些问题直接导致:
- "在我电脑上能跑" - 经典的环境差异问题
- 项目依赖关系混乱,维护成本高
- 新人上手困难,缺乏标准化流程
什么是 Maven
Maven 名源意第绪语,意为"专家"。它是 Apache 基金会开源的项目管理与构建自动化工具。
核心理念:基于 POM(Project Object Model) 的声明式项目管理
三个核心功能:
- 依赖管理:在
pom.xml中声明库坐标,自动下载并解析传递依赖 - 构建生命周期:标准化
clean、compile、test、package、deploy - 插件机制:通过插件扩展构建能力(测试、打包、发布、站点生成)
一句话总结:Maven 同时处理三件事:依赖下载与解析、规范化项目结构与构建流程、按生命周期产出稳定的构建结果。
为什么选择 Maven
解决传统开发痛点:
- 统一依赖管理:通过声明式依赖与仓库统一版本,消除"环境差异"
- 标准化构建:规范的生命周期与插件体系,保证构建与测试自动化执行
- 中心化坐标:传递依赖解析,避免"缺 JAR、版本乱"的常见问题
带来的价值:
- 可复现性:相同的
pom.xml产生相同的构建结果 - 自动化:一键完成编译、测试、打包等所有构建步骤
- 标准化:项目结构统一,团队协作更高效
第一个 Maven 项目
在 IDEA 中创建项目:
- 选择 New Project → Maven
- 选择 maven-archetype-quickstart 原型(推荐)
- 填入 GroupId 和 ArtifactId
- 确认 Maven 配置(首次可使用 IDEA 自带版本)
示例坐标:
GroupId:cn.demo.learnArtifactId:learn-mavenVersion:1.0.0
项目结构解析
约定优于配置:
src/main/java- 业务源代码src/test/java- 测试源代码src/main/resources- 资源文件(配置、模板等)pom.xml- 项目对象模型,记录依赖、插件、构建信息
建议:遵循默认结构,便于工具识别与团队协作
POM 基础认识
<!-- POM文件的XML命名空间和Schema定义 -->
<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">
<!-- POM模型版本,固定为4.0.0,表示当前使用的POM版本 -->
<modelVersion>4.0.0</modelVersion>
<!-- 组织ID:通常是公司或组织的反向域名,确保全局唯一性 -->
<groupId>cn.demo.learn</groupId>
<!-- 项目ID:组织内项目的唯一标识符,通常使用项目名 -->
<artifactId>learn</artifactId>
<!-- 版本号:遵循语义化版本规范,SNAPSHOT表示开发版本 -->
<version>1.0.0</version>
<!-- 项目名称:更友好的项目显示名称 -->
<name>learn</name>
<!-- 项目描述:简要说明项目用途和功能 -->
<description>hello</description>
<!-- 属性定义:可在POM中引用的变量,便于统一管理配置 -->
<properties>
<!-- 指定源代码兼容的Java版本 -->
<maven.compiler.source>17</maven.compiler.source>
<!-- 指定编译生成的字节码目标版本 -->
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
GAV 坐标说明:
groupId- 组织ID(反向域名,如org.apache)artifactId- 项目ID(组内唯一)version- 版本(-SNAPSHOT表示开发版)
依赖解析机制
解析流程:
- 查找本地仓库(
~/.m2/repository) - 未命中时访问远程仓库(镜像/中央)
repo1.maven.org/maven2 - 下载并缓存到本地仓库
- 解析传递依赖,处理冲突
- 加入项目类路径
本地仓库详解
默认位置:
${user.home}\.m2\repository(如 C:\Users\你的用户名\.m2\repository)
作用:
- 缓存依赖以加速构建
- 支持离线构建模式
- 团队共享本地缓存
自定义配置:
在 settings.xml 中指定本地仓库路径:
<settings>
<localRepository>D:/software/apache-maven/repository</localRepository>
</settings>
建议:将仓库放在非系统盘,便于管理和备份
远程仓库与镜像配置
中央仓库:
- 官方地址:
https://repo.maven.apache.org/maven2/ - 权威且稳定,收录丰富的构件
国内镜像配置:
<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>Aliyun Maven</name>
<url>https://maven.aliyun.com/repository/central</url>
</mirror>
</mirrors>
作用:加速国内访问,提升下载速度和稳定性
settings.xml 完整配置
在 C:\Users\用户名\.m2\settings.xml 中配置:
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository>D:/software/apache-maven/repository</localRepository>
<mirrors>
<mirror>
<id>aliyun</id>
<mirrorOf>central</mirrorOf>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
</settings>
注意:如果 .m2 目录下没有 settings.xml,可直接创建该文件
依赖管理: 添加依赖
依赖(Dependency):项目直接在 <dependencies> 中声明的库。
这里我们尝试添加第一个依赖:Lombok。Lombok 是一个 Java 库,用于减少样板代码,如 getter、setter、toString 等。
步骤:
- 在 MVNRepository 搜索
lombok - 选择稳定版本,复制 Maven 坐标
- 在
pom.xml的<dependencies>标签内添加 - 保存后 IDEA 自动下载依赖
<!-- dependencies标签:定义项目的所有依赖,位于project标签内 -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<!-- dependencies通常放在properties之后,build之前 -->
<dependencies>
<!-- 单个依赖的定义 -->
<dependency>
<groupId>org.projectlombok</groupId> <!-- 依赖的组织ID -->
<artifactId>lombok</artifactId><!-- 依赖的项目ID -->
<version>1.18.22</version> <!-- 依赖的版本号 -->
<scope>provided</scope> <!-- 依赖作用域:provided表示编译时需要,运行时由容器提供 -->
</dependency>
</dependencies>
<build>
<!-- 构建配置 -->
</build>
</project>
注意:scope=provided 表示仅编译期需要,运行时由环境提供
依赖管理:验证生效
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Student {
String name;
int age;
}
public class Main {
public static void main(String[] args) {
Student student = new Student("小明", 18);
System.out.println(student);
}
}
预期输出:Student(name=小明, age=18)
如果看到正常输出,说明 Lombok 已成功生效!
注意:如果你的IDEA的安装路径或者项目路径中包含中文,那么可能会报错,建议将路径改为英文。
如果已经很难修改了,那么不妨尝试一下在设置中搜索"maven",然后增加参数 -Dfile.encoding=GBK
依赖作用域详解
作用域(scope)决定依赖在编译、测试与运行阶段的可见性:
实践建议:正确设置作用域能减少包冲突与最终包体积
| 作用域 | 编译期 | 测试期 | 运行期 | 典型依赖 | 设置原因/场景 |
|---|---|---|---|---|---|
compile |
✓ | ✓ | ✓ | 通用库(如核心框架) | 代码在所有阶段都需要 |
provided |
✓ | ✓ | 由环境提供 | lombok、Servlet API
|
仅编译/测试需要,运行期由容器/工具提供 |
runtime |
× | ✓ | ✓ | JDBC 驱动 | 编译面向接口,具体实现仅在运行/测试需要 |
test |
× | ✓ | × | JUnit、Mockito | 仅用于测试代码与运行器 |
system |
✓ | ✓ | 环境/本地提供 | 本地路径依赖 | 不可移植,不推荐 |
为什么这些库要用不同的 scope?
Lombok → provided
- 编译期通过注解处理器生成字节码;运行期不需要 Lombok 本身。
- 设置为
provided,避免打包进产物、减小体积且不引入无意义的运行期依赖。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
JDBC 驱动 → runtime
- 编译面向
java.sql接口;具体驱动实现仅在运行时加载。 - 设置为
runtime,不参与编译但在运行和测试阶段可用;若直接引用驱动类(如com.mysql.cj.jdbc.Driver),可改用compile。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
JUnit → test
- 仅用于测试代码与测试运行器;不应该出现在生产环境的可运行包中。
- 设置为
test,确保不会被传递到生产依赖树。
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
Servlet API → provided(参考)
- 运行期由容器(Tomcat/Jetty/Spring Boot)提供;项目不应打包它。
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
选择原则:编译期才需要 → 使用 provided;仅运行/测试期需要 → 使用 runtime;仅测试代码需要 →
使用 test;跨阶段都需要 → 使用 compile。
可选依赖(optional)
什么是可选依赖?
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
<optional>true</optional>
</dependency>
用途:不希望被传递给下游,适用于可插拔的组件。例如:一个库支持多种日志实现,但不想强制下游项目引入所有实现,可以把这些实现标记为
optional。
📚 深入理解 optional 依赖
核心思想:让下游项目自主选择需要的实现,而不是强制引入所有可能的实现。
🎯 典型场景:日志框架适配
假设你开发了一个通用工具库 my-common-utils,它需要记录日志,但你希望用户能自由选择使用哪种日志框架:
<!-- 在 my-common-utils 的 pom.xml 中 -->
<dependencies>
<!-- 日志门面 API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<!-- Log4j2 实现 - 标记为可选 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.20.0</version>
<optional>true</optional> <!-- 关键!不会被传递 -->
</dependency>
<!-- Logback 实现 - 也标记为可选 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
<optional>true</optional> <!-- 关键!不会被传递 -->
</dependency>
</dependencies>
🔧 下游项目的选择权
使用 my-common-utils 的项目现在可以自主选择日志实现:
<!-- 项目A选择使用 Log4j2 -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>my-common-utils</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 必须显式添加想要的日志实现 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.20.0</version>
</dependency>
</dependencies>
✅ optional 的优势
- 避免冲突:防止同时引入多个日志实现导致的类路径冲突
- 减少依赖:下游项目只引入真正需要的实现,保持项目轻量
- 提高灵活性:用户可以根据项目需求选择最合适的实现
- 版本控制:下游项目可以自主选择日志实现的版本
⚠️ 注意事项
- 文档说明:库作者必须在文档中明确说明有哪些可选实现以及如何选择
- 运行时要求:下游项目必须显式添加至少一个实现,否则运行时会报错
- 测试覆盖:库作者应该测试所有可选实现的兼容性
排除传递依赖(exclusions)
什么是传递依赖?
- 传递依赖(Transitive Dependency):你引入的库自身又依赖的其他库,Maven 会自动把这些间接依赖也下载并加入你的项目。例如:引入
spring-boot-starter-web会自动引入spring-web、spring-webmvc、spring-boot-starter-tomcat等。 - 依赖树:通过
mvn dependency:tree可以查看项目的完整依赖关系,包括直接依赖与传递依赖。
如何排除传递依赖?
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<exclusions>
<exclusion>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
用途:剔除冲突或不必要的传递依赖。例如:项目已引入 log4j2,但某个依赖传递引入了 logback,可通过
<exclusions> 排除 logback,避免日志实现冲突。
典型场景:排除传递依赖
<!-- 项目已使用 log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
<!-- 某工具库传递引入了 logback,需要排除 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>some-tool</artifactId>
<version>1.2.3</version>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>
效果:避免同时存在两套日志实现,防止类加载冲突与日志输出混乱。
💡 排除依赖的最佳实践
- 精确排除:只排除真正冲突的依赖,避免过度排除导致功能缺失
- 版本统一:使用
<dependencyManagement>统一管理版本,减少排除需求 - 依赖分析:定期使用
mvn dependency:tree分析依赖关系 - 文档记录:在项目文档中记录排除依赖的原因和替代方案
测试:Scope 掌握了吗?
Q1: 引入 JUnit 单元测试框架,应该使用哪个 scope?
Q2: 使用 Lombok 简化代码,避免打包进最终 jar,用哪个?
IDEA Maven 配置
打开 File → Settings → Build Tools → Maven
配置要点:
- Maven home path - Maven 安装目录
- User settings file - 用户配置文件
- Local repository - 本地仓库路径
建议:初学者可使用 IDEA 自带 Maven,熟悉后再配置独立版本
Maven 生命周期
Maven有三套相互独立的生命周期:clean、default和site。每套生命周期包含多个阶段,阶段按顺序执行,执行某个阶段时会先执行其前面的所有阶段。
1. Clean 生命周期
负责项目清理,包含三个阶段:
pre-clean- 执行清理前的工作,如释放资源clean- 清理上一次构建生成的文件,删除target目录post-clean- 执行清理后的工作,如日志记录
2. Default 生命周期
核心构建生命周期,包含主要阶段:
validate- 验证项目是否正确,检查pom.xml文件compile- 编译项目源代码,生成.class文件到target/classestest- 使用单元测试框架测试代码,如JUnit、TestNG等package- 将编译代码打包为可分发格式,生成JAR/WAR文件verify- 检查包是否有效且质量合格,运行集成测试install- 将包安装到本地仓库,供本地其他项目依赖deploy- 将最终包复制到远程仓库,供团队共享
3. Site 生命周期
负责项目文档生成和站点发布:
pre-site- 执行站点生成前的工作,准备文档资源site- 生成项目站点文档,创建HTML格式的项目报告post-site- 执行站点生成后的工作,处理文档元数据site-deploy- 将生成的站点部署到服务器,发布到Web
Maven 的生命周期是顺序执行的。点击下方阶段,体验执行流程:
清理
target/ 目录,删除上次构建产物
编译
src/main/java 到
target/classes
运行
src/test/java 下的单元测试用例
将编译结果打包为 JAR/WAR 文件
将包安装到本地仓库,供其他项目依赖
实战:Hutool 工具库
任务描述:
- 新建一个 Maven 项目
challenge-maven - 引入 Hutool 依赖(一个超好用的国产Java工具包)
- 编写代码打印当前时间
- 执行打包命令
💡 提示:Hutool 的坐标是 cn.hutool:hutool-all:5.8.16
// 参考代码
import cn.hutool.core.date.DateUtil;
public class App {
public static void main(String[] args) {
// 使用 Hutool 打印当前时间
String now = DateUtil.now();
System.out.println("Current Time: " + now);
}
}
// 验证命令
mvn clean package
java -cp target/classes App
* Maven 核心原理
1. 本质:骨架与血肉
Maven 的核心只是一个空的容器。 所有的实际工作全都是由插件(Plugin)完成的。
- 生命周期 (Lifecycle) = 流程图/接口
它只定义了“编译、测试、打包”的顺序,全是抽象方法,不包含实现逻辑。 - 插件 (Plugin) = 实现类/具体的工人
它是具体的。比如maven-compiler-plugin才是真正调用javac的人。
2. 面向对象视角:Plugin 与 Goal
作为 Java 开发者,你们可以这样理解 Maven 的设计:
|
// 伪代码演示 CompilerPlugin plugin = new CompilerPlugin(); plugin.compile(); // 对应 goal: compile plugin.testCompile(); // 对应 goal: testCompile |
结论:Maven 的运行,本质上就是在特定的生命周期阶段,调用特定“类”里的特定“方法”。
3. 既然是空的,为什么我没配插件也能跑?
这是因为 Maven 引入了 "Convention over Configuration" (约定优于配置) 的原则。
通过 POM.xml 中的 <packaging> 标签 (默认值为 jar),Maven 隐式地为你绑定了一套标准插件。
以最常见的 <packaging>jar</packaging> 为例:
| 阶段 (Phase) | 绑定的插件目标 (Plugin:Goal) | Java 类比理解 |
|---|---|---|
| compile | maven-compiler-plugin:compile |
Compiler.compile(src)
|
| test | maven-surefire-plugin:test |
JunitRunner.run(tests)
|
| package | maven-jar-plugin:jar |
JarArchiver.pack(classes)
|
* 这就是为什么你不需要写任何配置就能把代码打包成 JAR。
* 自定义插件 (1/3): 项目配置
Maven 插件本质上是一个 Jar 包,包含一个或多个可执行的目标(Goal),称为 Mojo (Maven Old Java Object)。
第一步:创建 Maven 项目并配置 POM
创建一个普通的 Maven 项目,修改 pom.xml 引入必要的依赖和打包方式。
<groupId>com.example</groupId>
<artifactId>hello-maven-plugin</artifactId>
<version>1.0.0</version>
<!-- 1. 关键:设置打包方式为 maven-plugin -->
<packaging>maven-plugin</packaging>
<dependencies>
<!-- 2. 引入 Maven 插件 API (核心接口) -->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.8.6</version>
<scope>provided</scope>
</dependency>
<!-- 3. 引入注解支持 (用于 @Mojo, @Parameter 等) -->
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.6.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
* 自定义插件 (2/3): 编写 Mojo
Mojo 是插件的具体执行逻辑。你需要继承 AbstractMojo 并实现 execute 方法。
package com.example;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
// 1. 定义 Goal 的名称,将来使用 mvn plugin:sayhi 调用
@Mojo(name = "sayhi")
public class HelloMojo extends AbstractMojo {
// 2. 定义参数,支持从命令行通过 -Dmsg=xxx 注入
// defaultValue 也可以绑定到 POM 中的属性
@Parameter(property = "msg", defaultValue = "World")
private String msg;
// 3. 核心执行逻辑
public void execute() throws MojoExecutionException {
// 使用 getLog() 获取 Maven 的日志记录器
getLog().info("Hello, " + msg + "!");
}
}
💡 代码解析
- @Mojo: 标记这是一个 Mojo 类,
name属性定义了 Goal 的名字。 - @Parameter: 标记一个字段为可配置参数。
property: 允许通过命令行-D传参。defaultValue: 参数默认值。
- getLog(): 类似于 `System.out`,但更规范,支持 info/warn/error 级别。
* 自定义插件 (3/3): 安装与运行
第三步:安装插件
在插件项目根目录下运行,将其安装到本地仓库:
mvn clean install
方式 A:命令行直接运行
格式:groupId:artifactId:version:goal
# 使用默认参数
mvn com.example:hello-maven-plugin:1.0.0:sayhi
# 传递参数
mvn com.example:hello-maven-plugin:1.0.0:sayhi -Dmsg=Maven
方式 B:在其他项目中使用
<plugin>
<groupId>com.example</groupId>
<artifactId>hello-maven-plugin</artifactId>
<version>1.0.0</version>
<configuration>
<msg>Developers</msg>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals><goal>sayhi</goal></goals>
</execution>
</executions>
</plugin>
最佳实践总结
项目组织:
- 遵循标准目录结构,不要随意修改
- 使用有意义的
groupId和artifactId - 版本号遵循语义化版本规范
依赖管理:
- 优先使用稳定版本,避免
alpha/beta - 合理设置
scope,减少不必要的依赖 - 使用
dependencyManagement统一版本管理
构建优化:
- 配置国内镜像,提升下载速度
- 本地仓库放在非系统盘
- 定期清理不需要的依赖
团队协作:
- 统一团队的开发环境配置
- 将
settings.xml纳入版本控制管理 - 建立规范的构建和发布流程