主题
界面
主题
在 《IDEA 与项目创建》 里,你建了一个 Maven 项目,知道了 pom.xml 和 src/main/java。本节要把 Maven 从"项目模板"变成"随时可用的工具":编译、测试、打包、看依赖、排查依赖冲突。
Maven 是后续所有内容的基础设施——Spring Boot 项目、测试框架、覆盖率插件,全部通过它引入和运行。
完成本节后,你应该能够:
mvn compile / test / package / dependency:tree 完成日常操作;scope(compile / provided / runtime / test)的作用;pom.xml 里 groupId、artifactId、version 的含义吗?没有构建工具的时代,项目里有一个 lib/ 目录,塞满手工下载的 .jar。问题接踵而来:
NoClassDefFoundError;commons-lang 2.6,B 库要 3.12,到底放哪个?Maven 用三层约定解决它们:坐标定位构件、仓库自动下载、生命周期固定流程。这也是"约定优于配置"最典型的例子。
<groupId>cn.kb.learn</groupId> <!-- 谁做的:组织或包名反写 -->
<artifactId>start</artifactId> <!-- 叫什么:项目/模块名 -->
<version>1.0.0</version> <!-- 第几版 -->三者 + 打包类型(jar/war/pom)唯一确定一个构件。后续引入任何依赖,都是写这四项。
Maven 有三套生命周期,日常只用 default 这一套,它的关键阶段按顺序执行:
validate → compile → test → package → verify → install → deploy
校验 编译 测试 打包 验证 装到本地 发布到远程关键规则:执行后面的阶段,会自动执行前面所有阶段。
./mvnw compile # 只到编译
./mvnw test # 编译 + 跑测试
./mvnw package # 编译 + 测试 + 打成 jar
./mvnw install # 上面的全部 + 装进本地仓库(供其他模块引用)所以 mvn package 失败时,先看是编译错误还是测试失败——它把前面的阶段都跑了一遍。
<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 里塞了一堆测试库。
你只声明 mockito-core,但实际用到了 byte-buddy——因为 Maven 会把依赖的依赖也拉进来,这叫传递依赖。
版本冲突时,Maven 用两条规则决定用哪个版本:
pom.xml 层级更近的胜出;<dependency> 的胜出。不确定时别猜,用依赖树看:
./mvnw -pl testing-unit dependency:tree[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。
本地仓库(~/.m2/repository)
↓ 没有就找
远程仓库(中央仓库 / 公司私服 / 阿里云等镜像)
↓ 没有就报错
构建失败:Could not resolve dependencies国内必须配镜像,否则下载会慢到不可用——这就是 《IDEA 与项目创建》 里配 settings.xml 的原因,也是本仓库 labs/.mvn/settings.xml 做的事。
cd labs
./mvnw -pl start clean package-pl start 表示"只构建 start 这个模块"(多模块工程里非常常用)。
[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 里排除)。
<!-- 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>子模块就不用再写版本号:
<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 的区别是高频面试点:前者只是"管理版本",不会真的引入依赖;后者才是引入。
<!-- 不好:因为"看着像",手工指定一个传递依赖的版本 -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.15.10</version>
</dependency>这么写能"修好"当前的报错,但下次别人升级 Mockito 时,版本又对不上了,而且没人知道这行是为什么加的。
正确姿势:先用 dependency:tree 定位冲突来源,再考虑用 dependencyManagement 统一管理版本,并在注释里写明原因。
文件:labs/start/pom.xml(S0 阶段的实验模块)
<?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>命令序列与预期输出:
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 # 只跑指定测试类[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。
./mvnw -pl start clean package,观察 start/target/ 下生成了什么:classes/(编译产物)、start-1.0.0.jar(打包产物);./mvnw -pl testing-unit dependency:tree,找到 byte-buddy 那一行,它的缩进层级表示"被谁引入"——缩进越深,离你的直接依赖越远;./mvnw -X -pl start compile(-X 是 debug 模式),输出会爆炸式增长:里面有 Maven 实际使用的 settings.xml 路径、本地仓库位置、JDK 版本。排查"配置没生效"时直接搜这些关键字段;pom.xml 里的 artifactId 改成不存在的值,观察报错信息里 Maven 如何描述问题。Could not resolve dependencies 三种原因,按顺序排查:坐标写错(拼写、groupId 与 artifactId 弄反);镜像没配(国内必配);本地仓库里有损坏的半成品文件(删除对应目录重试)。
pom.xml 但没生效 原因:IDEA 没有刷新 Maven 项目。修复:点击右上角 Maven 面板的刷新按钮,或右键 pom.xml → Reload project。
source option 5 is no longer supported 之类的编译级别错误 原因:没有声明编译目标,Maven 用了远古默认值。修复:在 properties 里写 <maven.compiler.release>21</maven.compiler.release>。
target/ 提交进了 Git 症状:仓库里出现几百个 .class 文件,每次构建都有巨大的 diff。修复:加 .gitignore 并从版本控制移除:
git rm -r --cached start/target目标:用 Maven 完成一次完整的构建并看懂依赖来源。
步骤:
labs 下执行 ./mvnw -pl start clean package,记录输出里的 jar 路径;jar tf start/target/start-1.0.0.jar 列出 jar 内容,确认里面有 cn/kb/learn/Main.class;./mvnw -pl testing-unit dependency:tree,把输出保存成 dependency-tree.txt;junit-jupiter 的直接依赖是谁?byte-buddy 是几层传递进来的?它的 scope 是什么?./mvnw -pl testing-unit test -Dtest=UserServiceTest,确认只跑了这一个测试类。验收方式:能回答第 4 步的三个问题,并解释 jar 里为什么没有测试类。
扩展任务:给 start 模块加一个 commons-lang3 依赖(org.apache.commons:commons-lang3:3.17.0),用 StringUtils.isBlank(...) 写一行代码,再重新打包,观察依赖树的变化。
基础题
mvn package 时不会运行测试。(对 / 错)dependencyManagement 与 dependencies 的区别是什么?代码阅读题
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>provided?如果改成 compile 会有什么后果?编程题
labs/start 模块配置打包插件,让 jar 的 META-INF/MANIFEST.MF 里带上 Main-Class: cn.kb.learn.Main,然后执行 java -jar start/target/start-1.0.0.jar 验证能直接运行。综合任务
commons-lang3 的 3.12.0 和 3.17.0 版本: dependency:tree 找出实际生效的版本;dependencyManagement 统一固定为 3.17.0;提示 1(方向):第 5 题用 maven-jar-plugin 的 manifest 配置,或 maven-assembly-plugin 打可执行 jar。
提示 2(关键点):第 6 题的依赖树里,看 commons-lang3 出现几次、缩进层级如何;冲突时 Maven 按"最短路径优先"选择。
提示 3(接近答案):第 6 题统一版本后要重跑一次 dependency:tree 确认,并在注释里写清"锁定原因 + 升级条件"。
package 会执行它之前的所有阶段,包括 test。想跳过测试要用 -DskipTests(编译测试但不运行)或 -Dmaven.test.skip=true(连编译都跳过)。dependencyManagement 只声明版本与配置,不真正引入依赖;dependencies 才是引入。子模块通过 dependencyManagement 继承版本号,避免每个模块都写一遍。Servlet API 由 Servlet 容器(Tomcat 等)在运行时提供,不需要打进你的产物。
如果改成 compile:打出来的 jar/war 里会带上 Servlet API 的类,部署到 Tomcat 时可能与容器自带的版本冲突,导致 ClassNotFoundException 或类加载异常——这类问题排查起来非常痛苦。
<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>打包后执行:
java -jar start/target/start-1.0.0.jar
# 预期输出:项目已跑通,JDK 目标版本 = 25<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 单元测试 的价值所在。
compile / test / package / dependency:tree 完成日常操作;scope 四种取值的差别并正确选择;《Git 基础与 cherry-pick》 会把工程代码放进版本控制;后续 《Maven 进阶》 会讲私服与多模块打包。