Maven详解

从项目结构、POM、依赖管理到生命周期的 Maven 实用笔记

Posted by Ekko on February 9, 2021

这篇笔记用于梳理 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

Apache Maven - Settings Reference

Apache Maven - Maven Dependencies

[TOC]


为什么需要 Maven

Maven 是 Java 生态里最常见的项目管理和构建工具之一。它解决的不是“怎么写代码”,而是“怎么把一个项目稳定地组织、构建、测试、打包和复用”。

如果没有 Maven,一个普通 Java 项目通常要手动处理这些问题:

  • 目录怎么组织,源码、资源文件、测试代码放哪里。
  • 第三方依赖从哪里下载,版本怎么管理。
  • 编译、测试、打包、安装、发布分别用什么命令。
  • 多个模块之间如何统一版本、共享配置、安排构建顺序。

Maven 的核心价值可以概括为三点:

  1. 约定统一的项目结构。
  2. 提供标准化的构建生命周期。
  3. 通过坐标和仓库机制管理依赖。

很多团队对 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.ymllogback.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”:

  1. 先由普通打包过程生成原始 JAR。
  2. 再由 spring-boot-maven-pluginrepackage 目标重写成可执行 JAR。
  3. 原始产物可能会以 .original 的形式保留。

因此,Spring Boot 项目里看到 xxx.jar.original 是正常现象。

spring-boot:runjava -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.propertiesapplication.yml 自己就支持 ${...} 占位符,因此如果启用 Maven 过滤,通常要明确占位符策略,避免和 Spring 的占位符语法混淆。


发布到远端仓库

如果项目不仅是“应用服务”,还是要给别的项目复用的公共模块、SDK、starter、组件包,那么 Maven 的发布流程就非常关键。

installdeploy 的实际区别

  • 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.xmlserver.id 必须和 distributionManagement 里的 repository.idsnapshotRepository.id 对应起来,否则 Maven 找不到认证信息。

发布命令

最常见的发布命令就是:

1
mvn clean deploy

它会:

  1. 清理旧产物。
  2. 编译、测试、打包。
  3. 安装到本地仓库。
  4. 上传到远端仓库。

对于 CI 环境,这是非常典型的一条流水线命令。

Spring Boot 应用要不要发布到远端仓库

这取决于项目类型:

  • 如果是可独立部署的业务应用,很多团队只需要产出 JAR、Docker 镜像或发布制品到制品库,不一定给别人作为 Maven 依赖使用。
  • 如果是公共 starter、内部 SDK、工具包、自动配置模块,那就非常适合发布到公司私服,供其他 Spring Boot 项目复用。

换句话说,“Spring Boot 应用”不一定要 deploy 到 Maven 私服,但“Spring Boot 生态里的公共模块”往往非常需要。

企业里常见的仓库实践

在真实团队里,通常不会让每个项目直接连各种外网仓库,而是通过统一私服或仓库管理器处理:

  • 下载时通过镜像或虚拟仓库代理外部依赖。
  • 上传时区分 releasessnapshots
  • 权限和审计由 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:控制应用运行时配置,例如 devtestprod

它们名字相近,但不在同一层面。不要把运行环境切换全部塞进 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 不会自动被带进来。只有业务项目自己显式再声明一次,才真正进入它的类路径。

这个特性特别适合下面几类场景:

  • 基础组件里支持多个可选能力,但不想强推给所有业务应用。
  • 某些能力依赖体积大、初始化重、外部配置多,不适合默认带上。
  • 组件作者只想提供扩展点,不想强绑定某个第三方实现。

optionalscope 不是一回事

这也是一个很容易混淆的点:

  • scope 决定依赖在当前项目的哪些 classpath 生效。
  • optional 决定依赖是否继续向下游项目传播。

所以:

  • runtimeprovidedtest 是“当前项目怎么用”。
  • 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

它可以直接展示依赖树,是分析冲突、确认传递路径时最有效的入口之一。

dependencyManagementdependencies 的区别

很多人第一次接触多模块项目时,容易把这两个标签混淆。

  • 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 会把依赖下载到本地仓库;后续如果本地已有匹配版本,通常就直接复用,不需要重复下载。

开发中常见的两个认知点:

  1. 本地仓库不是项目目录的一部分,而是当前用户机器上的共享缓存。
  2. -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.xmlservers 里,而不是直接写在项目 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:createmvn eclipse:eclipsemvn 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。
  • 通用发布配置约定。

但通常不会在父 pomdependencies 里塞太多业务依赖。因为父 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 变量去解析,最终造成:

  • 本地能跑,打包后配置被改坏。
  • 占位符被提前替换成空值。
  • 某些运行时变量失效。

比较稳妥的做法通常有两个:

  1. 不对 application.yml / application.properties 做大范围过滤。
  2. 如果必须过滤,改用 @...@ 这类和 Spring 占位符区分开的分隔符。

例如:

1
2
app:
  version: @project.version@

这样就比直接混用 ${...} 安全得多。

坑三:把敏感配置打进包里

很多团队一开始会把下面这些信息直接通过资源过滤写进包里:

  • 数据库密码。
  • 第三方密钥。
  • 内网地址。
  • 不同环境的账号信息。

这样的问题很明显:

  • 包一旦泄露,敏感信息就跟着泄露。
  • 同一应用不同环境必须重新打包。
  • 配置轮换和密钥更新会很痛苦。

生产环境更推荐的做法是:

  • 密钥放配置中心、环境变量或密钥管理系统。
  • 包里只保留默认值或占位符。
  • 运行时注入真正的环境信息。

坑四:Maven Profile 名称和 Spring Profile 名称完全绑定

例如团队里定义:

  • Maven devtestprod
  • Spring 也叫 devtestprod

这样看起来统一,但很容易在流程里造成误导,让人误以为:

1
mvn clean package -Pprod

就等价于:

1
--spring.profiles.active=prod

实际上不是。

更清晰的习惯通常是:

  • Maven Profile 用于构建语义,例如 cireleasewith-docker
  • Spring Profile 用于运行环境语义,例如 devtestprod

这样职责分层会更清楚。

坑五:多模块项目里各模块各自配一套 profile

多模块工程一旦每个模块都自己写 profiles、自己搞资源过滤,很快就会出现:

  • 某些模块用 dev,某些模块用 local
  • 占位符命名不统一。
  • 构建链条里参数无法贯通。

更好的方式通常是:

  • pom 统一定义 profile 和公共属性。
  • 子模块只做很小范围的补充。
  • 统一环境变量名、统一过滤规则、统一打包命令。

一个比较稳妥的实践思路

对于 Spring Boot 公司项目,更推荐的思路通常是:

  1. Maven 负责把代码稳定编译、测试、打包。
  2. 包尽量环境无关,只保留少量构建元信息。
  3. Spring Profile 决定运行时加载哪套配置。
  4. 真正的环境差异尽量放到配置中心、环境变量或启动参数。

如果一定要在包里写一点构建信息,最常见也最安全的是这类内容:

  • 应用版本。
  • Git 提交号。
  • 构建时间。
  • 构建环境标识。

而不是数据库账号、密钥或完整环境地址。


一些容易混淆的点

1. pom.xmlsettings.xml 不是一回事

  • pom.xml 描述项目。
  • settings.xml 描述当前机器或当前用户的构建环境。

如果一个配置是“任何人拉下这个项目都应该一致”,优先考虑写进 pom.xml;如果一个配置和个人网络、认证、私服地址相关,更适合写进 settings.xml

2. packageinstalldeploy 的层次不同

  • package:只是产出构建包。
  • install:把产物放入本地仓库。
  • deploy:把产物发布到远程仓库。

本地调试通常到 packageinstall 就够了,deploy 更常见于 CI/CD。

3. dependencyManagement 不等于真正引入依赖

它更像“版本对齐规则表”,不是“自动依赖清单”。

4. target 一般不提交到版本库

它是构建输出目录,通常应加入 .gitignore


小结

Maven 的学习曲线看起来来自 XML 和概念较多,但真正高频的内容其实并不复杂。

只要抓住下面这条主线,很多配置都会自然清晰起来:

  • 用标准目录结构组织项目。
  • pom.xml 描述项目身份、依赖和构建方式。
  • 用生命周期驱动编译、测试和打包。
  • 用插件扩展构建能力。
  • 用父子模块和聚合工程管理大型项目。
  • settings.xml 处理机器相关的仓库、代理和认证配置。

对于日常开发来说,能熟练理解 pom.xml、依赖作用域、mvn clean packagedependencyManagement、多模块继承与聚合,已经足以覆盖绝大多数 Maven 场景。