Maven 入门与依赖管理

项目管理与构建自动化 - 从零到实践

用 POM 驱动依赖、生命周期与交付

引言

君子生非异也,善假于物也

—— 荀子

学习目标

  • 理解 Maven 的定位与核心概念(POM、仓库、生命周期)
  • 掌握依赖声明与解析流程,告别手动管理 JAR
  • 配置本地/镜像仓库,加速国内构建体验
  • 在 IDEA 中创建与配置 Maven 项目并实践依赖使用

完成本节后,你将能够:

  • pom.xml 管理依赖与构建,确保可复现与一致性
  • 熟练配置本地仓库与国内镜像,解决依赖下载缓慢问题
  • 通过 mvn 生命周期完成清理、编译、测试、打包的标准流程

学习建议:

  • 边读边做:在 IDEA 中新建一个 quickstart 项目并跟随示例操作
  • 遇到依赖问题时,先检查 settings.xml 镜像与本地仓库路径是否正确

旧开发痛点

旧开发痛点
  • 手动管理 JAR:下载、拷贝、调整依赖关系,耗时且易错
  • 环境不一致:不同环境依赖版本不同,构建结果不可复现
  • 流程不统一:缺少统一的构建流程与测试执行,交付质量不稳定

这些问题直接导致:

  • "在我电脑上能跑" - 经典的环境差异问题
  • 项目依赖关系混乱,维护成本高
  • 新人上手困难,缺乏标准化流程

什么是 Maven

Maven 核心功能

Maven 名源意第绪语,意为"专家"。它是 Apache 基金会开源的项目管理与构建自动化工具

核心理念:基于 POM(Project Object Model) 的声明式项目管理

三个核心功能:

  • 依赖管理:pom.xml 中声明库坐标,自动下载并解析传递依赖
  • 构建生命周期:标准化 cleancompiletestpackagedeploy
  • 插件机制:通过插件扩展构建能力(测试、打包、发布、站点生成)

一句话总结:Maven 同时处理三件事:依赖下载与解析、规范化项目结构与构建流程、按生命周期产出稳定的构建结果。

为什么选择 Maven

Maven 核心价值

解决传统开发痛点:

  • 统一依赖管理:通过声明式依赖与仓库统一版本,消除"环境差异"
  • 标准化构建:规范的生命周期与插件体系,保证构建与测试自动化执行
  • 中心化坐标:传递依赖解析,避免"缺 JAR、版本乱"的常见问题

带来的价值:

  • 可复现性:相同的 pom.xml 产生相同的构建结果
  • 自动化:一键完成编译、测试、打包等所有构建步骤
  • 标准化:项目结构统一,团队协作更高效

第一个 Maven 项目

在 IDEA 中创建项目:

确认 pom.xml 存在
  1. 选择 New Project → Maven
  2. 选择 maven-archetype-quickstart 原型(推荐)
  3. 填入 GroupIdArtifactId
  4. 确认 Maven 配置(首次可使用 IDEA 自带版本)

示例坐标:

  • GroupId: cn.demo.learn
  • ArtifactId: learn-maven
  • Version: 1.0.0

项目结构解析

Maven 项目结构

约定优于配置:

  • 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 表示开发版)

依赖解析机制

Maven 仓库概念图

解析流程:

  1. 查找本地仓库(~/.m2/repository
  2. 未命中时访问远程仓库(镜像/中央)repo1.maven.org/maven2
  3. 下载并缓存到本地仓库
  4. 解析传递依赖,处理冲突
  5. 加入项目类路径

本地仓库详解

默认位置:

${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 等。

步骤:

  1. MVNRepository 搜索 lombok
  2. 选择稳定版本,复制 Maven 坐标
  3. pom.xml<dependencies> 标签内添加
  4. 保存后 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

Maven 中文路径报错

依赖作用域详解

作用域(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-webspring-webmvcspring-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有三套相互独立的生命周期:cleandefaultsite。每套生命周期包含多个阶段,阶段按顺序执行,执行某个阶段时会先执行其前面的所有阶段。

1. Clean 生命周期

负责项目清理,包含三个阶段:

  • pre-clean - 执行清理前的工作,如释放资源
  • clean - 清理上一次构建生成的文件,删除target目录
  • post-clean - 执行清理后的工作,如日志记录

2. Default 生命周期

核心构建生命周期,包含主要阶段:

  • validate - 验证项目是否正确,检查pom.xml文件
  • compile - 编译项目源代码,生成.class文件到target/classes
  • test - 使用单元测试框架测试代码,如JUnit、TestNG等
  • package - 将编译代码打包为可分发格式,生成JAR/WAR文件
  • verify - 检查包是否有效且质量合格,运行集成测试
  • install - 将包安装到本地仓库,供本地其他项目依赖
  • deploy - 将最终包复制到远程仓库,供团队共享

3. Site 生命周期

负责项目文档生成和站点发布:

  • pre-site - 执行站点生成前的工作,准备文档资源
  • site - 生成项目站点文档,创建HTML格式的项目报告
  • post-site - 执行站点生成后的工作,处理文档元数据
  • site-deploy - 将生成的站点部署到服务器,发布到Web

Maven 的生命周期是顺序执行的。点击下方阶段,体验执行流程:

// 点击上方按钮开始构建...
clean
清理 target/ 目录,删除上次构建产物
compile
编译 src/main/javatarget/classes
test
运行 src/test/java 下的单元测试用例
package
将编译结果打包为 JAR/WAR 文件
install
将包安装到本地仓库,供其他项目依赖

实战:Hutool 工具库

任务描述:

  1. 新建一个 Maven 项目 challenge-maven
  2. 引入 Hutool 依赖(一个超好用的国产Java工具包)
  3. 编写代码打印当前时间
  4. 执行打包命令

💡 提示: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 的设计:

  • 📦 Plugin (插件)Java Class (类)
    它是功能的集合体。例:CompilerPlugin
  • 🎯 Goal (目标)Public Method (方法)
    它是最小执行单元。例:compile()
// 伪代码演示
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>

最佳实践总结

最佳实践总结

项目组织:

  • 遵循标准目录结构,不要随意修改
  • 使用有意义的 groupIdartifactId
  • 版本号遵循语义化版本规范

依赖管理:

  • 优先使用稳定版本,避免 alpha/beta
  • 合理设置 scope,减少不必要的依赖
  • 使用 dependencyManagement 统一版本管理

构建优化:

  • 配置国内镜像,提升下载速度
  • 本地仓库放在非系统盘
  • 定期清理不需要的依赖

团队协作:

  • 统一团队的开发环境配置
  • settings.xml 纳入版本控制管理
  • 建立规范的构建和发布流程
1 / X