Maven 从入门到工程实践:POM、仓库、生命周期、依赖与打包

欢迎你来读这篇博客,这篇博客主要是关于 Maven

其中包括 Maven 的基础概念、项目结构、POM、GAV 坐标、仓库配置、生命周期、常用命令、依赖范围、资源目录、测试跳过、普通 Java 项目打包、Spring Boot 项目打包,以及一些常见问题和工程实践经验。

如果把 Java 项目比作一座房子,那么 Maven 就是施工图纸、材料清单、仓库管理员和流水线工头的合体。它不写业务代码,但它决定代码如何被组织、依赖如何被拉取、项目如何被编译、测试、打包和发布。少了它,项目不是不能跑,而是容易跑得像“手搓火箭”:能飞,但每次点火都心惊肉跳。

序言

刚开始学习 Java 的时候,我们经常会把各种 jar 包手动复制到项目里。这个阶段看起来很直观:需要什么 jar,就下载什么 jar,然后放到 lib 目录下。

但项目一复杂,问题就开始来了:

  1. jar 包太多,不知道每个 jar 是干什么的;
  2. 不同 jar 之间还依赖别的 jar,手动维护非常麻烦;
  3. 多个 jar 版本冲突,编译时没问题,运行时突然报错;
  4. 每个人本地环境不同,别人能跑,我这边跑不起来;
  5. 打包上线时,不清楚哪些依赖需要带上,哪些不需要;
  6. 项目结构不统一,接手项目的人需要先猜目录含义。

Maven 要解决的就是这些工程化问题。

它的核心思想可以概括为一句话:

用统一的项目模型,管理 Java 项目的依赖、构建、测试、打包和发布。

Maven 不只是一个“下载 jar 包的工具”。它更像是一套 Java 项目的构建协议:你按照它约定的结构放代码,按照它的 POM 写配置,按照它的生命周期执行命令,它就能用相对稳定的方式把项目构建出来。

正文

chapter 1:Maven 是什么

Maven 是 Apache 旗下的项目管理和构建工具,主要服务于 Java 项目。它通过一个名为 pom.xml 的文件描述项目的信息,包括项目坐标、依赖、插件、构建方式、仓库、模块关系等。

Maven 的常见作用有:

  1. 管理依赖;
  2. 编译源代码;
  3. 执行单元测试;
  4. 打包 jar 或 war;
  5. 安装构件到本地仓库;
  6. 发布构件到远程仓库;
  7. 管理多模块项目;
  8. 通过插件扩展构建能力。

Maven 有几个非常重要的概念:

概念 说明
POM Project Object Model,项目对象模型,核心文件是 pom.xml
GAV Maven 坐标,由 groupIdartifactIdversion 组成
Repository 仓库,用于存放 jar、pom、插件等构件
Lifecycle 生命周期,描述项目构建流程
Phase 生命周期阶段,例如 compiletestpackage
Goal 插件目标,例如 dependency:treedependency:copy-dependencies
Plugin 插件,真正执行编译、测试、打包等动作
Scope 依赖作用域,决定依赖在编译、测试、运行时是否生效

所以 Maven 的运行逻辑不是“执行一个神秘命令”,而是:

读取 POM → 解析依赖 → 确定生命周期阶段 → 调用对应插件 → 生成构建结果。

chapter 2:Maven 项目的标准目录结构

Maven 推崇“约定优于配置”。也就是说,只要你按照 Maven 默认的目录结构组织项目,很多配置就可以不用写。

一个标准 Maven 项目通常长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
demo-project
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com/example/demo
│ │ │ └── App.java
│ │ └── resources
│ │ └── application.yml
│ └── test
│ ├── java
│ │ └── com/example/demo
│ │ └── AppTest.java
│ └── resources
└── target

各目录含义如下:

路径 作用
pom.xml Maven 项目的核心配置文件
src/main/java 主程序 Java 源码
src/main/resources 主程序资源文件,例如配置文件、模板文件
src/test/java 测试代码
src/test/resources 测试资源文件
target Maven 构建后的输出目录

重点记住:

  • src/main/java 里的代码会被编译到 target/classes
  • src/main/resources 里的资源也会被复制到 target/classes
  • src/test/java 里的代码会被编译到 target/test-classes
  • src/test/resources 里的资源会被复制到 target/test-classes
  • target 是构建产物目录,一般不提交到 Git。

这套结构的价值非常大:团队里所有 Java 项目都遵守同一种结构,新人接手项目时,不用先考古半天。

chapter 3:POM 与 GAV 坐标

pom.xml 是 Maven 项目的灵魂文件。它描述了一个项目“是谁”“依赖谁”“怎么构建”“怎么打包”。

最小可用的 pom.xml 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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
https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

</project>

其中最核心的是 GAV 坐标:

1
2
3
<groupId>com.example</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0.0</version>

GAV 的含义如下:

元素 说明 示例
groupId 组织或公司标识,通常是域名反写 com.example
artifactId 项目或模块名称 maven-demo
version 项目版本号 1.0.0
packaging 打包类型 jarwarpom

Maven 用 GAV 唯一定位一个构件。比如:

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>

这段配置的意思是:当前项目需要依赖 org.apache.commons:commons-lang3:3.14.0 这个构件。

注意:版本号里如果带有 SNAPSHOT,通常表示这是一个开发中的快照版本,例如:

1
<version>1.0.0-SNAPSHOT</version>

SNAPSHOT 版本不是稳定发布版,在团队内部开发阶段很常见,但正式发布时一般应该使用明确的 release 版本。

chapter 4:Maven 仓库机制

Maven 仓库用于存放构件。构件可以是 jar,也可以是 pom、war、maven 插件等。

Maven 仓库主要分为三类:

仓库类型 说明
本地仓库 当前机器上的缓存目录,默认在用户目录的 .m2/repository
中央仓库 Maven 官方中央仓库,大量开源依赖都在这里
远程仓库 公司私服、第三方仓库、镜像仓库等

Maven 解析依赖的大致过程是:

  1. 先看本地仓库有没有;
  2. 本地没有,再去远程仓库下载;
  3. 下载成功后缓存到本地仓库;
  4. 后续构建优先使用本地缓存。

默认本地仓库路径通常是:

1
2
Windows: C:\Users\用户名\.m2\repository
macOS/Linux: ~/.m2/repository

如果你想修改本地仓库路径,可以在 settings.xml 中配置:

1
2
3
<settings>
<localRepository>D:/maven/repository</localRepository>
</settings>

settings.xml 通常有两个位置:

1
2
Maven 安装目录/conf/settings.xml
用户目录/.m2/settings.xml

一般更推荐修改用户目录下的 settings.xml,因为它只影响当前用户,不会污染 Maven 安装目录。

chapter 5:配置 Maven 镜像与阿里云仓库

国内使用 Maven 时,经常会遇到依赖下载慢的问题。这时候可以配置镜像仓库。

比较推荐的方式是在 settings.xml 中配置 mirror,而不是在每个项目的 pom.xml 里都写一遍仓库地址。

示例:

1
2
3
4
5
6
7
8
9
10
<settings>
<mirrors>
<mirror>
<id>aliyunmaven</id>
<name>Aliyun Maven Central Mirror</name>
<url>https://maven.aliyun.com/repository/central</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
</settings>

如果你希望所有远程仓库请求都经过某个公司私服或统一代理,可以使用:

1
<mirrorOf>*</mirrorOf>

但这里要注意:

  • mirrorOf="central":只镜像 Maven Central;
  • mirrorOf="*":镜像所有仓库请求;
  • 公司内部项目更推荐搭建 Nexus、Artifactory 这类私服,统一代理外部依赖,并管理内部 jar;
  • 不建议在大量业务项目的 pom.xml 里硬编码公共镜像地址,否则以后换仓库会很麻烦。

项目级别的 repositories 更适合配置项目确实需要的特殊仓库,例如公司私服:

1
2
3
4
5
6
<repositories>
<repository>
<id>company-repo</id>
<url>https://repo.example.com/maven/releases</url>
</repository>
</repositories>

如果仓库需要账号密码,不要把账号密码写进 pom.xml。应该在 settings.xmlservers 中配置:

1
2
3
4
5
6
7
<servers>
<server>
<id>company-repo</id>
<username>your-username</username>
<password>your-password</password>
</server>
</servers>

这里的 id 要和 repository 里的 id 对应。

chapter 6:Maven 生命周期、阶段、插件的关系

Maven 最容易混淆的地方就是生命周期、阶段、插件、目标。

先说结论:

生命周期定义流程,阶段定义步骤,插件真正干活,目标是插件里的具体动作。

Maven 内置三套生命周期:

生命周期 作用
clean 清理构建产物
default 编译、测试、打包、安装、部署
site 生成项目站点文档

最常用的是 default 生命周期,它包含很多阶段,常见阶段如下:

1
validate -> compile -> test -> package -> verify -> install -> deploy

这些阶段是有顺序的。执行后面的阶段,会自动执行前面的阶段。

例如:

1
mvn package

并不是只执行 package,它会先执行:

1
validate -> compile -> test -> package

再比如:

1
mvn install

会执行:

1
validate -> compile -> test -> package -> verify -> install

这就是为什么执行 mvn install 时,测试也会被跑起来。不是 Maven 多管闲事,而是生命周期就是这么安排的。

常见命令如下:

命令 作用
mvn clean 删除 target 目录
mvn compile 编译主程序
mvn test-compile 编译测试代码
mvn test 执行单元测试
mvn package 打包项目
mvn install 打包并安装到本地仓库
mvn deploy 发布到远程仓库
mvn dependency:tree 查看依赖树
mvn help:effective-pom 查看最终生效的 POM
mvn -o package 离线构建

其中 dependency:tree 这种命令不是生命周期阶段,而是插件目标。它的格式一般是:

1
插件前缀:目标

例如:

1
2
3
mvn dependency:tree
mvn dependency:copy-dependencies
mvn help:effective-pom

chapter 7:依赖范围 scope 详解

Maven 的依赖不只是“引入或不引入”这么简单。每个依赖还可以配置作用域,也就是 scope

常见 scope 如下:

scope 编译可用 测试可用 运行可用 是否传递 常见场景
compile 默认值,大多数普通依赖
provided Servlet API、容器提供的依赖
runtime JDBC 驱动、运行时才需要的依赖
test JUnit、Mockito 等测试依赖
system 本地 jar,强烈不推荐
import 不直接参与 不直接参与 不直接参与 不直接参与 BOM 导入,只能用于 dependencyManagement

示例:测试依赖使用 test

1
2
3
4
5
6
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>

示例:Servlet API 使用 provided

1
2
3
4
5
6
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>

为什么 Servlet API 通常是 provided

因为运行时 Tomcat、Jetty 这类容器会提供这部分类。如果你自己再打进去,可能会造成类冲突。

再比如 JDBC 驱动可以配置为 runtime

1
2
3
4
5
6
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.4.0</version>
<scope>runtime</scope>
</dependency>

system scope 虽然能引用本地 jar,但不推荐使用:

1
2
3
4
5
6
7
<dependency>
<groupId>com.example</groupId>
<artifactId>local-lib</artifactId>
<version>1.0.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/libs/local-lib-1.0.0.jar</systemPath>
</dependency>

原因很简单:

  1. 本地路径不可移植;
  2. 别人拉代码后可能没有这个 jar;
  3. CI/CD 环境可能构建失败;
  4. Maven 无法像普通依赖一样管理它。

更推荐的做法是:

  • 把本地 jar 安装到本地仓库;
  • 或上传到公司 Maven 私服;
  • 或把它改造成一个标准 Maven 模块。

安装本地 jar 的命令示例:

1
2
3
4
5
6
mvn install:install-file \
-Dfile=libs/local-lib-1.0.0.jar \
-DgroupId=com.example \
-DartifactId=local-lib \
-Dversion=1.0.0 \
-Dpackaging=jar

chapter 8:properties、dependencyManagement 与 BOM

当项目依赖很多时,如果每个依赖都手写版本号,后期维护会很痛苦。

比如 Spring 相关依赖都要保持同一个版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<properties>
<spring.version>6.1.6</spring.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>

这样做的好处是:以后升级 Spring 版本时,只需要改一个地方。

但在大型项目里,仅靠 properties 还不够。更常见的做法是使用 dependencyManagement 统一管理版本:

1
2
3
4
5
6
7
8
9
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>
</dependencyManagement>

子模块真正使用依赖时,就可以不写版本:

1
2
3
4
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>

注意:dependencyManagement 只是管理版本,不会自动引入依赖。真正引入依赖,还是要写到 dependencies 里。

Spring Boot 项目经常使用 BOM 管理依赖版本:

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

或者直接继承 Spring Boot Parent:

1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/>
</parent>

这就是为什么 Spring Boot 项目里很多依赖不写版本也能正常工作。不是版本消失了,而是被父 POM 或 BOM 管理起来了。

chapter 9:资源目录与编码配置

Maven 默认会把 src/main/resources 下的文件复制到 target/classes

例如:

1
src/main/resources/application.yml

构建后会变成:

1
target/classes/application.yml

如果需要自定义资源目录,可以配置 resources

1
2
3
4
5
6
7
8
9
10
11
12
13
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>

如果开启 filtering

1
<filtering>true</filtering>

Maven 会替换资源文件中的占位符,例如:

1
2
app.name=${project.artifactId}
app.version=${project.version}

构建后会被替换成当前项目的 artifactId 和 version。

但要小心:不是所有资源都适合 filtering。比如图片、字体、证书、二进制文件,如果被 Maven 当文本处理,可能会损坏。

中文乱码问题也很常见。建议在 POM 中统一配置编码:

1
2
3
4
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

如果是普通 Java 项目,还可以配置编译插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>17</release>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>

如果你还在使用 JDK 8,可以用:

1
2
<source>1.8</source>
<target>1.8</target>

JDK 9 之后更推荐使用 release,因为它不仅控制语法级别,还能限制可用 API,避免“本地能编译,低版本 JDK 运行报错”的尴尬局面。

chapter 10:测试与跳过测试

执行:

1
mvn test

Maven 会编译并运行测试代码。默认情况下,测试通常由 maven-surefire-plugin 执行。

打包时如果想跳过测试,常见有两种方式。

第一种:

1
mvn package -DskipTests

或者:

1
mvn package -DskipTests=true

它的特点是:

  • 不运行测试;
  • 但会编译测试代码。

第二种:

1
mvn package -Dmaven.test.skip=true

它的特点是:

  • 不运行测试;
  • 也不编译测试代码。

区别非常重要:

参数 是否编译测试代码 是否执行测试
-DskipTests
-Dmaven.test.skip=true

工程建议:

  1. 本地临时打包可以使用 -DskipTests
  2. 如果测试代码本身编译不过,而你只是想快速打包,可以使用 -Dmaven.test.skip=true
  3. CI/CD 主流水线不建议长期跳过测试;
  4. 如果测试太慢,应该区分单元测试和集成测试,而不是一刀切跳过;
  5. 如果某些测试不稳定,应该修复测试,而不是让跳过测试成为团队传统手艺。

也可以在 POM 中配置默认跳过测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<properties>
<skipTests>true</skipTests>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.6</version>
<configuration>
<skipTests>${skipTests}</skipTests>
</configuration>
</plugin>
</plugins>
</build>

如果某次想重新启用测试:

1
mvn test -DskipTests=false

chapter 11:普通 Java 项目如何打包

普通 Java 项目打包时,经常遇到一个问题:

1
打包成功了,但是 java -jar 运行报 ClassNotFoundException

原因通常是:你的 jar 包里没有把第三方依赖一起带上,或者 MANIFEST.MF 中没有正确声明 classpath。

普通 Java 项目一般有三种打包方式。

方式一:主 jar 与依赖 libs 分离

这种方式会生成:

1
2
3
4
5
target
├── app.jar
└── libs
├── commons-lang3-3.14.0.jar
└── other-lib.jar

配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<build>
<finalName>app</finalName>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>com.example.App</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>libs/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.11.0</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/libs</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

执行:

1
2
mvn clean package
java -jar target/app.jar

优点:

  • 依赖清晰;
  • 可以单独替换某些 jar;
  • 适合传统部署。

缺点:

  • 部署时必须保证 app.jarlibs 目录一起上传;
  • 少一个依赖就可能运行失败。

方式二:打成 fat jar

fat jar 就是把项目代码和依赖都打进一个大 jar。

可以使用 maven-shade-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.App</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

优点:

  • 一个 jar 就能运行;
  • 部署简单。

缺点:

  • jar 体积大;
  • 依赖冲突时排查更麻烦;
  • 可能需要处理资源文件合并问题。

方式三:Spring Boot 项目使用 spring-boot-maven-plugin

如果是 Spring Boot 项目,通常不需要自己手动配置 jar plugin 和 dependency plugin。

使用:

1
2
3
4
5
6
7
8
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

执行:

1
2
mvn clean package
java -jar target/demo.jar

Spring Boot Maven 插件会帮你完成 repackage,生成可执行 jar。

这也是 Spring Boot 项目推荐的方式:不要硬凹 Maven 原生 jar 配置,能交给 Boot 插件就交给 Boot 插件。专业的事交给专业的插件,程序员少熬一晚是一晚。

chapter 12:多模块项目中的 Maven 管理

当项目变大后,通常会拆成多模块:

1
2
3
4
5
6
7
8
9
10
mall
├── pom.xml
├── mall-common
│ └── pom.xml
├── mall-api
│ └── pom.xml
├── mall-service
│ └── pom.xml
└── mall-web
└── pom.xml

父工程的 pom.xml 通常使用 pom 打包:

1
<packaging>pom</packaging>

父工程声明模块:

1
2
3
4
5
6
<modules>
<module>mall-common</module>
<module>mall-api</module>
<module>mall-service</module>
<module>mall-web</module>
</modules>

子模块继承父工程:

1
2
3
4
5
6
7
<parent>
<groupId>com.example</groupId>
<artifactId>mall</artifactId>
<version>1.0.0</version>
</parent>

<artifactId>mall-common</artifactId>

父 POM 常用来做几件事:

  1. 统一 Java 版本;
  2. 统一依赖版本;
  3. 统一插件版本;
  4. 统一编码;
  5. 统一仓库;
  6. 统一打包规则;
  7. 管理模块之间的依赖关系。

推荐结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencyManagement>
<dependencies>
<!-- 统一管理依赖版本 -->
</dependencies>
</dependencyManagement>

<build>
<pluginManagement>
<plugins>
<!-- 统一管理插件版本 -->
</plugins>
</pluginManagement>
</build>

注意:

  • dependencyManagement 不会自动引入依赖;
  • pluginManagement 不会自动启用插件;
  • 子模块需要使用时,仍然要在自己的 dependenciesplugins 中声明;
  • 父工程版本要稳定,否则子模块发布会混乱;
  • 多模块项目中,模块之间依赖要避免循环引用。

chapter 13:常见 Maven 问题排查

1. mvn 命令无法识别

现象:

1
mvn: command not found

或:

1
'mvn' 不是内部或外部命令

排查:

  1. Maven 是否已下载并解压;
  2. 是否配置 MAVEN_HOME
  3. 是否把 %MAVEN_HOME%\bin$MAVEN_HOME/bin 加入 PATH;
  4. 是否重新打开终端;
  5. 执行 mvn -v 验证。

2. 依赖下载失败

常见原因:

  1. 网络问题;
  2. 镜像配置错误;
  3. 仓库地址不可用;
  4. 公司代理限制;
  5. 依赖版本不存在;
  6. 本地仓库缓存了失败文件。

常用命令:

1
mvn -U clean package

-U 表示强制更新 snapshot 和 release 依赖。

也可以删除本地仓库中对应依赖目录后重新下载。

3. 依赖冲突

现象:

1
2
3
NoSuchMethodError
ClassNotFoundException
ClassCastException

很可能是依赖版本冲突。

查看依赖树:

1
mvn dependency:tree

只看某个依赖:

1
mvn dependency:tree -Dincludes=org.slf4j

排除传递依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.example</groupId>
<artifactId>some-lib</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>

工程建议:

  • 直接使用的依赖,最好显式声明;
  • 不要完全依赖传递依赖;
  • 日志相关依赖尤其要小心;
  • Spring Boot 项目尽量尊重 Boot 的依赖管理,不要随便覆盖版本。

4. 打包成功但运行失败

如果执行:

1
java -jar app.jar

出现:

1
no main manifest attribute

说明 jar 里没有配置主类。

如果出现:

1
ClassNotFoundException

说明运行时找不到依赖。

解决思路:

  1. 普通 Java 项目配置 maven-jar-pluginmainClass
  2. 使用 maven-dependency-plugin 复制依赖;
  3. 或使用 maven-shade-plugin 打 fat jar;
  4. Spring Boot 项目使用 spring-boot-maven-plugin

5. JDK 版本不匹配

比如本地 JDK 17,项目要求 JDK 8,或者反过来,都可能出问题。

建议明确配置:

1
2
3
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>

或者:

1
2
3
4
5
6
7
8
9
10
11
12
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
</plugins>
</build>

不要让 Maven 猜你的 JDK 版本。它猜错的时候,背锅的一般是你。

chapter 14:我对 Maven 的工程实践建议

1. 不要在业务项目里到处写仓库地址

能放 settings.xml 的镜像配置,就不要散落在每个项目的 pom.xml 中。

项目 POM 应该描述项目本身,不应该承担过多个人环境配置。

2. 不要滥用 system scope

systemPath 看起来方便,但对团队协作和 CI/CD 非常不友好。

更好的方式是:

  1. 安装到本地仓库;
  2. 上传公司私服;
  3. 改造成标准 Maven 模块;
  4. 实在不行,也要写清楚依赖来源和导入方式。

3. Spring Boot 项目优先使用官方依赖管理

Spring Boot 已经帮你维护了一整套依赖版本组合。除非你明确知道为什么要覆盖,否则不要随便指定 starter 相关依赖版本。

4. 使用 dependency:tree 排查冲突

遇到依赖冲突,不要靠猜。

先执行:

1
mvn dependency:tree

再判断到底是谁引入了冲突版本。

“凭感觉改依赖”就像闭眼拆炸弹,刺激,但不推荐。

5. CI/CD 中不要长期跳过测试

本地为了快速打包,可以临时跳过测试。

但如果主干流水线长期跳过测试,项目质量迟早要还债,而且通常是上线当天连本带利一起还。

6. 多模块项目要收敛版本

父 POM 应该统一管理:

  1. Java 版本;
  2. 编码;
  3. 依赖版本;
  4. 插件版本;
  5. 仓库策略;
  6. 发布规则。

否则模块一多,版本就会变成“野生动物园”。

7. 构建配置要能被新人读懂

好的 pom.xml 不只是能跑,还应该能读。

建议保留必要注释,尤其是:

  1. 为什么要排除某个依赖;
  2. 为什么要覆盖某个版本;
  3. 为什么某个依赖使用 provided
  4. 为什么某个插件绑定到某个 phase;
  5. 为什么某个仓库必须存在。

这些注释不是写给 Maven 看的,是写给三个月后的自己看的。三个月后的自己通常什么都不记得,但很会骂人。

参考资料

  1. Apache Maven 官方文档:POM Reference
  2. Apache Maven 官方文档:Introduction to the Build Lifecycle
  3. Apache Maven 官方文档:Introduction to the Dependency Mechanism
  4. Apache Maven 官方文档:Settings Reference
  5. Apache Maven 官方文档:Introduction to Repositories
  6. Apache Maven 官方文档:Using Mirrors for Repositories
  7. Apache Maven Surefire Plugin:Skipping Tests
  8. Apache Maven Dependency Plugin:copy-dependencies
  9. NorthCastle:Maven 基础与进阶系列
  10. Maven 打包跳过测试的多种方式相关文章
  11. Maven POM、GAV、scope、生命周期、打包、资源目录、阿里云仓库配置等相关文章

启示录

Maven 的价值不只是“帮我们下载 jar 包”,而是把 Java 项目的构建过程标准化、可复制化、可维护化。

小项目里,Maven 让你少复制几个 jar。

大项目里,Maven 决定你的构建体系会不会失控。

真正掌握 Maven,不是背几个命令,而是理解:

  1. POM 描述项目;
  2. GAV 定位构件;
  3. 仓库保存构件;
  4. scope 控制依赖边界;
  5. 生命周期定义构建流程;
  6. 插件执行具体动作;
  7. 父 POM 和 BOM 统一工程规则。

技术越往后走,越会发现:能把项目跑起来只是第一步,能让项目稳定、清晰、可协作、可交付,才是工程能力。

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


Maven 从入门到工程实践:POM、仓库、生命周期、依赖与打包
https://allendericdalexander.github.io/2026/06/02/mvn/
作者
AtLuoFu
发布于
2026年6月2日
许可协议