Maven 与项目结构
pom.xml、backend 模块对照。
Maven 与项目结构
这一课你将能做到
- 读懂
pom.xml:坐标(GAV)、依赖、properties、build 插件各部分的作用
- 独立执行 Maven 常用命令(clean/compile/package),并说出标准目录结构每一层的职责
- 用 Spring Initializr 生成一个含 Web + MySQL + Redis 依赖的 Spring Boot 骨架项目,与之前的
javac 手工方式对比出「构建工具」的价值
为什么要学
前 13 课你一直用「单文件 + java Xxx.java」跑代码。写练习够了,但真实工程不行:本站后端依赖 Spring Boot、MyBatis、MySQL 驱动、Redis 客户端等几十个第三方 jar,手工下载、管理版本、维护 classpath 完全不现实。
Maven 解决三件事:
- 依赖管理:在
pom.xml 里写一行坐标,自动下载全部 jar(含传递依赖)
- 标准化构建:一套命令完成 编译 → 测试 → 打包,所有 Java 团队的工程结构长得一样
- 统一结构:进任何一家公司,
src/main/java、src/main/resources 这些路径都是肌肉记忆
学完这课,你就有能力打开本站后端源码并看懂工程结构了。
先建立图像
Maven 的工作流:
pom.xml(项目说明书)
│ mvn compile
▼
按坐标到本地仓库(~/.m2)找 jar ──没有──▶ 到远程仓库(阿里云镜像)下载
│
▼
编译 src/main/java ──▶ target/classes
│ mvn package
▼
打成 target/xxx.jar(可部署产物)
坐标定位一个依赖,全球唯一:
groupId 谁写的 com.mysql
artifactId 叫什么 mysql-connector-j
version 哪个版本 8.4.0
跟着做
步骤 1:确认 Maven 可用
mvn -v
期望输出包含 Apache Maven 3.9.x 和 Java version: 17+。
没有的话:IDEA 自带捆绑版 Maven(Settings → Build Tools → Maven 可查路径);独立安装则解压后把 bin 加入 PATH,并配置阿里云镜像加速(settings.xml 的 <mirrors> 节点里加 https://maven.aliyun.com/repository/public,国内下载速度差 10 倍以上)。
步骤 2:手写一个最小 Maven 工程
新建目录结构(手工建目录体验一次,以后都让 IDE 生成):
maven-demo/
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/roadmap/demo/App.java
└── resources/
└── app.properties
pom.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>
<!-- 坐标:本项目在全球 Maven 世界的唯一身份证 -->
<groupId>com.roadmap</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging> <!-- 打包类型:jar(库/应用)、war(传统 Web) -->
<properties>
<maven.compiler.source>17</maven.compiler.source> <!-- 源码语法级别 -->
<maven.compiler.target>17</maven.compiler.target> <!-- 编译目标字节码版本 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- 依赖清单:写一行坐标,Maven 自动下载并纳入 classpath -->
<dependencies>
<!-- 必备:给所有未标注 scope 的依赖提供的默认(compile)范围示例 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
</dependency>
<!-- 只在编译期需要、运行时由容器提供:servlet API 常设 provided -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
package com.roadmap.demo;
import com.google.gson.Gson;
import java.util.Map;
public class App {
public static void main(String[] args) {
// 引用刚下载的第三方库 Gson——依赖管理生效的直接证据
String json = new Gson().toJson(Map.of("user", "tom", "id", 1001));
System.out.println("JSON: " + json);
System.out.println("配置读取: " + System.getProperty("app.name", "默认值"));
}
}
src/main/resources/app.properties 先放任意内容(此步仅占位,resources 会进最终 jar)。
步骤 3:执行构建生命周期
在 maven-demo 目录(pom.xml 所在处)执行:
mvn clean compile
# clean:删 target;compile:编译 src/main/java → target/classes
# 第一次运行会下载大量依赖到 ~/.m2,耐心等待
mvn package
# 编译 + 测试 + 打包 → target/maven-demo-1.0.0.jar
# 注意:普通 jar 不能直接 java -jar 运行(没有 Main-Class 元数据),见卡点急救
mvn dependency:tree
# 打印依赖树:谁引入了谁、版本冲突在哪里,排冲突神器
对比一下:没有 Maven 之前,你得手动下载 gson.jar、手动 javac -cp gson.jar ...、手动维护一串 classpath;现在一切自动化。
步骤 4:scope 依赖范围
<dependencies>
<!-- compile(默认):编译+测试+运行都可用,会传递给下游 -->
<!-- provided:编译期可用,运行期由环境提供(如 servlet-api、lombok) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope> <!-- 只参与 src/test 的编译运行,不进最终 jar -->
</dependency>
<!-- runtime:编译不可见,运行期可用。典型:MySQL 驱动(代码只写 JDBC 接口,不写驱动类名) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.4.0</version>
<scope>runtime</scope>
</dependency>
</dependencies>
选错 scope 的典型症状:编译报「程序包不存在」(该 runtime 的写成 test),或启动报「找不到驱动类」。
步骤 5:用 Spring Initializr 生成本站同款骨架
- Project:Maven,Language:Java,Spring Boot:3.x
- Group:
com.roadmap,Artifact:backend
- 勾选依赖:Spring Web、Spring Data JPA(或 MyBatis Framework)、MySQL Driver、Lombok、Data Redis
backend/
├── pom.xml ← 依赖里出现 spring-boot-starter-web 等
└── src/
├── main/
│ ├── java/com/roadmap/backend/
│ │ └── BackendApplication.java ← @SpringBootApplication + main
│ └── resources/
│ ├── application.yml ← 端口、MySQL 连接、Redis 地址全在这
│ ├── static/ ← 静态资源
│ └── templates/ ← 模板
└── test/java/ ← 测试代码(package 阶段自动执行)
pom.xml 的核心差异——parent 统一管理版本(BOM 机制):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<dependencies>
<!-- starter = 一组配套依赖的打包;版本号由 parent 统一裁定,不用写 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
在 IDEA 中打开,等索引建完,点击运行 BackendApplication 即可启动内嵌 Tomcat——这就是你后面 Spring Boot 课程的起点。
步骤 6:对照本站后端真实结构
本站 backend 模块采用多模块聚合工程,骨架如下(概念对照用):
backend/
├── pom.xml ← 聚合 POM:modules 列出子模块,dependencyManagement 定版本
├── backend-common/ ← 工具类、统一响应体、全局异常(第 11 课的 BizException 在这)
├── backend-domain/ ← 实体、Mapper、Service(业务核心)
└── backend-web/ ← Controller、配置、启动类,打包成可执行 jar
多模块的好处:公共代码复用、依赖边界清晰。现在只需要认识这个形状,Spring 课会带你逐层填充。
讲清楚要点
- Maven = 依赖管理 + 构建生命周期 + 约定优于配置:目录位置、阶段名称都是约定,不配置也有合理默认值。
- 坐标(GAV)全球唯一:groupId 用域名倒写,artifactId 项目名,version 遵循语义化(1.0.0;SNAPSHOT 表示开发中快照)。
- 依赖会传递:A 依赖 B、B 依赖 C,A 自动拥有 C。传递带来的版本冲突按「最短路径优先、先声明优先」仲裁,排查靠
dependency:tree。
- scope 决定依赖出现在哪个阶段:compile 全程;provided 只编译;runtime 只运行;test 只测试。jar 最终产物里不含 test/provided。
- 生命周期是有序阶段链:
mvn package 会自动执行之前的 validate→compile→test;clean 和 site 是独立链。日常就记 clean、compile、package 三个。
- starter 是 Spring 的依赖打包艺术:引一个
spring-boot-starter-web,自动带上 Spring MVC + Jackson + 内嵌 Tomcat + 日志全家桶,版本由 parent 兜底。
你可能会懵的地方
-
mvn package 出的 jar 为什么不能 java -jar 直接跑?
默认 jar 插件生成的「瘦 jar」不含依赖 jar,也没有 Main-Class 清单。Spring Boot 的 spring-boot-maven-plugin 会 repackage 成「胖 jar」——依赖全嵌进去、指定启动类。所以 Spring Boot 项目才能 java -jar backend.jar 一键启动。
-
~/.m2 仓库是什么?删了会怎样?
本地缓存仓库,存所有下载过的 jar。删了不丢代码,下次构建重新联网下载(慢)。磁盘紧张时可删 *.lastUpdated 残留文件修复「下载失败缓存」。
-
同一个 jar 两个版本被依赖了,听谁的?
Maven 按最短路径 + 先声明优先仲裁,最终只留一个版本进 classpath。冲突排查:mvn dependency:tree -Dincludes= groupId:artifactId 定位谁引入的,用 <exclusions> 排掉或 <dependencyManagement> 钉死版本。
-
为什么 Spring Boot 的依赖不用写版本号?
parent(spring-boot-starter-parent)里的 <dependencyManagement> 预置了整个生态的版本清单,子项目只写坐标。既省事,又保证几十个 jar 版本互相兼容。
-
Maven 和 Gradle 什么关系?
两个主流构建工具,做同样的事。Maven 用 XML、约定重、国内企业存量最大;Gradle 用 Groovy/Kotlin DSL、灵活、Android 系主流。本站和本课程用 Maven。
-
IDEA 里点绿色三角运行,和 mvn 有什么关系?
IDEA 的运行按钮直接调 javac/java(快,跳过完整生命周期);Maven 命令走标准构建链(打包、CI 部署必须)。开发期两者并存,交付靠 Maven。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 手工往工程里塞 jar 包建 lib 目录 | 版本失控、无法传递依赖 | 全部走 pom 依赖坐标 |
| 依赖不写 version 且没有 parent/dependencyManagement | 构建报「missing version」 | 写版本或交给 parent 管理 |
| MySQL 驱动 scope 写成 compile 还纳进 war | 冲突/冗余 | 驱动用 runtime;web 容器托管类用 provided |
| 改了依赖 IDEA 不认 | 索引未刷新 | 点 Maven 面板刷新按钮,或 mvn -U 强制更新 |
mvn package 后 java -jar 失败「no main manifest」 | 瘦 jar 无启动配置 | Spring Boot 项目确认 repackage 插件;普通项目配 mainClass |
groupId 写成项目拼音缩写 com.xmgl | 不专业、易冲突 | 域名倒写 com.roadmap.xxx |
卡点急救
- 下载依赖卡住/超时:国内网络问题。
settings.xml 配阿里云镜像;或走公司私服(Nexus)。检查是否被代理劫持。
- 报错「无效的目标发行版:17」:本机 Maven 用的 JDK 版本低于 17。IDEA 里 Project Structure → SDK 选 17+;Settings → Maven → JDK for importer 同步改。
No compiler is provided in this environment:装的是 JRE 不是 JDK,或 JAVA_HOME 指错。指向 JDK 根目录。
- 依赖红色下划线「Cannot resolve」:先刷新 Maven 面板;再删
~/.m2/repository 下对应目录重下;版本号拼写错误也常见(去 mvnrepository.com 核对)。
- resources 里的文件读取不到:确认放在
src/main/resources 且路径引用 /app.properties 根开头;注意 target/classes 才是运行时实际位置,改完 resources 要重新构建。
- 多模块工程子模块标红:先在父 pom 所在目录执行
mvn install,让子模块先装进本地仓库。
面试常问
-
Maven 的生命周期有哪些?常用命令?
三套独立链:default(核心:validate→compile→test→package→install→deploy)、clean、site。日常:clean compile、clean package、install(装进本地仓库供其他模块引用)。
-
Maven 如何解决依赖冲突?
传递依赖按「最短路径优先、声明顺序优先」仲裁;dependency:tree 定位来源;exclusions 排除、dependencyManagement 统一版本。
-
compile、provided、runtime、test 四种 scope 的区别?
参与的构建阶段与是否打进产物:compile 全程且传递;provided 编译期在、运行期由容器提供;runtime 编译不可见运行可用(典型 JDBC 驱动);test 仅测试期(典型 JUnit)。
-
dependencyManagement 和 dependencies 的区别?
dependencyManagement 只声明版本不实际引入,子模块引用时免写版本;dependencies 实际引入依赖。BOM 统一版本治理就靠它。
-
Spring Boot 的 starter 是什么机制?
依赖聚合 + 自动配置:一个 starter 拉齐一组互相兼容的依赖,配合 @EnableAutoConfiguration 按类路径自动装配 Bean。相当于「功能级依赖包」。
自测三问
- GAV 坐标分别是什么?为什么 groupId 惯例用域名倒写?
mvn package 生成的 jar 想用 java -jar 直接运行,缺了什么?Spring Boot 怎么补上的?
- MySQL 驱动应该用什么 scope?为什么?写错成 test 会在哪个阶段报什么错?
小结一句话
Maven 用坐标管理依赖、用标准生命周期构建、用约定目录统一工程;starter + parent 是 Spring Boot 依赖体系的两大支柱——从此告别手工 javac,你具备了打开本站后端源码的能力。
延伸