Skip to content

Maven 基础 ​

S0-003必学入门约 90 分钟JDK 21学习准备与环境 · 工程工具

1. 本节定位 ​

在 《IDEA 与项目创建》 里,你建了一个 Maven 项目,知道了 pom.xml 和 src/main/java。本节要把 Maven 从"项目模板"变成"随时可用的工具":编译、测试、打包、看依赖、排查依赖冲突。

Maven 是后续所有内容的基础设施——Spring Boot 项目、测试框架、覆盖率插件,全部通过它引入和运行。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说清 Maven 的坐标、目录结构、生命周期三件事;
  • 用 mvn compile / test / package / dependency:tree 完成日常操作;
  • 解释 scope(compile / provided / runtime / test)的作用;
  • 看懂依赖树,判断一个类是从哪个依赖来的;
  • 遇到"依赖下载失败""版本冲突"时知道去哪里查。

3. 前置知识检查 ​

  • 能用 IDE 或命令行编译运行 Java 文件吗?(《IDEA 与项目创建》)
  • 知道 pom.xml 里 groupId、artifactId、version 的含义吗?

4. 为什么需要这个知识 ​

没有构建工具的时代,项目里有一个 lib/ 目录,塞满手工下载的 .jar。问题接踵而来:

  • 依赖传递:用 Spring 就要带上它依赖的十几个 jar,漏一个就 NoClassDefFoundError;
  • 版本地狱:A 库要 commons-lang 2.6,B 库要 3.12,到底放哪个?
  • 构建不一致:你的机器能编译,同事的机器不行,因为 IDE 设置不同;
  • 流程手工:编译、跑测试、打包、复制 jar,全是重复劳动。

Maven 用三层约定解决它们:坐标定位构件、仓库自动下载、生命周期固定流程。这也是"约定优于配置"最典型的例子。

5. 核心概念 ​

5.1 坐标:构件的唯一身份 ​

xml
<groupId>cn.kb.learn</groupId>        <!-- 谁做的:组织或包名反写 -->
<artifactId>start</artifactId>         <!-- 叫什么:项目/模块名 -->
<version>1.0.0</version>               <!-- 第几版 -->

三者 + 打包类型(jar/war/pom)唯一确定一个构件。后续引入任何依赖,都是写这四项。

5.2 生命周期:把"构建"拆成有序阶段 ​

Maven 有三套生命周期,日常只用 default 这一套,它的关键阶段按顺序执行:

text
validate → compile → test → package → verify → install → deploy
  校验      编译     测试    打包      验证      装到本地   发布到远程

关键规则:执行后面的阶段,会自动执行前面所有阶段。

powershell
./mvnw compile    # 只到编译
./mvnw test       # 编译 + 跑测试
./mvnw package    # 编译 + 测试 + 打成 jar
./mvnw install    # 上面的全部 + 装进本地仓库(供其他模块引用)

所以 mvn package 失败时,先看是编译错误还是测试失败——它把前面的阶段都跑了一遍。

5.3 依赖与 scope ​

xml
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.11.3</version>
  <scope>test</scope>          <!-- 只在测试阶段可见 -->
</dependency>
scope何时可见是否进入发布产物典型用途
compile(默认)全部阶段是业务依赖,如 Spring
provided编译与测试否容器已提供的,如 Servlet API
runtime运行与测试是运行时才需要的,如 JDBC 驱动
test仅测试否JUnit、Mockito

scope 写错的典型症状:测试依赖写成了 compile,打出来的 jar 里塞了一堆测试库。

5.4 传递依赖与依赖调解 ​

你只声明 mockito-core,但实际用到了 byte-buddy——因为 Maven 会把依赖的依赖也拉进来,这叫传递依赖。

版本冲突时,Maven 用两条规则决定用哪个版本:

  1. 最短路径优先:离你的 pom.xml 层级更近的胜出;
  2. 先声明优先:路径长度相同时,写在前面 <dependency> 的胜出。

不确定时别猜,用依赖树看:

powershell
./mvnw -pl testing-unit dependency:tree
text
[INFO] \- org.mockito:mockito-core:jar:5.14.2:test
[INFO]    +- net.bytebuddy:byte-buddy:jar:1.15.4:test
[INFO]    +- net.bytebuddy:byte-buddy-agent:jar:1.15.4:test
[INFO]    \- org.objenesis:objenesis:jar:3.3:test

这段输出告诉你:byte-buddy 不是你自己声明的,是 Mockito 带进来的,版本 1.15.4。

5.5 仓库的三个层次 ​

text
本地仓库(~/.m2/repository)
   ↓ 没有就找
远程仓库(中央仓库 / 公司私服 / 阿里云等镜像)
   ↓ 没有就报错
构建失败:Could not resolve dependencies

国内必须配镜像,否则下载会慢到不可用——这就是 《IDEA 与项目创建》 里配 settings.xml 的原因,也是本仓库 labs/.mvn/settings.xml 做的事。

6. 递进示例 ​

6.1 最小示例:打包 ​

powershell
cd labs
./mvnw -pl start clean package

-pl start 表示"只构建 start 这个模块"(多模块工程里非常常用)。

text
[INFO] Building jar: ...\labs\start\target\start-1.0.0.jar
[INFO] BUILD SUCCESS
[INFO] Total time:  11.224 s

产物在 start/target/start-1.0.0.jar。注意 target 目录是构建产物,不提交到 Git(本仓库已在 .gitignore 里排除)。

6.2 业务示例:多模块与依赖共享 ​

xml
<!-- labs/pom.xml:父工程,只做统一管理,不写业务代码 -->
<packaging>pom</packaging>
<modules>
  <module>start</module>
  <module>oop-basics</module>
</modules>

<properties>
  <maven.compiler.release>21</maven.compiler.release>   <!-- 所有子模块共用 -->
</properties>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.11.3</version>        <!-- 版本只在父工程写一次 -->
    </dependency>
  </dependencies>
</dependencyManagement>

子模块就不用再写版本号:

xml
<parent>
  <groupId>cn.kb.learn</groupId>
  <artifactId>labs</artifactId>
  <version>1.0.0</version>
</parent>

<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <!-- 版本由父工程的 dependencyManagement 决定 -->
  </dependency>
</dependencies>

dependencyManagement 与 dependencies 的区别是高频面试点:前者只是"管理版本",不会真的引入依赖;后者才是引入。

6.3 反例:靠猜版本解决冲突 ​

xml
<!-- 不好:因为"看着像",手工指定一个传递依赖的版本 -->
<dependency>
  <groupId>net.bytebuddy</groupId>
  <artifactId>byte-buddy</artifactId>
  <version>1.15.10</version>
</dependency>

这么写能"修好"当前的报错,但下次别人升级 Mockito 时,版本又对不上了,而且没人知道这行是为什么加的。

正确姿势:先用 dependency:tree 定位冲突来源,再考虑用 dependencyManagement 统一管理版本,并在注释里写明原因。

7. 完整可运行代码 ​

文件:labs/start/pom.xml(S0 阶段的实验模块)

xml
<?xml version="1.0" encoding="UTF-8"?>
<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">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>cn.kb.learn</groupId>
    <artifactId>labs</artifactId>
    <version>1.0.0</version>
  </parent>

  <artifactId>start</artifactId>
  <description>S0 学习准备与环境</description>
</project>

命令序列与预期输出:

powershell
cd labs

./mvnw -pl start compile          # 编译
./mvnw -pl start test             # 跑测试(start 模块暂无测试,会通过)
./mvnw -pl start clean package    # 打 jar

./mvnw -pl testing-unit dependency:tree   # 看依赖树
./mvnw -pl testing-unit test -Dtest=OrderCalculatorTest   # 只跑指定测试类
text
[INFO] Building jar: ...\labs\start\target\start-1.0.0.jar
[INFO] BUILD SUCCESS
[INFO] Total time:  11.224 s

本项目为什么用 mvnw

labs 自带 Maven Wrapper(mvnw / mvnw.cmd),第一次运行会自动下载对应版本的 Maven 到用户目录,本机无需预装 Maven。这也保证了你和 CI 用的是同一个 Maven 版本。

Windows 上用 .\mvnw.cmd,macOS / Linux 上用 ./mvnw。

8. Debug 观察点 ​

  1. 执行 ./mvnw -pl start clean package,观察 start/target/ 下生成了什么:classes/(编译产物)、start-1.0.0.jar(打包产物);
  2. 执行 ./mvnw -pl testing-unit dependency:tree,找到 byte-buddy 那一行,它的缩进层级表示"被谁引入"——缩进越深,离你的直接依赖越远;
  3. 执行 ./mvnw -X -pl start compile(-X 是 debug 模式),输出会爆炸式增长:里面有 Maven 实际使用的 settings.xml 路径、本地仓库位置、JDK 版本。排查"配置没生效"时直接搜这些关键字段;
  4. 故意把 pom.xml 里的 artifactId 改成不存在的值,观察报错信息里 Maven 如何描述问题。

9. 常见错误 ​

错误 1:Could not resolve dependencies ​

三种原因,按顺序排查:坐标写错(拼写、groupId 与 artifactId 弄反);镜像没配(国内必配);本地仓库里有损坏的半成品文件(删除对应目录重试)。

错误 2:改了 pom.xml 但没生效 ​

原因:IDEA 没有刷新 Maven 项目。修复:点击右上角 Maven 面板的刷新按钮,或右键 pom.xml → Reload project。

错误 3:source option 5 is no longer supported 之类的编译级别错误 ​

原因:没有声明编译目标,Maven 用了远古默认值。修复:在 properties 里写 <maven.compiler.release>21</maven.compiler.release>。

错误 4:把 target/ 提交进了 Git ​

症状:仓库里出现几百个 .class 文件,每次构建都有巨大的 diff。修复:加 .gitignore 并从版本控制移除:

powershell
git rm -r --cached start/target

10. 动手实验 ​

目标:用 Maven 完成一次完整的构建并看懂依赖来源。

步骤:

  1. 在 labs 下执行 ./mvnw -pl start clean package,记录输出里的 jar 路径;
  2. 用 jar tf start/target/start-1.0.0.jar 列出 jar 内容,确认里面有 cn/kb/learn/Main.class;
  3. 执行 ./mvnw -pl testing-unit dependency:tree,把输出保存成 dependency-tree.txt;
  4. 在依赖树里回答三个问题:junit-jupiter 的直接依赖是谁?byte-buddy 是几层传递进来的?它的 scope 是什么?
  5. 执行 ./mvnw -pl testing-unit test -Dtest=UserServiceTest,确认只跑了这一个测试类。

验收方式:能回答第 4 步的三个问题,并解释 jar 里为什么没有测试类。

扩展任务:给 start 模块加一个 commons-lang3 依赖(org.apache.commons:commons-lang3:3.17.0),用 StringUtils.isBlank(...) 写一行代码,再重新打包,观察依赖树的变化。

11. 练习题 ​

基础题

  1. 判断:执行 mvn package 时不会运行测试。(对 / 错)
  2. 填空:Maven 坐标由 、、______ 三部分组成。
  3. 简答:dependencyManagement 与 dependencies 的区别是什么?

代码阅读题

xml
<dependency>
  <groupId>jakarta.servlet</groupId>
  <artifactId>jakarta.servlet-api</artifactId>
  <version>6.0.0</version>
  <scope>provided</scope>
</dependency>
  1. 为什么 Servlet API 要用 provided?如果改成 compile 会有什么后果?

编程题

  1. 为 labs/start 模块配置打包插件,让 jar 的 META-INF/MANIFEST.MF 里带上 Main-Class: cn.kb.learn.Main,然后执行 java -jar start/target/start-1.0.0.jar 验证能直接运行。

综合任务

  1. 假设你在项目里同时用到两个库,它们分别依赖 commons-lang3 的 3.12.0 和 3.17.0 版本:
    • 用 dependency:tree 找出实际生效的版本;
    • 用 dependencyManagement 统一固定为 3.17.0;
    • 写一段注释说明为什么固定这个版本;
    • 说明如果两个版本差异很大(比如有 API 被删除)会带来什么风险。
分级提示

提示 1(方向):第 5 题用 maven-jar-plugin 的 manifest 配置,或 maven-assembly-plugin 打可执行 jar。

提示 2(关键点):第 6 题的依赖树里,看 commons-lang3 出现几次、缩进层级如何;冲突时 Maven 按"最短路径优先"选择。

提示 3(接近答案):第 6 题统一版本后要重跑一次 dependency:tree 确认,并在注释里写清"锁定原因 + 升级条件"。

12. 参考答案 ​

基础题
  1. 错。package 会执行它之前的所有阶段,包括 test。想跳过测试要用 -DskipTests(编译测试但不运行)或 -Dmaven.test.skip=true(连编译都跳过)。
  2. groupId、artifactId、version(外加 packaging)。
  3. dependencyManagement 只声明版本与配置,不真正引入依赖;dependencies 才是引入。子模块通过 dependencyManagement 继承版本号,避免每个模块都写一遍。
代码阅读题(第 4 题)

Servlet API 由 Servlet 容器(Tomcat 等)在运行时提供,不需要打进你的产物。

如果改成 compile:打出来的 jar/war 里会带上 Servlet API 的类,部署到 Tomcat 时可能与容器自带的版本冲突,导致 ClassNotFoundException 或类加载异常——这类问题排查起来非常痛苦。

编程题(第 5 题)
xml
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <version>3.4.2</version>
      <configuration>
        <archive>
          <manifest>
            <mainClass>cn.kb.learn.Main</mainClass>
          </manifest>
        </archive>
      </configuration>
    </plugin>
  </plugins>
</build>

打包后执行:

powershell
java -jar start/target/start-1.0.0.jar
# 预期输出:项目已跑通,JDK 目标版本 = 25
综合任务(第 6 题)
xml
<dependencyManagement>
  <dependencies>
    <!-- 锁定原因:两个上游库分别要求 3.12.0 与 3.17.0,统一用较新的 3.17.0。
         升级条件:下游库升级或确认 3.17.0 无兼容问题时再调整。 -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.17.0</version>
    </dependency>
  </dependencies>
</dependencyManagement>

风险说明:如果某库依赖了在 3.17.0 中被删除的 API,锁定到新版本会在运行时报 NoSuchMethodError(编译期发现不了,因为它读的是自己带的 API)。所以升级依赖后必须跑一遍完整测试——这也是 T1-001 单元测试 的价值所在。

13. 掌握标准 ​

  • [ ] 能用 compile / test / package / dependency:tree 完成日常操作;
  • [ ] 能解释 scope 四种取值的差别并正确选择;
  • [ ] 能用坐标判断一个类是从哪个依赖来的;
  • [ ] 遇到依赖问题时知道先看依赖树,而不是猜版本。

14. 扩展资源 ​

15. 下一步学习 ​

《Git 基础与 cherry-pick》 会把工程代码放进版本控制;后续 《Maven 进阶》 会讲私服与多模块打包。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引