这篇笔记用于梳理 Maven 的核心模型:标准目录结构、
pom.xml、依赖管理、构建生命周期、插件机制与多模块工程。它适合作为 Java 项目的 Maven 入门与回查手册,而不是覆盖所有冷门配置项的完整手册。
文章重点放在日常开发最常见的用法上,例如如何组织项目、如何声明依赖、什么时候改
settings.xml、为什么mvn clean package能完成一整套构建动作。像仓库部署细节、密码加密、企业私服治理等进阶主题这里只做必要提示。
参考资料:
Apache Maven - Introduction to the Standard Directory Layout
Apache Maven - Introduction to the POM
Apache Maven - Introduction to the Build Lifecycle
[TOC]
为什么需要 Maven
Maven 是 Java 生态里最常见的项目管理和构建工具之一。它解决的不是“怎么写代码”,而是“怎么把一个项目稳定地组织、构建、测试、打包和复用”。
如果没有 Maven,一个普通 Java 项目通常要手动处理这些问题:
- 目录怎么组织,源码、资源文件、测试代码放哪里。
- 第三方依赖从哪里下载,版本怎么管理。
- 编译、测试、打包、安装、发布分别用什么命令。
- 多个模块之间如何统一版本、共享配置、安排构建顺序。
Maven 的核心价值可以概括为三点:
- 约定统一的项目结构。
- 提供标准化的构建生命周期。
- 通过坐标和仓库机制管理依赖。
很多团队对 Maven 的第一感受是“配置很多”。但从工程化角度看,Maven 真正做的是把大量重复劳动收敛成统一规则。只要遵守约定,项目从本地开发到 CI 构建的行为就更容易保持一致。
Maven 标准目录结构
Maven 非常强调“约定大于配置”。只要项目遵守默认目录结构,很多配置都不需要显式声明。
一个典型的 Maven 项目默认如下:
1
2
3
4
5
6
7
8
9
10
my-app
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ ├── java
│ └── resources
└── target
各目录职责可以这样理解:
pom.xml:项目描述文件,定义坐标、依赖、插件和构建配置。src/main/java:业务源码。src/main/resources:主程序资源文件,例如application.yml、logback.xml。src/test/java:测试源码。src/test/resources:测试资源。target:构建输出目录,例如编译结果、测试报告、打包产物。
这个结构的好处不只是“看起来统一”,更关键的是 Maven 插件默认就认识这些目录。比如编译插件会默认编译 src/main/java,资源插件会默认复制 src/main/resources,测试插件会默认执行 src/test/java 下的测试。
如果目录结构随意改动,Maven 当然也能配置,但会失去默认约定带来的便利。
pom.xml 是什么
pom.xml 是 Maven 项目的核心配置文件。POM 是 Project Object Model 的缩写,可以把它理解成“项目的说明书 + 构建入口”。
一个最小可用的 POM 通常长这样:
1
2
3
4
5
6
7
8
9
10
<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>demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
</project>
这里最重要的是三组坐标:
groupId:组织或项目分组标识,通常使用反向域名风格。artifactId:当前制品名称,通常对应模块名。version:版本号。
这三者组合起来唯一标识一个 Maven 制品:
1
groupId:artifactId:version
例如:
1
com.example:demo:1.0.0-SNAPSHOT
除此之外,packaging 也很重要:
jar:默认值,常见于普通 Java 应用或类库。war:Web 应用。pom:聚合工程或父工程,通常不直接产出业务代码制品。
一个更贴近日常开发的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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>demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
</dependencies>
</project>
可以把 pom.xml 看成四层信息:
- 项目身份:谁,叫什么,什么版本。
- 依赖关系:项目依赖哪些库。
- 构建方式:使用哪些插件,打成什么包。
- 继承与聚合:是否有父工程,是否包含多个子模块。
pom.xml 里最常用的区域
日常开发里,真正高频修改的 pom.xml 通常集中在下面这些区域:
properties:统一维护 Java 版本、编码、依赖版本、插件参数。dependencies:声明当前模块真正需要的依赖。dependencyManagement:统一约束依赖版本,常见于父工程或 BOM 导入。build:配置插件、产物名、资源处理、测试行为。profiles:按环境切换构建参数。repositories/pluginRepositories:补充依赖仓库或插件仓库。distributionManagement:定义制品发布到哪个远端仓库。
可以把它理解成一句话:dependencies 决定“项目需要什么”,build 决定“项目怎么构建”,distributionManagement 决定“项目往哪里发”。
properties 的价值
properties 最适合存放跨插件、跨依赖都会复用的变量,例如:
1
2
3
4
5
6
<properties>
<java.version>21</java.version>
<maven.compiler.release>${java.version}</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<revision>1.0.0-SNAPSHOT</revision>
</properties>
好处主要有三个:
- 版本集中管理,升级时不会满文件搜索替换。
- 插件和依赖可以共用同一份属性。
- 多模块工程里,父
pom的属性可以统一下发。
对于企业项目,properties 往往还是版本治理的第一层入口。
Spring Boot 项目里的 pom 怎么用
在普通 Java 项目里,pom.xml 已经很重要;在 Spring Boot 项目里,它通常还是整个应用的构建中枢。因为 Spring Boot 不只是依赖管理方便,它还把可执行 JAR、镜像构建、资源过滤、测试和插件约定都整合到了 Maven 体系里。
最常见的 Spring Boot pom
一个典型的 Spring Boot Java 应用通常会这样写:
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
40
41
42
43
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>demo project for Spring Boot</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这种写法常见,是因为 spring-boot-starter-parent 已经帮项目做好了很多默认配置:
- 管理大量常见依赖版本。
- 预置
repackage相关执行。 - 统一 Java 编译参数、编码和常用插件配置。
- 让很多 starter 可以省略
<version>。
对于大多数 Spring Boot 单体应用、后台服务、接口服务来说,这都是最省心的起点。
spring-boot-starter-parent 和 BOM 的区别
Spring Boot 项目最容易混淆的一点,是“继承 parent”和“导入 BOM”不是同一件事。
方案一:继承 spring-boot-starter-parent
这是最省事的方式,适合没有额外父 pom 约束的项目。
优点:
- 依赖版本直接托管。
- 常见插件配置和
repackage已经准备好。 - 新项目最容易落地。
限制:
- 一个 Maven 模块只能有一个父
pom。 - 如果公司已经有统一父
pom,就不能再直接继承 Boot 的 parent。
方案二:导入 spring-boot-dependencies
如果项目必须继承公司自己的父 pom,更常见的做法是导入 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.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
这样做的效果是:
- 仍然可以省略很多 Spring Boot 依赖的版本号。
- 但不会自动获得
spring-boot-starter-parent带来的插件管理和部分默认配置。
因此使用 BOM 时,通常还要自己补齐编译插件、测试插件和 spring-boot-maven-plugin 配置。
Spring Boot 项目里常见的 pom 约定
对于 Java 应用来说,下面这些约定非常常见:
packaging通常是jar。- Web 项目多数直接打成可执行 JAR,而不是传统 WAR。
- 依赖优先使用 Boot starter,而不是手工拼一堆 Spring 模块。
- 应用入口通常由
spring-boot-maven-plugin参与处理,而不是单独依赖maven-jar-plugin手写清单。
如果是需要部署到外部 Tomcat 的项目,才会更多考虑 war 打包和 provided 的容器依赖。
Spring Boot 打包与运行
Spring Boot 项目之所以能做到 java -jar app.jar 直接启动,关键就在 spring-boot-maven-plugin。
spring-boot-maven-plugin 做了什么
这个插件最核心的目标是 repackage。它会在 Maven package 阶段生成一个可执行归档,把应用代码和依赖重新组织成 Spring Boot 能直接启动的结构。
常见配置如下:
1
2
3
4
5
6
7
8
9
10
11
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.DemoApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
执行:
1
mvn clean package
通常会在 target/ 下得到一个可执行 JAR,然后可以直接运行:
1
java -jar target/demo-0.0.1-SNAPSHOT.jar
如果 mainClass 不写,插件通常会自动寻找合适的启动类;但在多入口项目里,显式指定会更稳妥。
package 之后发生了什么
在 Spring Boot 应用里,mvn package 往往不只是“普通 Maven 打一个 JAR”:
- 先由普通打包过程生成原始 JAR。
- 再由
spring-boot-maven-plugin的repackage目标重写成可执行 JAR。 - 原始产物可能会以
.original的形式保留。
因此,Spring Boot 项目里看到 xxx.jar.original 是正常现象。
spring-boot:run 和 java -jar 的区别
开发时常见两种运行方式:
1
mvn spring-boot:run
1
java -jar target/demo-0.0.1-SNAPSHOT.jar
它们的区别可以概括为:
spring-boot:run:更适合开发期快速启动,不一定依赖最终产物。java -jar:更接近生产运行方式,依赖已经打好的包。
如果要验证“最终包能不能跑”,一定要以 mvn package 后再 java -jar 为准。
Spring Boot 项目常见打包插件组合
Boot 应用除了 spring-boot-maven-plugin,还常和这些插件配合:
maven-compiler-plugin:控制 Java 编译版本。maven-surefire-plugin:执行单元测试。maven-failsafe-plugin:执行集成测试。maven-resources-plugin:资源过滤与复制。maven-jar-plugin:需要额外控制普通 JAR 行为时使用。
如果项目要打 Docker 镜像,Spring Boot 还支持通过 Maven 插件生成 OCI 镜像;但对多数团队来说,先把可执行 JAR 构建稳定,通常是更核心的一步。
JAR 和 WAR 怎么选
对于 Spring Boot Java 应用,优先选择 jar 往往更简单:
- 部署方式统一,
java -jar即可启动。 - 容器内嵌,不依赖外部 Tomcat。
- 更适合微服务和容器化部署。
只有在下面场景里,war 才更常见:
- 公司基础设施要求部署到统一的外部 Servlet 容器。
- 需要兼容历史应用服务器环境。
这时通常要把内嵌容器改成 provided,例如:
1
2
3
4
5
6
7
<packaging>war</packaging>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
编译、测试、打包时最常见的 pom 配置
编译参数
Java 版本通常通过属性统一管理:
1
2
3
4
<properties>
<java.version>21</java.version>
<maven.compiler.release>${java.version}</maven.compiler.release>
</properties>
如果不走 Spring Boot parent,也可以显式声明编译插件:
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.14.0</version>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
</plugins>
</build>
测试插件
多数项目默认只需要 Surefire 执行单元测试:
1
2
3
4
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
如果项目把集成测试拆到 integration-test / verify 阶段,则会再加 maven-failsafe-plugin。
这类拆分在 Spring Boot 项目里很常见,尤其是涉及数据库、消息队列、外部服务联调时。
跳过测试的两个参数
这是 Maven 使用中非常容易混淆、但又非常常见的点:
1
mvn clean package -DskipTests
1
mvn clean package -Dmaven.test.skip=true
区别在于:
-DskipTests:跳过测试执行,但通常仍会编译测试代码。-Dmaven.test.skip=true:连测试编译也一起跳过。
日常 CI 或本地临时提速,更常用 -DskipTests;只有明确不需要测试代码参与构建时,才考虑 maven.test.skip。
产物名称和资源过滤
如果希望打出的文件名更明确,可以用:
1
2
3
<build>
<finalName>demo-app</finalName>
</build>
生成产物就会更接近:
1
target/demo-app.jar
资源过滤则常用于把 Maven 属性写进配置文件:
1
2
3
4
5
6
7
8
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
但 Spring Boot 项目要特别注意:application.properties 和 application.yml 自己就支持 ${...} 占位符,因此如果启用 Maven 过滤,通常要明确占位符策略,避免和 Spring 的占位符语法混淆。
发布到远端仓库
如果项目不仅是“应用服务”,还是要给别的项目复用的公共模块、SDK、starter、组件包,那么 Maven 的发布流程就非常关键。
install 和 deploy 的实际区别
mvn install:发布到本机本地仓库,只能当前机器使用。mvn deploy:发布到远端仓库,团队其他人或 CI 环境都可以拉取。
因此:
- 业务应用项目本地验证时,通常到
install就够了。 - 组件、公共库、基础框架模块要共享时,就需要
deploy。
distributionManagement 的作用
项目要发布到哪里,通常在 pom.xml 中通过 distributionManagement 声明:
1
2
3
4
5
6
7
8
9
10
<distributionManagement>
<repository>
<id>company-releases</id>
<url>https://repo.example.com/maven/releases</url>
</repository>
<snapshotRepository>
<id>company-snapshots</id>
<url>https://repo.example.com/maven/snapshots</url>
</snapshotRepository>
</distributionManagement>
这里的规则通常是:
1.0.0这类正式版发布到repository。1.0.1-SNAPSHOT这类快照版发布到snapshotRepository。
为什么账号密码不写在 pom.xml
仓库认证信息应放在用户本地 settings.xml 中,而不是提交到 Git 的 pom.xml 中。原因很简单:
pom.xml是项目配置,应该可共享。- 用户名和密码是环境敏感信息,不应该进版本库。
对应配置一般像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<settings>
<servers>
<server>
<id>company-releases</id>
<username>deploy-user</username>
<password>deploy-password</password>
</server>
<server>
<id>company-snapshots</id>
<username>deploy-user</username>
<password>deploy-password</password>
</server>
</servers>
</settings>
这里最关键的一点是:settings.xml 里 server.id 必须和 distributionManagement 里的 repository.id、snapshotRepository.id 对应起来,否则 Maven 找不到认证信息。
发布命令
最常见的发布命令就是:
1
mvn clean deploy
它会:
- 清理旧产物。
- 编译、测试、打包。
- 安装到本地仓库。
- 上传到远端仓库。
对于 CI 环境,这是非常典型的一条流水线命令。
Spring Boot 应用要不要发布到远端仓库
这取决于项目类型:
- 如果是可独立部署的业务应用,很多团队只需要产出 JAR、Docker 镜像或发布制品到制品库,不一定给别人作为 Maven 依赖使用。
- 如果是公共 starter、内部 SDK、工具包、自动配置模块,那就非常适合发布到公司私服,供其他 Spring Boot 项目复用。
换句话说,“Spring Boot 应用”不一定要 deploy 到 Maven 私服,但“Spring Boot 生态里的公共模块”往往非常需要。
企业里常见的仓库实践
在真实团队里,通常不会让每个项目直接连各种外网仓库,而是通过统一私服或仓库管理器处理:
- 下载时通过镜像或虚拟仓库代理外部依赖。
- 上传时区分
releases和snapshots。 - 权限和审计由 Nexus、Artifactory、Azure Artifacts 等统一管理。
对于大型组织,还会把下载和上传地址更多地下沉到 settings.xml 统一管理,而不是散落在每个项目 pom.xml 中。
Spring Boot 项目里几个很值钱的 Maven 习惯
1. 优先让版本收口到父 pom 或 BOM
不要在各个模块里到处手写版本号。尤其是 Spring Boot 项目,版本一旦分散,升级时很容易出现兼容性问题。
2. 应用项目和公共库项目分开思考
- 应用项目重点是“能编译、能打包、能运行、能发布部署”。
- 公共库项目重点是“版本稳定、依赖清晰、可复用、可发布到私服”。
两者都用 Maven,但 pom 的重心并不完全一样。
3. 区分 Maven Profile 和 Spring Profile
这是另一个非常常见的混淆点:
- Maven Profile:控制构建过程,例如打包参数、仓库、资源过滤。
- Spring Profile:控制应用运行时配置,例如
dev、test、prod。
它们名字相近,但不在同一层面。不要把运行环境切换全部塞进 Maven Profile,也不要试图用 Spring Profile 代替构建配置。
4. 不要随意覆盖 Spring Boot 管理的依赖版本
Spring Boot 的 BOM 价值在于它管理了一组经过验证的依赖组合。手工覆盖版本当然可以,但每覆盖一处,都意味着要自己承担兼容性验证成本。
5. 让构建命令尽量标准化
对于 Spring Boot Java 应用,最常用也最值得团队统一的命令通常是:
1
2
3
4
mvn clean test
mvn clean package
mvn clean package -DskipTests
mvn clean deploy
构建命令越统一,本地、测试环境和 CI 的行为就越容易保持一致。
依赖管理
依赖管理是 Maven 最核心的能力之一。
在 pom.xml 中声明依赖后,Maven 会根据依赖坐标从仓库中下载对应制品,并自动处理传递依赖。也就是说,A 依赖 B,B 又依赖 C,那么项目通常只需要声明 A,Maven 会把 B、C 一并解析出来。
例如:
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.5.0</version>
</dependency>
这类 starter 背后通常会继续引入日志、JSON、Web 容器等一整套依赖,开发者不需要手动逐个拼接。
常见依赖作用域
Maven 常用的依赖作用域有四个:
| scope | 说明 | 常见场景 |
|---|---|---|
compile |
默认作用域,编译、测试、运行都可见 | 普通业务依赖 |
provided |
编译和测试时需要,运行时由 JDK 或容器提供 | servlet-api |
runtime |
编译时不需要,运行或测试时需要 | JDBC 驱动 |
test |
仅测试代码可见 | JUnit、Mockito |
示例:
1
2
3
4
5
6
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.12.2</version>
<scope>test</scope>
</dependency>
1
2
3
4
5
6
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.3.0</version>
<scope>runtime</scope>
</dependency>
1
2
3
4
5
6
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
基础组件包里常见的 optional
你提到的“基础组件包做好了,应用项目需要时再引入才生效”的那个关键字,很多时候就是:
1
<optional>true</optional>
它表示当前依赖对“当前模块”是可用的,但不会作为传递依赖继续传给下游项目。
例如,团队里做了一个基础组件包 base-component,它内部依赖了某个短信 SDK:
1
2
3
4
5
6
<dependency>
<groupId>com.example</groupId>
<artifactId>sms-sdk</artifactId>
<version>1.0.0</version>
<optional>true</optional>
</dependency>
这时如果业务应用只依赖:
1
2
3
4
5
<dependency>
<groupId>com.example</groupId>
<artifactId>base-component</artifactId>
<version>1.0.0</version>
</dependency>
sms-sdk 不会自动被带进来。只有业务项目自己显式再声明一次,才真正进入它的类路径。
这个特性特别适合下面几类场景:
- 基础组件里支持多个可选能力,但不想强推给所有业务应用。
- 某些能力依赖体积大、初始化重、外部配置多,不适合默认带上。
- 组件作者只想提供扩展点,不想强绑定某个第三方实现。
optional 和 scope 不是一回事
这也是一个很容易混淆的点:
scope决定依赖在当前项目的哪些 classpath 生效。optional决定依赖是否继续向下游项目传播。
所以:
runtime、provided、test是“当前项目怎么用”。optional是“下游项目要不要自动继承”。
Spring Boot 组件化里常见的做法
在 Spring Boot 生态里,基础组件包通常不只是一个普通 JAR,而会进一步拆成更清晰的结构:
xxx-core:放通用代码、接口、工具类。xxx-autoconfigure:放自动配置。xxx-starter:给业务项目引用的入口依赖。
业务项目一般不会直接手动引很多底层模块,而是引入 starter:
1
2
3
4
5
<dependency>
<groupId>com.example</groupId>
<artifactId>message-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
这类 starter 的价值在于:
- 统一依赖组合,避免每个项目自己拼装。
- 自动装配开箱即用。
- 可以通过条件装配做到“引入即生效,不引入就完全无影响”。
从工程实践上说,optional 更像“控制传递依赖扩散”,starter 更像“给业务方一个稳定的引入入口”。两者经常一起出现。
传递依赖与排除依赖
传递依赖虽然省事,但也可能带来版本冲突或不需要的库。此时可以使用 exclusions 做精确排除:
1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.5.0</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
日常排查依赖问题时,最常用的命令是:
1
mvn dependency:tree
它可以直接展示依赖树,是分析冲突、确认传递路径时最有效的入口之一。
dependencyManagement 和 dependencies 的区别
很多人第一次接触多模块项目时,容易把这两个标签混淆。
dependencies:真正引入依赖。dependencyManagement:只做版本和规则约束,不会自动引入依赖。
也就是说,在父工程里写:
1
2
3
4
5
6
7
8
9
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.12.2</version>
</dependency>
</dependencies>
</dependencyManagement>
子模块仍然需要显式声明:
1
2
3
4
5
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
区别在于这里不必再重复写版本号。
仓库与本地缓存
Maven 的依赖下载依赖于仓库系统,通常会涉及三类仓库:
- 本地仓库:默认位于
~/.m2/repository。 - 中央仓库:默认公共仓库,很多开源库都发布在这里。
- 私服或镜像仓库:团队内部常用,用来做缓存、权限控制和制品托管。
当项目第一次构建时,Maven 会把依赖下载到本地仓库;后续如果本地已有匹配版本,通常就直接复用,不需要重复下载。
开发中常见的两个认知点:
- 本地仓库不是项目目录的一部分,而是当前用户机器上的共享缓存。
-SNAPSHOT版本表示开发中的快照版本,和正式发布版本的更新策略不同。
settings.xml 该怎么理解
settings.xml 是 Maven 的运行环境配置文件,它和 pom.xml 的职责不同。
pom.xml关注项目本身,适合提交到版本库。settings.xml关注当前机器或当前用户的构建环境,通常不应该随项目分发。
常见位置有两个:
- 全局配置:
${maven.home}/conf/settings.xml - 用户配置:
${user.home}/.m2/settings.xml
如果两个文件同时存在,用户级配置会覆盖同名的全局配置。
最常见的配置项
日常开发里最常改的通常不是所有顶级元素,而是下面几类:
localRepository:修改本地仓库位置。mirrors:配置镜像仓库。servers:配置访问远程仓库所需的账号密码。proxies:配置代理。profiles/activeProfiles:按环境切换仓库、属性或插件仓库。
一个常见的用户配置示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
<localRepository>/Users/your-name/.m2/repository</localRepository>
<mirrors>
<mirror>
<id>aliyunmaven</id>
<name>aliyun maven</name>
<url>https://maven.aliyun.com/repository/public</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
</settings>
这里需要注意两点:
- 镜像是“替代访问某个仓库的入口”,不是额外附加一个下载来源。
- 账号密码这类敏感信息应该放在
settings.xml的servers里,而不是直接写在项目pom.xml中。
profiles 的作用
profiles 常用于按环境切换一些构建配置,例如:
- 不同仓库地址。
- 不同属性值。
- 不同插件仓库。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<profiles>
<profile>
<id>company-repo</id>
<repositories>
<repository>
<id>internal-releases</id>
<url>https://repo.example.com/maven/releases</url>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>company-repo</activeProfile>
</activeProfiles>
如果是项目级环境切换,很多时候更适合把 profile 写在 pom.xml;如果是机器或用户相关的网络、认证和仓库访问配置,则更适合写在 settings.xml。
构建生命周期
Maven 的另一条主线是生命周期。很多命令看起来像独立指令,实际执行的是生命周期中的某个阶段。
Maven 内置三个生命周期:
default:负责编译、测试、打包、安装、部署。clean:负责清理构建产物。site:负责生成项目站点文档。
最常见的 default 生命周期阶段
日常最常碰到的是下面这些 phase:
validate:校验项目是否完整。compile:编译主代码。test:运行单元测试。package:打包,例如生成 JAR 或 WAR。verify:执行额外校验,通常包含集成测试后的检查。install:安装到本地仓库。deploy:发布到远程仓库。
理解 Maven 命令最重要的一点是:执行某个 phase,会连同它之前的 phase 一起执行。
例如:
1
mvn package
这并不只是“打包”这一步,而是会依次完成 validate -> compile -> test -> package。
再比如:
1
mvn clean install
这条命令会先执行 clean 生命周期清理 target,再执行 default 生命周期直到 install。
phase、plugin、goal 的关系
这三个概念很容易混淆,可以这样理解:
- lifecycle:一条完整流程。
- phase:流程中的阶段。
- goal:插件真正执行的具体任务。
因此,mvn package 的含义不是“调用一个叫 package 的二进制命令”,而是“让 Maven 运行到 package 这个阶段,并执行绑定到该阶段的 goal”。
插件机制
Maven 本身更像一个调度框架,真正干活的大多数能力都来自插件。
例如:
- 编译通常由
maven-compiler-plugin完成。 - 测试通常由
maven-surefire-plugin完成。 - 打包 JAR 通常由
maven-jar-plugin完成。 - 清理
target通常由maven-clean-plugin完成。
如果只是使用 Maven 默认行为,很多基础插件甚至不需要手写声明;但当需要定制参数、绑定额外 goal 或锁定插件版本时,就应该在 build/plugins 中显式配置。
例如,使用 maven-shade-plugin 生成可执行的 fat 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
<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.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
这里的关键点是:shade 这个 goal 被绑定到了 package 阶段,所以执行 mvn package 时会自动触发。
常用命令
如果把 Maven 当作日常工具来使用,最值得记住的是下面这些命令:
1
2
3
4
5
6
7
mvn --version
mvn clean
mvn test
mvn package
mvn verify
mvn install
mvn dependency:tree
它们分别对应的用途大致如下:
mvn --version:检查 Maven 和 Java 环境。mvn clean:删除target。mvn test:运行单元测试。mvn package:打包。mvn verify:执行更完整的验证流程。mvn install:安装到本地仓库,供其他本地项目依赖。mvn dependency:tree:查看依赖树。
常用命令参数
1
2
3
4
mvn clean package -DskipTests
mvn clean package -Pprod
mvn -o verify
mvn -X test
-Dxxx=yyy:传递属性,例如-DskipTests。-PprofileId:激活指定 profile。-o:离线模式。-X:输出调试日志。-e:显示异常堆栈。-U:强制检查远程 SNAPSHOT 更新。
创建项目的推荐方式
较新的 Maven 使用中,更推荐用 archetype 生成项目骨架:
1
2
3
4
5
6
mvn archetype:generate \
-DgroupId=com.example \
-DartifactId=my-app \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.5 \
-DinteractiveMode=false
相比一些旧资料里常见的 archetype:create、mvn eclipse:eclipse、mvn idea:idea 等命令,这种方式更贴近现在的 Maven 使用习惯。IDE 工程通常直接由 IntelliJ IDEA 或 Eclipse 导入 pom.xml 即可,不再需要依赖旧式插件生成工程文件。
多模块工程
当项目越来越大时,把一个工程拆成多个模块通常比维护一个巨大的单体模块更清晰。
一个典型的多模块 Maven 工程可能长这样:
1
2
3
4
5
6
7
8
multi-module-demo
├── pom.xml
├── common
│ └── pom.xml
├── service
│ └── pom.xml
└── web
└── pom.xml
这里面通常会出现两个容易混淆的角色:
- 父工程:负责统一版本、插件、依赖管理。
- 聚合工程:负责声明
<modules>,统一构建多个子模块。
很多项目里这两个角色会放在同一个根 pom.xml 中。
根工程示例
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
<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>multi-module-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>common</module>
<module>service</module>
<module>web</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.12.2</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
子模块继承父工程后,就可以共享:
- 项目版本。
- 插件版本。
- 公共依赖版本。
- 编译参数、编码配置等通用属性。
子模块示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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>
<parent>
<groupId>com.example</groupId>
<artifactId>multi-module-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>service</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
在根目录执行:
1
mvn clean package
Maven 会按照模块依赖关系和聚合顺序统一完成整个工程的构建。
公司项目里最常见的父子 pom / 聚合工程模板
在真实团队里,多模块 Maven 工程通常不会只停留在“会拆模块”这一层,而是会进一步形成比较稳定的模板。
最常见的结构一般长这样:
1
2
3
4
5
6
7
8
9
10
company-demo
├── pom.xml
├── common-core
│ └── pom.xml
├── common-spring-boot-starter
│ └── pom.xml
├── biz-service
│ └── pom.xml
└── biz-web
└── pom.xml
模板一:根 pom 同时做聚合与父工程
这是中小型项目里最常见的方案,一个根 pom.xml 同时承担两件事:
- 作为父工程,统一版本、插件、依赖管理。
- 作为聚合工程,声明
<modules>统一构建。
示例:
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<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>company-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>common-core</module>
<module>common-spring-boot-starter</module>
<module>biz-service</module>
<module>biz-web</module>
</modules>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.5.0</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
这种方式的优点是简单直接,团队上手成本低。大多数公司内部项目其实用这一种就够了。
模板二:单独拆出 parent 模块
如果模块很多,或者一个父工程要被多个聚合工程复用,常见做法是把 parent 单独拆出来:
1
2
3
4
5
6
7
8
company-platform
├── pom.xml
├── company-parent
│ └── pom.xml
├── order-service
│ └── pom.xml
└── user-service
└── pom.xml
这时:
- 根
pom主要负责聚合。 company-parent/pom.xml主要负责依赖版本、插件版本和通用属性。
根聚合工程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>company-platform</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>company-parent</module>
<module>order-service</module>
<module>user-service</module>
</modules>
</project>
子模块继承 parent:
1
2
3
4
5
6
<parent>
<groupId>com.example</groupId>
<artifactId>company-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../company-parent/pom.xml</relativePath>
</parent>
这种方式更适合平台化团队、基础架构团队,或者多个业务线共享一套 Maven 规范的场景。
公司父 pom 里通常放什么
企业里的父 pom 一般会集中放这些内容:
properties:Java 版本、编码、统一版本号。dependencyManagement:Spring Boot BOM、数据库驱动、日志、测试框架版本。pluginManagement:编译、测试、打包、代码检查插件版本。- 通用仓库声明或 profile。
- 通用发布配置约定。
但通常不会在父 pom 的 dependencies 里塞太多业务依赖。因为父 pom 更适合做“约束”和“默认值”,不是把所有模块都绑死。
子模块的常见分层
实际项目里,子模块一般会分成几类:
common-*:公共工具、基础能力、领域共享模型。*-starter:给业务应用引用的启动器模块。*-service:业务服务层。*-web/*-api:接口层或启动模块。
其中最需要注意的是:启动模块和公共库模块不要混在一起。
公共库模块应该尽量:
- 依赖少。
- 不带太重的运行时绑定。
- 方便复用和发布。
启动模块则可以更偏向“把依赖、配置、自动装配和打包方式拼完整”。
一个比较实用的子模块模板
如果是 Spring Boot 业务启动模块,pom 往往长这样:
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
40
<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>
<parent>
<groupId>com.example</groupId>
<artifactId>company-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>biz-web</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>biz-service</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>common-spring-boot-starter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这个模板的思路很稳定:
- 父
pom负责统一版本。 - 子模块只声明自己真正要用的依赖。
- 启动模块负责最终打包。
Spring Boot 多环境打包、profile、资源过滤和配置注入的坑点
这一块在公司项目里非常常见,而且最容易出现“本地没问题,线上出问题”的情况。
先区分三件事
讨论多环境时,至少要先分清下面三件事:
- Maven Profile:构建期概念,控制打包参数、资源过滤、依赖组合。
- Spring Profile:运行期概念,控制应用读哪套配置。
- 外部化配置:部署期概念,控制真正上线时从哪里拿配置。
很多问题,本质上都是把这三层混成了一层。
常见做法示例
一个 Maven Profile 配置大致可能这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<profiles>
<profile>
<id>dev</id>
<properties>
<env.name>dev</env.name>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<env.name>prod</env.name>
</properties>
</profile>
</profiles>
打包时:
1
mvn clean package -Pprod
资源文件里如果启用了 Maven 过滤,可能会写:
1
2
app:
env: @env.name@
而 Spring Boot 运行期常见的激活方式则是:
1
java -jar app.jar --spring.profiles.active=prod
这两者并不是一回事。前者影响“包里写进什么”,后者影响“运行时启用哪套 Spring 配置”。
坑一:把运行环境完全写死在打包阶段
很多项目喜欢打:
1
mvn clean package -Pprod
然后把 application-prod.yml 的行为通过 Maven 过滤直接固化进包里。这样短期看很省事,但问题很多:
- 一个环境一个包,产物变多。
- 同一份代码在不同环境无法保证完全同包。
- 回滚和排障会更麻烦。
更稳妥的做法通常是:
- 尽量构建“一份通用包”。
- 环境差异放到外部配置或运行参数。
也就是说,Maven Profile 更适合处理构建差异,不适合承载所有环境差异。
坑二:Maven 资源过滤和 Spring 占位符冲突
Spring Boot 配置文件天然支持:
1
2
3
spring:
datasource:
url: ${DB_URL:jdbc:mysql://127.0.0.1:3306/demo}
而 Maven 资源过滤也会处理占位符。如果直接对 application.yml 开启默认过滤,很容易把 Spring 的 ${...} 当成 Maven 变量去解析,最终造成:
- 本地能跑,打包后配置被改坏。
- 占位符被提前替换成空值。
- 某些运行时变量失效。
比较稳妥的做法通常有两个:
- 不对
application.yml/application.properties做大范围过滤。 - 如果必须过滤,改用
@...@这类和 Spring 占位符区分开的分隔符。
例如:
1
2
app:
version: @project.version@
这样就比直接混用 ${...} 安全得多。
坑三:把敏感配置打进包里
很多团队一开始会把下面这些信息直接通过资源过滤写进包里:
- 数据库密码。
- 第三方密钥。
- 内网地址。
- 不同环境的账号信息。
这样的问题很明显:
- 包一旦泄露,敏感信息就跟着泄露。
- 同一应用不同环境必须重新打包。
- 配置轮换和密钥更新会很痛苦。
生产环境更推荐的做法是:
- 密钥放配置中心、环境变量或密钥管理系统。
- 包里只保留默认值或占位符。
- 运行时注入真正的环境信息。
坑四:Maven Profile 名称和 Spring Profile 名称完全绑定
例如团队里定义:
- Maven
dev、test、prod - Spring 也叫
dev、test、prod
这样看起来统一,但很容易在流程里造成误导,让人误以为:
1
mvn clean package -Pprod
就等价于:
1
--spring.profiles.active=prod
实际上不是。
更清晰的习惯通常是:
- Maven Profile 用于构建语义,例如
ci、release、with-docker。 - Spring Profile 用于运行环境语义,例如
dev、test、prod。
这样职责分层会更清楚。
坑五:多模块项目里各模块各自配一套 profile
多模块工程一旦每个模块都自己写 profiles、自己搞资源过滤,很快就会出现:
- 某些模块用
dev,某些模块用local。 - 占位符命名不统一。
- 构建链条里参数无法贯通。
更好的方式通常是:
- 父
pom统一定义 profile 和公共属性。 - 子模块只做很小范围的补充。
- 统一环境变量名、统一过滤规则、统一打包命令。
一个比较稳妥的实践思路
对于 Spring Boot 公司项目,更推荐的思路通常是:
- Maven 负责把代码稳定编译、测试、打包。
- 包尽量环境无关,只保留少量构建元信息。
- Spring Profile 决定运行时加载哪套配置。
- 真正的环境差异尽量放到配置中心、环境变量或启动参数。
如果一定要在包里写一点构建信息,最常见也最安全的是这类内容:
- 应用版本。
- Git 提交号。
- 构建时间。
- 构建环境标识。
而不是数据库账号、密钥或完整环境地址。
一些容易混淆的点
1. pom.xml 和 settings.xml 不是一回事
pom.xml描述项目。settings.xml描述当前机器或当前用户的构建环境。
如果一个配置是“任何人拉下这个项目都应该一致”,优先考虑写进 pom.xml;如果一个配置和个人网络、认证、私服地址相关,更适合写进 settings.xml。
2. package、install、deploy 的层次不同
package:只是产出构建包。install:把产物放入本地仓库。deploy:把产物发布到远程仓库。
本地调试通常到 package 或 install 就够了,deploy 更常见于 CI/CD。
3. dependencyManagement 不等于真正引入依赖
它更像“版本对齐规则表”,不是“自动依赖清单”。
4. target 一般不提交到版本库
它是构建输出目录,通常应加入 .gitignore。
小结
Maven 的学习曲线看起来来自 XML 和概念较多,但真正高频的内容其实并不复杂。
只要抓住下面这条主线,很多配置都会自然清晰起来:
- 用标准目录结构组织项目。
- 用
pom.xml描述项目身份、依赖和构建方式。 - 用生命周期驱动编译、测试和打包。
- 用插件扩展构建能力。
- 用父子模块和聚合工程管理大型项目。
- 用
settings.xml处理机器相关的仓库、代理和认证配置。
对于日常开发来说,能熟练理解 pom.xml、依赖作用域、mvn clean package、dependencyManagement、多模块继承与聚合,已经足以覆盖绝大多数 Maven 场景。