上一章通过 Spring Initializr 创建了 spring 项目,并使用 ./mvnw spring-boot:run 启动应用。我们虽然没有在系统中单独安装 Maven,却已经借助 Maven 完成了依赖下载、源码编译和应用启动。
Maven 是 Java 项目的构建与依赖管理工具。它不负责处理 HTTP 请求,也不读取 application.properties 中的端口;它负责把项目源码、资源和依赖组织成可以运行或交付的构建产物。
1. Maven
Java 源文件不能直接变成 Spring Boot 应用。一个完整构建通常需要处理资源、编译主代码、编译测试、运行测试并打包 JAR。Maven 为这些步骤提供统一的项目模型和生命周期。
Maven 中经常出现下面几个概念:
| 概念 | 当前项目中的例子 | 含义 |
|---|---|---|
| POM | pom.xml | 描述项目、依赖、插件和构建配置 |
| 坐标 | cn.usehook:spring:0.0.1-SNAPSHOT | 唯一标识一个 Maven 构建产物 |
| 依赖 | spring-boot-starter-webmvc | 当前项目编译或运行时需要的库 |
| 插件 | spring-boot-maven-plugin | 执行编译、测试、打包或运行等构建任务 |
| 生命周期 | compile、test、package | 约定构建阶段及其执行顺序 |
| 仓库 | Maven Central、本地仓库 | 保存 POM、JAR 和插件 |
执行 ./mvnw spring-boot:run 时,各部分按照下面的顺序协作:
1mvnw2→ Maven Wrapper 准备指定版本的 Maven3→ Maven 读取 pom.xml4→ 解析项目坐标、依赖和插件5→ 从本地仓库或远程仓库取得所需文件6→ 编译项目7→ spring-boot-maven-plugin 启动应用
Maven 遵循「约定优于配置」。如果没有额外修改,它会从 src/main/java 查找主代码,从 src/main/resources 读取资源,从 src/test/java 查找测试,并把构建结果写入 target。Spring Initializr 生成的项目正是这种标准结构。
2. Maven Wrapper
Maven Wrapper 让每个项目记录自己使用的 Maven 版本。项目成员只需要安装 JDK,就可以通过仓库中的 Wrapper 脚本启动一致的 Maven,不必依赖电脑上全局安装的 mvn。
Wrapper 由三个部分组成:
| 文件 | 平台 | 职责 |
|---|---|---|
mvnw | macOS、Linux | 启动 Wrapper 的 Shell 脚本 |
mvnw.cmd | Windows | 启动 Wrapper 的批处理脚本 |
.mvn/wrapper/maven-wrapper.properties | 所有平台 | 保存 Maven 发行版和 Wrapper 配置 |
maven-wrapper.properties 的典型内容如下。版本号应以项目实际生成的文件为准:
1wrapperVersion=3.3.42distributionType=only-script3distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
| 配置项 | 作用 |
|---|---|
wrapperVersion | 生成当前 Wrapper 脚本时使用的 Maven Wrapper 版本 |
distributionType | Wrapper 的实现形式;only-script 不需要提交 maven-wrapper.jar |
distributionUrl | Maven 二进制发行包的下载地址,也是实际锁定 Maven 版本的位置 |
还可以设置 distributionSha256Sum,让 Wrapper 验证 Maven 发行包的 SHA-256 摘要。它不是运行项目的必填项,但可以在持续集成环境中确认下载内容与预期一致。
第一次执行 Wrapper 时,它会下载 distributionUrl 指向的 Maven,并缓存到 Maven 用户目录的 wrapper/dists。后续执行通常直接复用缓存。项目依赖不保存在这个目录,而是由 Maven 下载到本地仓库。
根据操作系统检查当前项目实际使用的 Maven:
1./mvnw --version
输出会同时包含 Maven 版本、Java 版本和运行平台。Maven 版本应该与 distributionUrl 一致;Java 版本来自当前终端环境,因为 Wrapper 只管理 Maven,不负责安装或选择 JDK。
mvnw、mvnw.cmd 和 .mvn/wrapper/maven-wrapper.properties 都应该提交到 Git。团队和持续集成环境应优先执行 Wrapper,避免不同机器的全局 Maven 版本造成构建差异。
3. POM
POM 是 Project Object Model 的缩写。Maven 在项目根目录读取 pom.xml,再结合父 POM 和 Maven 默认配置,得到最终的项目模型。
spring 项目的主要配置如下:
01<?xml version="1.0" encoding="UTF-8"?>02<project xmlns="http://maven.apache.org/POM/4.0.0"03xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"04xsi:schemaLocation="http://maven.apache.org/POM/4.0.005https://maven.apache.org/xsd/maven-4.0.0.xsd">06<modelVersion>4.0.0</modelVersion>0708<parent>09<groupId>org.springframework.boot</groupId>10<artifactId>spring-boot-starter-parent</artifactId>11<version>4.1.1</version>12<relativePath />13</parent>1415<groupId>cn.usehook</groupId>16<artifactId>spring</artifactId>17<version>0.0.1-SNAPSHOT</version>18<name>spring</name>19<description>Spring Boot learning project</description>2021<properties>22<java.version>21</java.version>23</properties>2425<dependencies>26<dependency>27<groupId>org.springframework.boot</groupId>28<artifactId>spring-boot-starter-webmvc</artifactId>29</dependency>3031<dependency>32<groupId>org.springframework.boot</groupId>33<artifactId>spring-boot-starter-webmvc-test</artifactId>34<scope>test</scope>35</dependency>36</dependencies>3738<build>39<plugins>40<plugin>41<groupId>org.springframework.boot</groupId>42<artifactId>spring-boot-maven-plugin</artifactId>43</plugin>44</plugins>45</build>46</project>
modelVersion 表示当前 POM 使用的模型版本。Maven 3 项目通常使用 4.0.0,它不是当前项目的版本。
groupId、artifactId 和 version 共同组成项目坐标:
1cn.usehook:spring:0.0.1-SNAPSHOT
groupId 表示组织或项目组,artifactId 是项目和默认构建产物的名称,version 是版本。SNAPSHOT 表示这个版本仍处于开发阶段。执行打包后,默认产物名称为 target/spring-0.0.1-SNAPSHOT.jar。
Maven 坐标不等于 Java 包名。groupId=cn.usehook 不会强制源码必须放在 cn.usehook 包中,只是项目通常采用相近命名,方便识别代码归属。
parent 声明当前项目继承 spring-boot-starter-parent。父 POM 提供常用依赖版本、插件版本和构建默认值,因此 Web Starter 和 Spring Boot 插件不需要在当前 POM 中重复填写版本。<relativePath /> 表示不从本地相对路径寻找父项目,而是通过仓库解析父 POM。
<java.version>21</java.version> 是 Spring Boot Parent 识别的属性,用于配置 Java 编译版本。它不会安装 JDK,实际执行 Maven 的机器仍然需要准备兼容的 Java 开发环境。
可以查看 Maven 合并父 POM 和默认值后的最终配置:
1./mvnw help:effective-pom
当代码中没有显式配置某个版本或插件行为时,effective-pom 可以帮助定位配置究竟来自当前 POM、Spring Boot Parent 还是 Maven 默认值。
4. 依赖
dependencies 中声明项目编译、运行或测试需要的库。一个依赖由 groupId、artifactId、version 和可选的 scope 标识。版本可以由当前 POM 直接指定,也可以由父 POM 的依赖管理统一提供。
spring-boot-starter-webmvc 是直接依赖,但它还依赖 Spring MVC、Jackson 和内嵌服务器等组件。Maven 会继续解析这些间接需要的库,这些库称为传递依赖。
使用下面的命令可以查看完整依赖树:
1./mvnw dependency:tree
依赖的 scope 决定它在哪些阶段可用:
| scope | 编译主代码 | 运行应用 | 编译和运行测试 | 常见用途 |
|---|---|---|---|---|
compile | 是 | 是 | 是 | 默认作用域,业务代码直接使用的库 |
runtime | 否 | 是 | 是 | 数据库驱动等运行时实现 |
test | 否 | 否 | 是 | 测试框架和测试 Starter |
provided | 是 | 否 | 是 | 由目标运行环境提供的 API |
Web Starter 没有显式 scope,所以使用默认的 compile。spring-boot-starter-webmvc-test 使用 test,不会成为应用正常运行所需的依赖。
dependencyManagement 与 dependencies 的作用不同。前者集中约束依赖版本,本身不会把依赖加入项目;后者才表示当前项目真正需要某个依赖。Spring Boot Parent 已经管理了大量常用库的兼容版本,添加这些库时通常不应该自行覆盖版本。
当某个传递依赖确实不应该进入项目时,可以在直接依赖中排除它:
01<dependency>02<groupId>example</groupId>03<artifactId>example-starter</artifactId>04<version>1.0.0</version>05<exclusions>06<exclusion>07<groupId>example</groupId>08<artifactId>unused-library</artifactId>09</exclusion>10</exclusions>11</dependency>
排除依赖会改变运行时 classpath,应该先通过 dependency:tree 确认来源和影响,而不是看到重复名称就随意排除。
5. 插件与生命周期
依赖为项目代码提供可调用的类型,插件为 Maven 提供可以执行的构建目标。spring-boot-maven-plugin 负责运行应用,并把普通 JAR 重新打包为包含启动信息和依赖的 Spring Boot 可执行 JAR。
Maven 的默认生命周期包含一系列按顺序执行的阶段。执行后面的阶段时,前面的阶段也会依次执行:
| 阶段 | 主要工作 |
|---|---|
validate | 检查项目结构和必要信息 |
compile | 编译 src/main/java |
test | 运行单元测试 |
package | 生成 JAR 或 WAR |
verify | 执行额外检查,确认构建结果有效 |
install | 将产物安装到本地 Maven 仓库 |
deploy | 将产物发布到远程 Maven 仓库 |
例如,执行 package 会先完成 validate、资源处理、compile 和 test,然后才生成 JAR。clean 属于独立的清理生命周期,它会删除上一次构建的 target:
1./mvnw clean package
命令中的 spring-boot:run 不是生命周期阶段,而是插件目标,格式为「插件前缀:目标」:
1./mvnw spring-boot:run
Spring Boot Parent 已经配置了 repackage 执行。当当前 POM 声明 spring-boot-maven-plugin 后,执行 package 会生成可通过下面命令启动的 JAR:
1java -jar target/spring-0.0.1-SNAPSHOT.jar
6. Maven 仓库
Maven 仓库保存 POM、JAR 和插件。解析一个依赖时,Maven 会先检查本地仓库;本地不存在时,再从 Maven Central 等远程仓库下载。
默认本地仓库位置如下:
1~/.m2/repository
仓库目录按照 groupId、artifactId 和 version 逐层组织。例如 cn.usehook:spring:0.0.1-SNAPSHOT 安装后的路径大致为:
1cn/usehook/spring/0.0.1-SNAPSHOT/
执行 ./mvnw install 会把当前项目的构建产物放入本地仓库,供本机其他 Maven 项目引用。deploy 则用于发布到团队或公共远程仓库,不等于部署正在运行的 Spring Boot 服务。
~/.m2/settings.xml 是 Maven 的用户级配置,可以设置镜像、代理、认证信息和自定义本地仓库位置。它不应该用来声明当前项目必须具备的业务依赖;需要随项目共享的依赖和插件仍应写入 pom.xml。
本地仓库是 Maven 管理的缓存和构建输入,不建议手动复制或修改其中的 JAR。依赖下载损坏时,应先确认网络、镜像和校验错误,再针对明确的依赖重新解析。
7. 常用命令
下面这些命令覆盖了当前阶段最常用的 Maven 操作。示例使用 macOS 和 Linux 的 ./mvnw;Windows 将它替换为 .\mvnw.cmd 即可。
| 命令 | 作用 |
|---|---|
./mvnw --version | 查看 Maven、Java 和运行环境 |
./mvnw compile | 编译主代码 |
./mvnw test | 编译并运行测试 |
./mvnw clean package | 清理并生成可执行 JAR |
./mvnw spring-boot:run | 通过 Spring Boot 插件运行项目 |
./mvnw dependency:tree | 查看直接依赖和传递依赖 |
./mvnw help:effective-pom | 查看合并后的最终 POM |
./mvnw install | 构建并安装到本地仓库 |
Maven 命令可以从左到右包含多个目标。例如 clean package 先执行清理生命周期的 clean,再执行默认生命周期直到 package。命令执行失败时,应从输出中找到第一个 BUILD FAILURE 对应的具体错误,再判断是 Java 编译、测试、依赖解析还是插件执行失败。