Maven = Java 构建世界的“事实标准”:从 pom.xml 到云原生 CI/CD

为什么 2025 年了你还是绕不开 Maven?

先问你一个直球问题:

你有多少次,在 IDEA 里点了一下绿色小三角,应用就跑起来了,

从来没认真想过:这一切到底是谁在背后"打工"?

再问三个更扎心一点的:

  • 你知道项目里的这些 jar 包是 从哪儿来的 吗?

  • 你知道 Jenkins / GitHub Actions 里那条 mvn clean package -DskipTests每个单词都在干嘛吗?

  • 你有没有遇到过"我改了一个依赖版本,结果线上全挂了,但我也说不清楚为啥"的场景?

如果你写的是 Java / Spring Boot,答案八成和一个东西有关:

Maven。

很多人对 Maven 的印象还停留在:

  • "那不就是 IDEA 自动帮我生成的那个 pom.xml 吗?"

  • "反正有报错就搜一下,把那段 <dependency> 复制进去就行了"

  • "构建慢?那就 -DskipTests 呗"

但现实是 ------ 在 2025 年这个 云原生 + CI/CD 已经成为默认配置 的时代,只要你:

  • 想搞清楚"本地跑得好好的,为啥 CI 上就过不了"

  • 想搞定"多模块大项目 + 私服 + 环境划分"的那堆坑

  • 想让自己的项目"稍微有点工程味儿"而不是"到处 copy 的 demo"

你就绕不过 Maven。

Maven 已经不只是一个"老牌构建工具",而是 Java 世界的事实标准基础设施

  • 所有依赖都用它的"坐标系统"来命名

  • 大多数开源框架的 README,第一个给你的就是 Maven 的 <dependency> 片段

  • 绝大部分企业 Java 项目的 CI/CD pipeline 底层,跑的都是 Maven 命令

所以,这篇文章的目标很简单:

不只是教你"会写 pom.xml",

而是帮你建立一张 清晰的 Maven 心智地图

1)它在 Java 生态里是什么角色?

2)它整体是怎么工作的?

3)pom.xml 每一块大概在干嘛?

这样以后你再遇到 Maven 问题,

就不是"到处百度复制粘贴",而是真正知道自己在动哪根神经。

什么是构建工具?Maven 在 Java 生态里的位置

先别急着看 pom.xml,我们先把问题退回到最原始的那个:

"写完代码,到底要经过哪些步骤,才能变成一个'可以上线的东西'?"

粗略列一下(不同语言略有差异,但套路差不多):

1)编译 / 转换

  • Java:javac.java 编成 .class,再打成 .jar / .war

  • TypeScript:tsc.ts.js

  • Go:go build 编成二进制

2)依赖管理

  • 把各种三方库搞到本地

  • 把 classpath 配好

3)测试

  • 单元测试

  • 集成测试

4)打包

  • 把编译结果 + 资源文件,打成 jar / war / 镜像 / zip

5)发布 / 部署

  • 上传到制品库(Nexus / Artifactory)

  • 部署到测试环境 / 生产环境 / K8s

如果完全靠你手动做,可能是这样:

bash 复制代码
javac -cp 一大坨路径  xxx.java
jar cvf xxx.jar ...
scp xxx.jar some-server
ssh some-server 'java -jar xxx.jar'

项目一旦稍微大一点,模块多一点,依赖多一点,环境多一点,就会变成:

  • 命令很难记

  • 很难保证所有人、所有环境都用同一套命令

  • CI/CD 上的脚本和本地完全是两套逻辑

  • 很难把"构建过程"变成可维护、可讨论的东西

构建工具的出现,就是为了解决这类问题。

一句话版定义

构建工具(Build Tool)= 把"源代码 + 依赖 + 配置"自动变成"可运行 / 可发布产物"的那套标准化流程和工具集合。

Maven 做的事,主要有三件:

1)构建自动化

  • 把编译、测试、打包、安装、发布这整条链路标准化

  • 所有人都用同一套命令:mvn clean packagemvn install

2)依赖管理

  • 通过 GAV 坐标(groupId/artifactId/version),从仓库拉取依赖

  • 处理传递依赖 & 冲突

3)项目结构与生命周期规范

  • 统一目录结构:src/main/javasrc/test/java ...

  • 统一"构建阶段"的定义:compiletestpackageinstalldeploy

  • 用插件(Plugin)在不同阶段插入不同的行为

Maven 在 Java 生态里的"地位"

简单排个时间线:

  • Ant:早期 Java 构建工具,类似"XML 版脚本语言"

  • Maven:引入 POM、生命周期、依赖管理,统一了一大堆"大家各玩各的"的东西

  • Gradle:基于 Groovy/Kotlin DSL,更灵活,性能更好,Android 领域几乎一统天下

但如果聚焦在"传统 Java / Spring Boot / 企业应用"这个大盘上,会看到一个现实:

Maven 依然是事实上的标准:

1)会 Maven 是 Java 后端工程师的"默认技能"

2)Gradle 是"加分项"和某些团队的主选项

原因也很现实:

  • 大量老项目和框架都是 Maven 起步的

  • 企业内部的私服、CI/CD、规范文档全部是 Maven 思维

  • 绝大多数开源 Java 库,文档里的第一段依赖示例代码就是 Maven <dependency>

所以,如果你把 Java 技术栈想象成一座城市:

JDK / JVM 是地下基础设施;

Spring 全家桶是城里的建筑群;

Maven 更像是"城市的物流系统"

  • 负责把各种依赖和构建产物运来运去,

  • 把这些东西规范地摆放在正确的位置,

  • CI/CD 工具(Jenkins / GitHub Actions / GitLab CI)都要跟它打交道。

先画一张心智图:Maven 整体工作流程长什么样?

很多人一开始学 Maven,是从各种 XML 配置、命令、插件开始的,结果越学越乱。

其实最好的方式,是先在脑子里画一张 "Maven 怎么工作"的总流程图

我们先给一个简化版:

bash 复制代码
源代码 + pom.xml
        ↓
   解析 POM(parent + super POM + profiles + properties)
        ↓
   解析依赖(构建依赖树,解决版本 & 冲突)
        ↓
   组装生命周期(Lifecycle)和每个阶段要执行的插件 Goal
        ↓
执行你输入的命令(比如 mvn clean package):
  clean: 删除 target
  compile: 编译 Java
  test: 执行单元测试
  package: 打包成 jar/war
  install: 安装到本地仓库 (~/.m2/repository)
  deploy: 部署到远程仓库 (Nexus/Artifactory)
        ↓
  得到产物(jar/war/docker 镜像等),交给后面的部署系统

把它拆开说,就是这么几步:

第一步:解析 POM(Project Object Model)

Maven 看任何项目,第一件事就是读 pom.xml

1)读当前项目的 pom.xml → 生成一个 Project Model

2)看是否有 <parent>,如果有:

  • 找到父 POM(本地或仓库里)

  • 把父 POM 的配置和当前 POM 合并(类似"继承 + 覆盖")

3)在最顶上,还有一个 Maven 内置的 Super POM

  • 里面有默认的仓库(比如 Maven Central)

  • 默认的插件绑定等

4)最终得到一个"合并后的 POM",也就是:effective POM

你可以用:

bash 复制代码
mvn help:effective-pom

来看看 Maven 最终"理解到的那份 POM"长什么样。

第二步:解析依赖,生成依赖树

拿到 effective POM 之后,Maven 会:

1)根据 <dependencies> 中的条目,一个个去仓库查找 POM

2)每个依赖也有自己的 dependencies,于是递归展开,形成一棵 依赖树

3)在这棵树上应用各种规则:

  • scope:compile/test/provided/runtime

  • optional:要不要被传递

  • exclusions:排除某些依赖

  • 冲突时用"就近优先 / 先声明优先"等策略解决版本

最终,Maven 得到:

  • 编译时 classpath

  • 测试时 classpath

  • 运行时 classpath

这些 classpath,后面都会被各种插件(compile/test/package)使用。

第三步:组装生命周期(Lifecycle)和插件执行计划

Maven 有三套内置生命周期:

  • clean

  • default(最重要)

  • site

我们最常用的是 default 这条链,它内部有一串 Phase:

bash 复制代码
validate → compile → test → package → verify → install → deploy

然后,Maven 会做这么一件事:

1)看当前项目的 <packaging>jarwar 还是 pom

2)根据 packaging 类型,加载默认的"插件绑定表"

  • 比如:compile 阶段默认会绑定 maven-compiler-plugin:compile

  • test 阶段绑定 maven-surefire-plugin:test

3)再把你在 <build><plugins> / <build><pluginManagement> 里写的配置叠加进去

4)得到一张"执行计划表":

  • 每个 Phase,要调用哪些插件、这些插件的哪些 Goal,以及这些 Goal 的配置参数

所以,当你敲下 mvn package 的时候,其实是在说:

"请执行从 validatepackage 的每一个 Phase,

每个 Phase 把你已经计划好的所有插件 Goal 都跑一遍。"

第四步:执行命令,产生构建产物

举个常见命令:

bash 复制代码
mvn clean package

Maven 会:

1)执行 clean 生命周期:删除 target 目录

2)执行 default 生命周期中,直到 package 为止的所有 Phase:

  • validate:校验项目结构

  • compile:编译 src/main/java

  • test:编译 & 运行测试(使用 surefire 插件)

  • package:打包 jarwar(使用 jar/war 插件或 spring-boot 插件重打包)

如果你再加一个:

bash 复制代码
mvn clean install

还会多做一步:

install:把打出来的构件放入本地仓库 ~/.m2/repository

  • 方便后续被其他项目 / 模块直接通过 GAV 坐标来依赖

再进一步 mvn deploy,就是把构建好的产物发到远程仓库(比如公司私服),给别的项目/环境用。

GAV 身份证:Maven 如何唯一标识一个构件?

前面我们说到,"依赖管理"的核心在于:你得有一套能唯一标识每个构件的命名系统

在 Maven 里,这套系统就是 GAV:

XML 复制代码
<groupId>com.example</groupId>
<artifactId>demo-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

你可以把它理解成一个"身份证":

  • groupId:姓氏 + 家族

  • artifactId:名字(在这个家族内部的唯一名字)

  • version:这个构件的版本

  • packaging:它是"什么类型"的东西(jar/war/pom/...)

groupId:你从哪来?

groupId 一般用 反转域名 风格:

  • org.springframework.boot

  • com.fasterxml.jackson.core

  • io.micrometer

它的意义是:

  • 标识"这是哪个组织 / 团队 / 公司"的构件

  • 在仓库里,也会反映到目录结构上,比如:

bash 复制代码
~/.m2/repository/
  org/springframework/boot/spring-boot-starter-web/3.3.1/...

团队内部起 groupId 时,一般会:

  • 先定公司层级:com.company

  • 再加业务线:com.company.paymentcom.company.order

  • 再往下才是 artifactId 来区分具体服务/模块

artifactId:你叫什么?

artifactId 就是构件在 groupId 内部的"名字":

  • spring-boot-starter-web

  • spring-boot-starter-actuator

  • spring-context

  • logback-classic

同一个 groupId 下,artifactId 必须唯一

在仓库目录结构里,它就是 groupId 下面的下一层目录名。

version:你是哪个版本?

版本号就更直观了:

  • 1.0.01.0.12.0.0 ...

  • 也有 1.0.0-SNAPSHOT 这种正在开发中的版本

Maven 不关心你版本号语义是不是严格"语义化版本控制(SemVer)",

它非常关心 version 这个字符串本身,因为:

  • 依赖解析时,GAV 必须一起才能锁定一个具体构件

  • 本地仓库里,version 对应目录的那一层:

bash 复制代码
~/.m2/repository/com/example/demo-app/1.0.0/demo-app-1.0.0.jar

packaging:你是什么类型的构件?

常见值:

  • jar:普通库 / 应用 jar

  • war:Web 应用(传统 Servlet 容器部署)

  • pom:纯 POM 项目(一般用作父 POM / 聚合工程)

  • 其他:earzip 等也可以

packaging 会影响很多东西:

1)哪个插件负责 package 阶段

  • jarmaven-jar-plugin

  • warmaven-war-plugin

2)有哪些默认的 lifecycle 绑定

3)在仓库里的文件名后缀是什么(.jar or .war

一张"小结图":Maven 坐标系长什么样?

你可以用这样一串路径来帮助自己记忆:

复制代码
groupId           → 目录层级(org/springframework/boot)
artifactId        → 目录名(spring-boot-starter-web)
version           → 版本目录(3.3.1)
packaging + GAV   → 文件名(spring-boot-starter-web-3.3.1.jar)

整合在一起,就是:

复制代码
~/.m2/repository/org/springframework/boot/spring-boot-starter-web/3.3.1/
    spring-boot-starter-web-3.3.1.jar
    spring-boot-starter-web-3.3.1.pom

一旦你接受了"所有依赖都是通过 GAV 来定位"的这个设定,

Maven 里很多看起来很"玄学"的东西都会变得非常顺理成章。

pom.xml 拆解一遍:从"项目名片"到"构建大脑"

很多人一打开 pom.xml,第一反应是:

"好长一坨 XML,看两行就想关掉。"

但如果你把它当成一个 结构化的"项目说明书 + 构建脚本",再慢慢拆,就会发现其实很有章法:

顶部:项目是谁(名片)

中间:项目需要什么(依赖 + 仓库)

底部:项目怎么被构建(build + plugins + profiles)

这一节,我们用一个精简版示例,从上到下拆一遍。

顶部:项目的"身份证"和血统

先看最上面这一块:

XML 复制代码
<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.3.1</version>
        <relativePath/>
    </parent>

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

这一块基本是 Maven 看项目的第一印象

modelVersion:POM 模型版本,几乎永远是 4.0.0

parent:说明这份 POM 继承自谁

  • Spring Boot 通常继承 spring-boot-starter-parent

  • 你自己的多模块项目,会继承公司自定义的 parent POM;

groupId / artifactId / version / packaging:上一节讲过,就是 GAV 坐标 + 类型。

你可以这么理解:

  • parent 决定了你的"家族"(继承谁的规则、依赖版本规范、插件默认配置)

  • groupId/artifactId/version/packaging 决定了你"自己是谁、要打成什么东西"

在 Maven 构建流程中,这一块会被先解析出来,用来:

  • 拼接出仓库里的路径和文件名(GAV)

  • 决定使用哪套默认 lifecycle / 插件绑定(由 packaging 决定)

  • 决定往上要再加载哪些父 POM(parent

项目信息区:更多是"介绍文字",但也很有用

继续往下,你会看到类似这样的内容:

XML 复制代码
<name>demo-app</name>
<description>A demo Spring Boot application.</description>
<url>https://example.com/demo-app</url>

<licenses>
    <license>
        <name>Apache License, Version 2.0</name>
        <url>https://www.apache.org/licenses/LICENSE-2.0</url>
    </license>
</licenses>

<developers>
    <developer>
        <id>renda</id>
        <name>Renda Zhang</name>
        <email>renda@example.com</email>
    </developer>
</developers>

<scm>
    <connection>scm:git:https://github.com/example/demo-app.git</connection>
    <url>https://github.com/example/demo-app</url>
</scm>

这一块更多是元数据(Metadata)

对构建结果没有直接影响

但会:

  • 出现在仓库 UI 上;

  • 出现在生成的 Maven site 文档里;

  • 对开源项目尤其重要(别人一看 POM 就知道你是谁、代码在哪里)。

在公司内部项目里,你可以视情况简化,但最起码:

  • name / description 写清楚;

  • scm 标一下 Git 仓库地址;

以后你在 CI/CD、制品库、SonarQube、项目门户里打开这个构件时,这些信息会让你少很多"这是谁的服务?哪里改代码?"的困惑。

properties:POM 里的"全局变量系统"

再往下,通常会有一段 <properties>

XML 复制代码
<properties>
    <java.version>17</java.version>
    <spring.boot.version>3.3.1</spring.boot.version>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>${java.version}</maven.compiler.source>
    <maven.compiler.target>${java.version}</maven.compiler.target>
</properties>

这里有几个关键点:

1)<properties> 定义的是 "POM 层面的变量" ,可以在整份 POM 中通过 ${xxx} 使用;

2)Maven 解析 effective POM 时,会做一次 属性插值(interpolation)

  • ${java.version} 替换成实际值;

  • 也支持内置属性,比如 ${project.artifactId}${project.version} 等;

3)属性可以被:

  • 父 POM 覆盖子 POM;

  • profile 覆盖默认属性;

  • 命令行 -Dxxx=yyy 覆盖 POM 中定义的属性。

这带来几个好处:

  • 统一版本 :比如所有地方都用 ${java.version},以后改版本只改一处

  • 简化配置 :插件配置 / 资源过滤 / 打包名字都可以引用 ${project.*}

  • 方便按环境调整 :配合 <profiles>,不同环境可以用不同的属性值(例如数据库地址、打包标签等)

心智模型:

properties = POM 级别的 "config center";

${} = 把这些配置注入到具体的插件参数 / 依赖版本 / 资源文件里。

dependencies vs dependencyManagement:谁是真依赖,谁是"版本规范表"?

这一块是很多人一开始最迷糊的地方。

先看两个块:

XML 复制代码
<!-- 版本规范表:不会直接进 classpath -->
<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>

        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>1.5.5.Final</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<!-- 真正参与构建的依赖清单 -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
    </dependency>
</dependencies>

区别一句话总结:

dependencies = 真正会进 classpath 的依赖

dependencyManagement = 只定义"默认版本和配置"的规范表

更细一点说:

dependencies 里的每一条依赖,都会被 Maven 加入依赖树;

  • 如果你不写 version,Maven 会去所有 parent + 当前 POM 的 dependencyManagement 里找;

dependencyManagement 不会主动把依赖加进 classpath,它只负责"当别人引用我时,应该用哪个版本、什么配置"。

典型用法:

在父 POM 的 dependencyManagement 里统一规定版本:

  • Spring Boot 的 BOM 就是这么干的;

  • 你公司内部也可以搞一个自己的 "依赖版本 BOM";

在子模块里,只写:

XML 复制代码
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
</dependency>

版本自动从 parent 的 dependencyManagement 里继承。

为什么要这么设计?

  • 多模块项目里,如果每个模块都自己写版本,很快就乱套;

  • BOM + dependencyManagement 让版本管理变成"中央统一控制", 子模块只负责"你要不要用这个依赖",而不是"你决定它用哪个版本"。

build:构建行为发生在这里(插件是核心)

来到 build 区块:

XML 复制代码
<build>
    <finalName>${project.artifactId}-${project.version}</finalName>

    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
        </resource>
    </resources>

    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>

    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
        </plugin>

        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <executions>
                <execution>
                    <id>repackage</id>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

可以拆成几个小块看:

finalName:产物的"文件名"
XML 复制代码
<finalName>${project.artifactId}-${project.version}</finalName>

决定了 target/ 里打包文件的基本名:

  • demo-app-1.0.0.jar

  • demo-app-1.0.0.war

配合 CI/CD 时,很多 pipeline 就是通过这个名字去找制品的。

resources / testResources:哪些目录算"资源文件"
XML 复制代码
<resources>
    <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
    </resource>
</resources>

Maven 会把这里列出的目录里的内容,复制到最终打包的 classes 路径下;

filtering=true 表示:

  • 会对资源文件里的 ${xxx} 做属性替换(用的就是 <properties> 和内置属性);

  • 典型用法:application-${app.env}.yaml 里写占位符,打包时替换成 profile 对应的值。

testResources 同理,只是作用域是测试。

pluginManagement vs plugins:又一组"规范表 vs 实际使用"

这和 dependencyManagement / dependencies 的关系非常像:

<pluginManagement>

  • 定义"推荐/统一的插件版本和配置";

  • 不会自动执行

<plugins>

  • 这里列出的插件才会真正绑定到生命周期里参与执行;

  • 如果没有写版本,就去 pluginManagement 里找。

举个例子,父 POM:

XML 复制代码
<build>
  <pluginManagement>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
        <configuration>...</configuration>
      </plugin>
    </plugins>
  </pluginManagement>
</build>

子模块只需要:

XML 复制代码
<build>
  <plugins>
    <plugin>
      <artifactId>maven-compiler-plugin</artifactId>
    </plugin>
  </plugins>
</build>

版本和配置会自动从父 POM 的 pluginManagement 里继承下来。

底层机制:

Maven 组装 lifecycle 的时候,会:

1)先加载 packaging 决定的默认插件绑定;

2)再用 pluginManagement 的配置"覆盖默认值、加上统一配置";

3)最后看 <plugins> 里实际启用了哪些插件,并进行更精细的覆盖和绑定。

仓库与发布:repositories / pluginRepositories / distributionManagement

这一块是 Maven 和"外部世界"的接口:

XML 复制代码
<repositories>
    <repository>
        <id>aliyun-maven</id>
        <url>https://maven.aliyun.com/repository/public</url>
    </repository>
</repositories>

<pluginRepositories>
    <pluginRepository>
        <id>central-plugins</id>
        <url>https://repo.maven.apache.org/maven2</url>
    </pluginRepository>
</pluginRepositories>

<distributionManagement>
    <repository>
        <id>releases-repo</id>
        <url>https://repo.example.com/maven/releases</url>
    </repository>
    <snapshotRepository>
        <id>snapshots-repo</id>
        <url>https://repo.example.com/maven/snapshots</url>
    </snapshotRepository>
</distributionManagement>

repositories

  • 用来拉 普通依赖 的远程仓库;

  • 默认已经有 Maven Central,一般只需要额外加公司私服 / 镜像(有些会放到 settings.xml 里)。

pluginRepositories

  • 专门用来拉 插件 JAR 的仓库,一般较少改。

distributionManagement

  • 决定 mvn deploy 要把构建结果 发到哪个远程仓库

  • 通常对应公司的 Nexus / Artifactory 的 release / snapshot 仓库。

心里要有的一点:

repositories 决定"依赖从哪拉 ",distributionManagement 决定"产物往哪推"。

profiles:一份 POM 装下多套"配置世界线"

最后一个非常关键但经常被忽略的区域:

XML 复制代码
<profiles>
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <app.env>dev</app.env>
        </properties>
    </profile>

    <profile>
        <id>prod</id>
        <properties>
            <app.env>prod</app.env>
        </properties>
        <build>
            <resources>
                <resource>
                    <directory>src/main/resources-prod</directory>
                    <filtering>true</filtering>
                </resource>
            </resources>
        </build>
    </profile>
</profiles>

你可以把 <profile> 理解成:

一块可以"按条件叠加上去的配置 patch"。

它可以里边放:

  • properties:环境变量

  • dependencies:某些环境才需要的组件

  • build:不同环境用不同资源目录 / 插件配置

  • repositories:某些环境用不同仓库

激活方式:

  • <activeByDefault>true</activeByDefault>:默认生效

  • 命令行:mvn clean package -Pprod

  • 根据环境变量 / JDK 版本 / 操作系统等条件激活(<activation>里可以写)

在 Maven 底层:

1)先读取所有 profile;

2)判断哪些 profile 被激活;

3)把它们的配置合并到主 POM 的模型里;

4)再走"依赖解析 + lifecycle 组装"的那套流程。

小结:再看 pom.xml,就按这 7 个区块读

以后你再看到一份 pom.xml,可以按下面顺序扫一遍:

1)顶层身份区modelVersion + parent + GAV + packaging

  • 这是谁?继承谁?打成什么?

2)项目信息区name / description / scm / developers

  • 这是谁写的?代码在哪?

3)属性区properties

  • 有哪些"全局变量"?后面 ${} 都从这里翻

4)依赖区dependencyManagement vs dependencies

  • 统一版本是怎么管的?真正用到哪些依赖?

5)构建区build + pluginManagement + plugins + resources

  • 编译 / 测试 / 打包的插件是谁?怎么配置?

6)仓库 & 发布区repositories / pluginRepositories / distributionManagement

  • 依赖从哪拉?产物往哪发?

7)Profile 区profiles

  • 有几套"世界线"?不同环境下 POM 其实长什么样?

把这 7 块吃透之后,pom.xml 就不再是"看不懂的 XML 垃圾堆",而是一个:

可以被你读懂、修改、复用、抽象成模板的"构建大脑"。

依赖管理的真实世界:传递依赖、scope 与冲突调解

讲 Maven,绕不开三个字:依赖地狱

你可能已经遇到过这些场景:

  • 引了个 A 依赖,结果多出一堆莫名其妙的 jar

  • 改了一个依赖版本,线上日志开始疯狂报 NoSuchMethodError

  • 本地跑得好好的,换个同事的电脑就编译不过

这些现象背后,基本都绕着三个关键词打转:

传递依赖、scope、冲突调解。

传递依赖:你只写了 A,B/C/D 也跟着进来了

最朴素的例子:

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

你只写了一个 starter,但实际上:

spring-boot-starter-web 自己又依赖了:

  • Spring MVC、Jackson、Tomcat/Netty、Validation、日志组件......

它们又各自依赖别的库。

最终,Maven 会生成一棵这样的依赖树(简化版):

bash 复制代码
spring-boot-starter-web
 ├─ spring-boot-starter
 ├─ spring-boot-starter-json
 │   ├─ jackson-databind
 │   ├─ jackson-core
 │   └─ jackson-annotations
 ├─ spring-webmvc
 └─ spring-boot-starter-tomcat
     ├─ tomcat-embed-core
     └─ ...

这就是 传递依赖(Transitive Dependencies)

你依赖 A,A 依赖 B,B 依赖 C,最终 B / C 都会加进你的 classpath。

好处:

  • 你不用手写一大堆依赖

  • 维护起来轻松很多(尤其是 Spring 这种"套娃式"框架)

问题:

  • 版本冲突(同一个库的不同版本)

  • 引进来一堆你没意识到的东西(安全、体积、兼容问题)

scope:依赖"在哪些阶段"生效?

Maven 的依赖 scope 本质上是在回答:

"这个依赖,在哪些生命周期阶段需要?在哪些 classpath 里出现?"

最常用的几个:

1)compile(默认)

  • 编译 + 测试 + 运行 都需要

  • 比如 Spring 框架本体、你自己的业务公共模块等

2)test

  • 只在测试阶段需要

  • 典型:JUnit、Mockito、Spring Boot 的 test starter

  • 不会打进最终产物

3)provided

  • 编译需要,但运行时由"外部环境"提供

  • 典型:javax.servlet-api(老式 war 部署时由容器提供)、lombok(只在编译生成代码)

  • 如果你把 provided 的东西打进可执行 jar 里,通常是"不合适"的设计

4)runtime

  • 运行时需要,但编译时不一定需要

  • 典型:JDBC 驱动

  • 编译时只依赖 JDBC 接口,运行时才需要具体实现

你可以用一张表来记:

scope 编译 测试编译 测试运行 打包产物 备注
compile 默认
test 只在 test classpath
provided 运行时由外部环境提供
runtime 只在运行期、测试运行期需要

冲突调解:同一个库不同版本,谁说了算?

再来看一个经典大坑:

  • 依赖 A 用 log4j:2.20.0

  • 依赖 B 用 log4j:2.17.0

  • 你项目里同时引了 A 和 B

最后 classpath 里到底是哪个版本?

Maven 的默认策略可以简化理解为两条:

1)"最短路径优先"(就近优先)

2)在同一深度上,"先声明者优先"

用依赖树来看:

bash 复制代码
你的项目
 ├─ A (依赖 log4j:2.20.0)
 └─ B (依赖 log4j:2.17.0)

如果 A/B 都是直接依赖(同一深度),那由谁先写在 <dependencies> 里决定:

XML 复制代码
<dependencies>
    <!-- 先声明 A,A 带的 log4j 更"占理" -->
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>A</artifactId>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>B</artifactId>
    </dependency>
</dependencies>

要是嵌套更深一点:

bash 复制代码
你的项目
 ├─ A (依赖 log4j:2.20.0)
 │   └─ log4j:2.20.0
 └─ C
     └─ B (依赖 log4j:2.17.0)
         └─ log4j:2.17.0

通常会选路径更短、距离根更近的那个版本。

现实工程里一般怎么做?

不依赖"玄学默认规则",而是 把版本"拉到明面上"统一管理

1)在父 POM 的 dependencyManagement 里写死版本:

XML 复制代码
<dependencyManagement>
   <dependencies>
       <dependency>
           <groupId>org.apache.logging.log4j</groupId>
           <artifactId>log4j-core</artifactId>
           <version>2.20.0</version>
       </dependency>
   </dependencies>
</dependencyManagement>

2)在子模块里,如果 A/B 各自带了不同版本的 log4j,可以通过 <exclusions> 排掉:

XML 复制代码
<dependency>
   <groupId>com.example</groupId>
   <artifactId>B</artifactId>
   <exclusions>
       <exclusion>
           <groupId>org.apache.logging.log4j</groupId>
           <artifactId>log4j-core</artifactId>
       </exclusion>
   </exclusions>
</dependency>

3)真正要用的版本,由你自己的 POM 决定,而不是让第三方库"绑架"你。

BOM:把"版本地狱"变成"版本清单"

Spring Boot 的做法特别值得学:

XML 复制代码
<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>

这个 spring-boot-dependencies 就是一个 BOM(Bill of Materials)

  • 它本身是个 POM,里面列了一大堆常用依赖的"推荐版本"

  • 你项目里只要 import 了它,就可以在 <dependencies> 里不写 version

好处:

  • Spring 官方帮你选好"一整套配套兼容的版本"

  • 你不容易因为自己乱配版本把 Spring 家族玩崩

  • 自己公司也可以学这个套路,搞一个内部 BOM 来管理统一依赖版本

仓库体系:本地仓库 / 远程仓库 / Maven Central / 私服

Maven 世界里,依赖和构件都被统一放在"仓库(Repository)"里。

你可以把它想象成一个多层级图书馆系统

1)你电脑上的小书架(本地仓库);

2)公司楼下的阅览室(私服);

3)城市里的公共图书馆(Maven Central)。

本地仓库:~/.m2/repository

第一次你执行 mvn clean package 时,可能会看到 Maven 疯狂打印:

  • Downloading from central: ...

这些 jar 下完之后,都被放到了一个固定地方:

  • Windows:C:\Users\<你用户名>\.m2\repository

  • macOS / Linux:~/.m2/repository

Maven 的基本策略是:

1)优先查本地仓库

2)找不到才去远程仓库拉;

3)拉回来后,缓存到本地仓库,后面再用就很快。

好处:

  • 减少网络请求;

  • 不同项目之间共享同一份依赖;

  • CI 机器也会有自己的本地仓库,配合 Maven 缓存可以大幅加速构建。

远程仓库:公共仓库 + 公司私服

远程仓库简单分两大类:

1)公共仓库(公网可访问)

  • 典型代表:Maven Central;

  • 很多国内会用阿里云等镜像加速。

2)私服(公司内部搭建)

Nexus / Artifactory / JFrog / Harbor(部分场景)等;

用来:

  • 缓存公共依赖(代理 Maven Central,防止外网问题);

  • 存放公司私有库(不公开的 jar / war / pom);

  • 做 Release / Snapshot 的版本管理与审核。

在 POM 中,你会看到类似:

XML 复制代码
<repositories>
    <repository>
        <id>aliyun-maven</id>
        <url>https://maven.aliyun.com/repository/public</url>
    </repository>
</repositories>

在 CI / 公司项目里,更常见的是把这些配置放到 settings.xml 里,通过 mirror 统一重定向所有请求到私服。

Maven Central:开源 Java 世界的"官方图书馆"

  • 几乎所有主流 Java 开源库,都会发布到 Maven Central;

  • 你在文档里看到的 <dependency>,90% 对应的 jar、pom 就在这里;

  • 默认情况下,Maven 就是从 Central 拉依赖。

换句话说:

只要你掌握了 Maven 的坐标体系(GAV),

就能在 Maven Central 这座"图书馆"里 准确拿出任何一本书

distributionManagement:构件"往哪儿发"

repositories 决定"从哪拉依赖",

distributionManagement 决定"发构件到哪"。

XML 复制代码
<distributionManagement>
    <repository>
        <id>releases-repo</id>
        <url>https://repo.example.com/maven/releases</url>
    </repository>
    <snapshotRepository>
        <id>snapshots-repo</id>
        <url>https://repo.example.com/maven/snapshots</url>
    </snapshotRepository>
</distributionManagement>

配合命令:

bash 复制代码
mvn deploy

Maven 就会:

  • 把当前构件(jar/pom 等)上传到 releasessnapshots 仓库;

  • 如果 version 以 -SNAPSHOT 结尾,就进 snapshot 仓库;

  • 否则进 release 仓库。

团队协作 / 多项目复用 里,这一步极其关键:

A 项目打包 deploy 到私服 → B 项目在 <dependencies> 里直接用 GAV 引用;

比较大的公司,通常会要求:

  • 只有"过了测试 / 代码审查"的版本,才能 deploy 到 release 仓库;

  • 快速迭代的版本停留在 snapshot 仓库。

生命周期 & 插件:从 mvn clean package 看懂整条流水线

现在再回头看你最常敲的一句命令:

bash 复制代码
mvn clean package

我们就不把它当"魔法咒语",而是拆成两部分:

  • clean:对应 clean 生命周期

  • package:对应 default 生命周期 里的一个 phase。

三大生命周期:clean / default / site

Maven 内置了三套 Lifecycle:

1)clean

  • 负责清理以前构建的产物(主要是删 target/

  • 常见 phase:pre-clean → clean → post-clean

2)default(构建主线)

  • 编译 / 测试 / 打包 / 安装 / 部署

  • 常见 phase:

bash 复制代码
validate
compile
test
package
verify
install
deploy

3)site

  • 生成项目信息站点(文档、报表)

  • 现在用得相对少

关键点:

  • 执行 mvn package,其实是在说:

"请把 default 这个生命周期里,从 validate 一直跑到 package。"

  • 执行 mvn clean package,等于:

"先跑一遍 clean 生命周期,再跑 default 生命周期到 package。"

Phase、Plugin、Goal:三件事的关系

我们先画一个关系图:

bash 复制代码
Lifecycle = 一条流水线
  ↓
Phase = 流水线上的一个站点(compile/test/package/...)
  ↓
Plugin + Goal = 在这个站点上要调用哪些"工具"(任务)
  • Phase (阶段):compiletestpackage 等;

  • Plugin(插件):一个 jar 包,里面装着一堆具体的"任务";

  • Goal(目标):插件里提供的某个具体任务(方法)。

例如:

maven-compiler-plugin:compile

  • Plugin = maven-compiler-plugin

  • Goal = compile

  • 通常绑定到 compile phase

maven-surefire-plugin:test

  • Plugin = maven-surefire-plugin

  • Goal = test

  • 绑定到 test phase

你在 pom.xml 里配置插件,实际就是在发指令:

"在某个 phase 上,请调用某个插件的某些 goal,参数如下......"

mvn clean package 背后到底发生了什么?

假设有这么一个经典的 Spring Boot 项目 pom:

XML 复制代码
<packaging>jar</packaging>

<build>
    <plugins>
        <!-- 编译 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.13.0</version>
            <configuration>
                <source>17</source>
                <target>17</target>
            </configuration>
        </plugin>

        <!-- 单元测试 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
        </plugin>

        <!-- Spring Boot 重打包 -->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <executions>
                <execution>
                    <id>repackage</id>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

当你执行:

bash 复制代码
mvn clean package

大致会经历这些步骤(略简化):

1)clean 生命周期

  • 调用 maven-clean-pluginclean goal

  • 删除 target/ 目录

2)default 生命周期直到 package

validate

  • 检查项目结构是否正常(这个阶段很多项目没有自定义插件)

compile

  • 调用 maven-compiler-plugin:compile

  • 编译 src/main/javatarget/classes

test

  • 编译 src/test/java

  • 调用 maven-surefire-plugin:test 运行单元测试

  • 把测试报告放到 target/surefire-reports

package

  • 调用 maven-jar-plugin:jar(默认)打一个普通 jar

  • 再调用 spring-boot-maven-plugin:repackage 把普通 jar 重打成可执行 fat-jar(这一步就是你可以用 java -jar demo-app-1.0.0.jar 直接跑起来的核心)

最终,你得到:

bash 复制代码
target/
  demo-app-1.0.0.jar          # 可执行 Spring Boot fat-jar
  demo-app-1.0.0.jar.original # 原始的普通 jar(可选)
  classes/                    # 编译后的 class & 资源
  test-classes/               # 测试 class

如果你接着执行:

bash 复制代码
mvn install

Maven 会在上一步基础上多做一件事:

  • demo-app-1.0.0.jar + 对应的 pom.xml 安装到本地仓库 ~/.m2/repository/com/example/demo-app/1.0.0/

  • 其他项目就可以通过 <dependency> 来引用它了

读插件配置的时候,脑子里该怎么想?

maven-compiler-plugin 为例:

XML 复制代码
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.13.0</version>
    <configuration>
        <source>17</source>
        <target>17</target>
    </configuration>
</plugin>

你可以在脑子里翻译成:

"在 compile phase 上,

版本 3.13.0 的编译插件 来编译代码,

编译参数:-source 17 -target 17。"

再看 Spring Boot 插件:

XML 复制代码
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
        <execution>
            <id>repackage</id>
            <goals>
                <goal>repackage</goal>
            </goals>
        </execution>
    </executions>
</plugin>

翻译成大白话就是:

"在 package 这个阶段,

除了默认打 jar 以外,

再执行一下 spring-boot-maven-pluginrepackage 任务,

把刚打完的 jar 重打成可执行 fat-jar。"

多模块工程:parent + modules + 继承与聚合

当项目还只有一个 demo-app 的时候,一份 pom.xml 足够优雅。

但当你开始想拆:

  • 公共工具抽出来一个 common

  • 接口定义抽出来一个 api

  • 业务服务有 order-service / user-service / payment-service

  • 再加一个 web 模块做管理后台

如果每个模块都是独立 Maven 工程,很快就会变成:

  • 每个项目 copy 一份差不多的 pom.xml

  • 版本号各改各的,半年后谁也说不清现在线上到底跑的是什么组合

  • 想全局升级一个依赖版本,要进 N 个项目挨个改

这时候,就该上 多模块 Maven 工程 了。

核心有两个关键词:

继承(parent) + 聚合(modules)

继承:抽出一个"家长 POM",统一规则

先看一个典型父 POM:

XML 复制代码
<!-- demo-parent/pom.xml -->
<project ...>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo-parent</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <properties>
        <java.version>17</java.version>
        <spring.boot.version>3.3.1</spring.boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- Spring Boot 官方 BOM -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- 公司统一规定版本的几个基础库 -->
            <dependency>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct</artifactId>
                <version>1.5.5.Final</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.13.0</version>
                    <configuration>
                        <source>${java.version}</source>
                        <target>${java.version}</target>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

注意几点:

packagingpom:这个模块不会生成 jar,而是专门做 父 POM / 配置中心

里面主要放:

  • 统一的 properties(JDK 版本、Spring Boot 版本等);

  • 统一的 dependencyManagement(依赖版本规范表);

  • 统一的 pluginManagement(插件版本规范表)。

子模块只要继承它:

XML 复制代码
<!-- demo-service/pom.xml -->
<project ...>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>demo-parent</artifactId>
        <version>1.0.0</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <!-- 不需要再写 groupId 和 version,可从 parent 继承 -->
    <artifactId>demo-service</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
        </dependency>
    </dependencies>
</project>

这时候你会发现:

  • 子模块可以不写 groupId / version

  • 子模块依赖 mapstruct 可以不写 version

  • 子模块配置 maven-compiler-plugin 可以不写 version,都走父 POM 的

继承解决的是:

多个模块之间的 "配置复用 + 规则统一" 问题。

聚合:让 mvn install 一把跑完所有模块

父 POM 还有一个常用角色:聚合工程(Aggregator)

在父 POM 里加上:

XML 复制代码
<modules>
    <module>demo-api</module>
    <module>demo-service</module>
    <module>demo-web</module>
</modules>

目录结构大致变成:

bash 复制代码
demo-parent/
  pom.xml              # 父 POM,同时也是聚合入口
  demo-api/
    pom.xml
  demo-service/
    pom.xml
  demo-web/
    pom.xml

这时你在 demo-parent 目录执行:

bash 复制代码
mvn clean install

Maven 会做两件事:

1)从父 POM 的 <modules> 里找到所有子模块

2)根据模块之间的依赖关系,按顺序构建:

  • 先构建被依赖的模块(比如 demo-api

  • 再构建依赖它的模块(比如 demo-servicedemo-web

聚合解决的是:

"一个命令构建整个工程树"的问题,方便 CI/CD 和本地开发。

很多时候,一个顶层 POM 会同时扮演这两个角色:

  • packaging=pom

  • 既是 parent,又是 aggregator

  • 既承载了"项目规则",又是"构建入口"

多模块里的依赖关系怎么设计?

常见几种模式:

1)API / SPI 抽象层

  • demo-api 模块只放接口、DTO、VO

  • demo-service 依赖 demo-api,实现这些接口

  • demo-web 同时依赖 demo-apidemo-service

2)公共工具模块

  • demo-common 放通用工具:异常封装、统一返回体、基础配置

  • 其他业务模块都依赖 demo-common

3)严格分层

  • demo-domain:领域模型

  • demo-infra:基础设施(DB/缓存/消息)

  • demo-app:应用服务

  • demo-interface:对外接口(REST/RPC)

多模块带来的最大好处是:

  • IDE 内一个工程就能看全所有模块的依赖关系

  • CI 一个 pipeline 就能完整构建 & 测试 & 打包所有模块

  • 逻辑分层非常清晰,模块间谁依赖谁一目了然

Profile:一套 pom 搞定多套环境配置

现实世界不只有一个"环境":

  • 开发环境(dev):本地 DB、本地 Redis、本地 MQ

  • 测试环境(test):共享测试库、测试 MQ

  • 预发环境(staging):接近生产配置

  • 生产环境(prod):高可用、限流、监控、告警全部拉满

问题来了:

到底是为每个环境搞一套不同的 pom.xml

还是一份 POM 搞定所有环境差异?

Maven 给出的答案就是:Profile

Profile 是什么?

一句话:

Profile = 可以按条件激活的一组"配置 patch"。

它可以覆盖 / 增补很多内容:

  • properties:环境变量

  • dependencies:某环境特有依赖

  • build:资源目录、插件参数

  • repositories:不同环境用不同仓库(比如内网 / 外网)

简单例子:

XML 复制代码
<profiles>
    <!-- 开发环境:默认激活 -->
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <app.env>dev</app.env>
            <db.url>jdbc:mysql://localhost:3306/demo_dev</db.url>
        </properties>
    </profile>

    <!-- 生产环境:手动指定 -Pprod 激活 -->
    <profile>
        <id>prod</id>
        <properties>
            <app.env>prod</app.env>
            <db.url>jdbc:mysql://prod-db:3306/demo_prod</db.url>
        </properties>
        <build>
            <resources>
                <resource>
                    <directory>src/main/resources-prod</directory>
                    <filtering>true</filtering>
                </resource>
            </resources>
        </build>
    </profile>
</profiles>

Profile 是怎么"叠加"到 POM 上的?

构建时,Maven roughly 会做:

1)解析主 POM

2)找到所有 <profile>

3)根据激活规则,确定谁生效:

  • <activeByDefault>true</activeByDefault>

  • 命令行 -Pprod

  • <activation> 里根据 JDK 版本、OS、系统属性等条件自动激活

4)把这些 profile 里的配置合并进主 POM 的 Model

5)再走依赖解析 + lifecycle + 插件执行那套流程

你可以理解为:

POM = 主配置 + (0~N 个 profile patch)

Profile 的常见激活方式

1)默认激活

XML 复制代码
<activation>
    <activeByDefault>true</activeByDefault>
</activation>

项目不指定 -P 时,就用这个 profile。

2)命令行激活

bash 复制代码
mvn clean package -Pprod

可以指定多个:

bash 复制代码
mvn clean package -Pprod,with-monitor,with-metrics

3)按环境 / JDK / 系统属性激活

XML 复制代码
<activation>
    <jdk>[17,)</jdk>  <!-- JDK ≥ 17 时生效 -->
</activation>

<activation>
    <os>
        <family>unix</family>
    </os>
</activation>

<activation>
    <property>
        <name>env</name>
        <value>prod</value>
    </property>
</activation>

配合命令行:

bash 复制代码
mvn clean package -Denv=prod

就能在 CI 里按变量激活对应 profile。

Profile 能帮你做什么?

几个常见、很实用的场景:

1)按环境切换资源目录

  • dev 用 src/main/resources

  • prod 用 src/main/resources-prod

2)按环境控制依赖

  • dev 引入 spring-boot-devtools

  • prod 不要这个依赖

3)按环境控制插件行为

  • dev 跳过某些耗时检查(比如集成测试、静态扫描)

  • CI / prod profile 开启全量检查

4)按环境切换编译参数 / 打包参数

  • 比如 prod 打包时额外带上某些 metadata 或启用某种混淆插件

Maven 在云原生时代:和 Spring Boot / Docker / CI/CD 怎么配合?

说到这里,你可能会问:

"这些东西听起来都挺工程化的,

那放在 Spring Boot + Docker + K8s + CI/CD 的世界里,

Maven 具体扮演什么角色?"

用一句稍微"硬核"一点的话说:

在云原生 Java 服务的落地流程里,

Maven 是"从源码到制品"的标准流水线

后面的 Docker / K8s / Helm / ArgoCD 都是接在它后面的。

典型流水线:从源码到 K8s 的一条链

我们用一个典型微服务为例,看一条完整链路:

bash 复制代码
Git 代码提交
   ↓
CI 拉取代码
   ↓
Maven 构建:mvn clean package -DskipTests
   ↓
得到可执行 jar(Spring Boot fat-jar)
   ↓
Docker 构建镜像(Dockerfile / Jib / Buildpacks)
   ↓
推送镜像到镜像仓库(Harbor / ECR / ACR / ...)
   ↓
K8s 部署(kubectl / Helm / ArgoCD 等)
   ↓
服务在集群中运行

在这条链上:

Maven 负责:

  • 解析依赖

  • 编译

  • 测试

  • 打包

后面的阶段(Docker / K8s)只关心:

  • "我拿到一个标准的 jar(或者直接拿到构建出来的镜像)"

所以你会看到,在 CI 的脚本里,常常有这样几行:

bash 复制代码
# GitHub Actions / GitLab CI / Jenkinsfile 里类似的步骤
- name: Build with Maven
  run: mvn -B clean package -DskipTests

- name: Build Docker image
  run: docker build -t registry.example.com/demo-service:${GIT_COMMIT} .

Spring Boot + Maven:最常见的组合拳

Spring Boot 官方给你准备好了非常顺手的一套:

1)继承 spring-boot-starter-parent

2)使用 spring-boot-dependencies 这个 BOM 管理版本

3)使用 spring-boot-maven-plugin

  • repackage:打可执行 jar

  • spring-boot:run:本地开发快速启动

最小 pom:

XML 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.1</version>
</parent>

<dependencies>
    <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>

配套命令:

bash 复制代码
# 本地开发
mvn spring-boot:run

# 构建可执行 jar(CI 常用)
mvn clean package -DskipTests

Maven 驱动 Docker 构建的几种方式

在"容器化时代",常见有三种玩法:

方式一:传统 Dockerfile

1)用 Maven 打 jar:

bash 复制代码
mvn clean package -DskipTests

2)Dockerfile:

bash 复制代码
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/demo-app-1.0.0.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

3)然后 CI 里:

bash 复制代码
docker build -t registry.example.com/demo-app:${GIT_COMMIT} .
docker push registry.example.com/demo-app:${GIT_COMMIT}

优点:灵活、通用;

缺点:有两套配置(Maven & Dockerfile),需要保持一致。

方式二:Jib / Buildpacks 等"Maven 插件直接出镜像"

比如 Spring Boot 3.x 官方推荐使用 Buildpacks:

bash 复制代码
mvn spring-boot:build-image \
  -Dspring-boot.build-image.imageName=registry.example.com/demo-app:${GIT_COMMIT}

Spring Boot 插件会帮你:

  • 打 jar

  • 基于 buildpacks 构建容器镜像

  • 推送到仓库(只要配置好权限)

好处:

  • 不需要 Dockerfile

  • 构建步骤更统一:所有操作都挂在 Maven 插件下

  • 对安全 / 多阶段构建 / 小镜像体积已经做了优化

Jib 同理(jib-maven-plugin),直接在 Maven lifecycle 里产生镜像。

方式三:多阶段 Docker + Maven 缓存优化

适合对构建性能和镜像体积有极致追求的场景:

bash 复制代码
# 构建阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn -B dependency:go-offline
COPY src ./src
RUN mvn -B package -DskipTests

# 运行阶段
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /build/target/demo-app-1.0.0.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

这里 Docker 和 Maven 紧密配合:

利用 mvn dependency:go-offline + Docker layer cache:

  • 依赖不变就复用缓存,加速构建。

运行镜像中只保留最终 jar,构建工具 & 源码不进入最终镜像。

Maven 在 CI/CD 中的"契约"角色

在一个成熟一点的团队里,通常会有这样的"分工":

1)Maven 负责:

构建逻辑层面的契约

  • 项目结构;

  • 依赖树;

  • 构建产物类型(jar/war);

  • 测试标准(哪些测试算 pass);

CI 只是负责"调用 Maven 构建",不关心具体细节。

2)CI/CD 工具(Jenkins / GitHub Actions / GitLab CI)负责:

  • 触发时机(push / PR / tag / 手动);

  • 并行策略 / 缓存策略;

  • 构建环境(JDK 版本、Maven 版本、Docker daemon 等);

  • 后续步骤(制品上传、镜像推送、部署到 K8s)。

这种分工的好处是:

  • Maven 配置是项目的一部分,版本受 Git 管控;

  • CI/CD 配置更多是"运行时逻辑",可以复用 / 模板化;

  • 当你从 Jenkins 换到 GitHub Actions 时,只要能跑 Maven 命令,构建本身不用重写。

为什么说"搞懂 Maven,云原生不会太慌"?

最后给一个有点"鸡汤",但又挺实在的结论:

当你真正搞清楚 Maven 的:

1)依赖管理(GAV / 传递依赖 / BOM);

2)生命周期 & 插件(Lifecycle / Phase / Plugin / Goal);

3)多模块 & Profile(parent / modules / profiles)。

你会发现:

1)换 Gradle,只是换个 DSL;

2)换 CI/CD,只是换个"谁来执行 mvn xxx";

3)换云平台(AWS / 阿里云 / K8s / 自建集群),只是换个"jar / 镜像最后被部署到哪"。

Maven 本质上帮你把 "代码 → 产物"这一段 pipeline 变成可描述、可复用、可迁移的东西

这就是为什么,哪怕都 2025 年了,你依然绕不开它。

实战部分:从 0 写一个"干净可维护"的 Maven 项目

前面讲了一大圈概念,这一节就来点"落地"的:

如果你现在开一个新的 Java / Spring Boot 项目,要怎么从 0 搭一份 既干净、又方便扩展、多模块 & CI 友好 的 Maven 工程?

我给你一个可以直接拿去复用的思路,分 5 步走。

Step 1:先想清楚 GAV 命名 & 目录结构

不要一上来就 mvn archetype:generate,先在草稿上想几件事:

1)groupId:公司 / 组织 / 业务线

bash 复制代码
com.yourcompany.platform
com.yourcompany.shop
com.yourcompany.payment

2)artifactId:这个"工程"的名字

先定一个顶层工程名,比如:

bash 复制代码
shop-platform

3)准备好以后拆多模块的空间

目录先这么想:

bash 复制代码
shop-platform/
 pom.xml               # 父 POM + 聚合工程
 shop-common/          # 公共工具模块
   pom.xml
 shop-user-service/    # 用户服务
   pom.xml
 shop-order-service/   # 订单服务
   pom.xml

即使你现在只打算先写一个模块,也建议从一开始就有 parent POM 的概念,后面要拆多模块会轻松很多。

Step 2:写一个"高内聚"的父 POM(parent + aggregator)

顶层 shop-platform/pom.xml

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
            https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <!-- 顶层 groupId / artifactId / version -->
    <groupId>com.yourcompany.shop</groupId>
    <artifactId>shop-platform</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <!-- 作为聚合工程:收纳所有子模块 -->
    <modules>
        <module>shop-common</module>
        <module>shop-user-service</module>
        <module>shop-order-service</module>
    </modules>

    <!-- 统一属性 -->
    <properties>
        <java.version>17</java.version>
        <spring.boot.version>3.3.1</spring.boot.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- 依赖版本规范表(可以引入 Spring Boot BOM,也可以引自己公司的 BOM) -->
    <dependencyManagement>
        <dependencies>
            <!-- Spring Boot 版本统一管理 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- 公司内部统一依赖,如 mapstruct / lombok 等 -->
            <dependency>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct</artifactId>
                <version>1.5.5.Final</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- 插件版本规范表 -->
    <build>
        <pluginManagement>
            <plugins>
                <!-- 编译插件 -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.13.0</version>
                    <configuration>
                        <source>${java.version}</source>
                        <target>${java.version}</target>
                    </configuration>
                </plugin>

                <!-- 单元测试插件 -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>3.2.5</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

    <!-- 环境 Profile 放到顶层,子模块统一使用 -->
    <profiles>
        <profile>
            <id>dev</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <app.env>dev</app.env>
            </properties>
        </profile>

        <profile>
            <id>prod</id>
            <properties>
                <app.env>prod</app.env>
            </properties>
        </profile>
    </profiles>
</project>

这个父 POM 已经扮演了两个角色:

  • parent:提供统一的属性 / 版本 / 插件配置;

  • aggregator:一次 mvn clean install,能把所有子模块都跑完。

到这里,你已经有了一个"团队级模板"的雏形了。

Step 3:写一个典型业务模块的 POM(以 user-service 为例)

shop-user-service/pom.xml

XML 复制代码
<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>

    <!-- 继承顶层父 POM -->
    <parent>
        <groupId>com.yourcompany.shop</groupId>
        <artifactId>shop-platform</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <!-- 只需要声明 artifactId -->
    <artifactId>shop-user-service</artifactId>
    <packaging>jar</packaging>

    <name>shop-user-service</name>
    <description>User service of shop platform</description>

    <dependencies>
        <!-- Spring Boot Web / Actuator / JPA 等依赖,都不写 version -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 数据库驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- 公共模块 -->
        <dependency>
            <groupId>com.yourcompany.shop</groupId>
            <artifactId>shop-common</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!-- MapStruct,版本由父 POM 的 dependencyManagement 管理 -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
        </dependency>

        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>${project.artifactId}-${project.version}</finalName>

        <plugins>
            <!-- 直接使用父 POM 里定义好的插件版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>

            <!-- Spring Boot Maven 插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

注意这里的几个"克制"设计:

  • 不在子模块重复写 groupId / version / 插件版本;

  • 不在子模块写 <dependencyManagement> / <pluginManagement>

  • 只关心:这个模块自己需要哪些依赖 & 插件

  • 版本、规则、环境 profile 都交给父 POM 统一管。

这样一来,你后面要做全局升级,比如:

  • 从 JDK 17 升级到 21;

  • 从 Spring Boot 3.3.x 升级到 3.4.x;

  • 从某个依赖的 1.x 升级到 2.x。

只要改一处:顶层父 POM。

Step 4:给团队约定一份"pom 规范模板"

你可以把上面这两个 POM 精简成一份规范:

  • parent-pom-template.xml:团队统一 parent

  • service-pom-template.xml:新建服务时 copy 的模板

并且约定好几点:

1)所有服务必须继承团队的 parent POM

2)子模块中严禁随意写 version(除非有特殊说明)

3)统一放到某个 Git 仓库 / Wiki,新建项目直接 copy

4)CI/CD 里只允许执行有限的几条 Maven 命令:

bash 复制代码
mvn -B clean verify
mvn -B clean package -DskipTests
mvn -B deploy -Pprod

有了这个"模板 + 准入规则",你的 Maven 工程就从"个人习惯"升级成了"团队工程规范"。

Step 5:配一条最小可用的 CI 脚本

以 GitHub Actions 为例,一个最小的 Java + Maven + Docker 的 workflow:

bash 复制代码
name: CI

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17
          cache: maven

      - name: Build with Maven
        run: mvn -B clean package -DskipTests

      # 如果需要构建 Docker 镜像,再加一段:
      # - name: Build Docker image
      #   run: docker build -t registry.example.com/shop-user-service:${{ github.sha }} .

你会发现:

  • CI 只负责拉代码 + 配环境 + 调用 Maven;

  • 具体依赖版本、构建逻辑、模块关系,全写在 pom.xml 里;

  • 当你把项目迁到 Jenkins / GitLab CI,只要能执行 mvn,脚本改动很小。

到这一步,一个 "干净、可维护、易扩展、多环境友好" 的 Maven 项目就算成型了。

把 Maven 放进你的 Java 心智模型

最后,我们不再谈 XML、命令、插件这些细节,回到一个更大的问题:

在你的 Java 技术栈认知里,Maven 应该被放在哪个位置?

如果你之前看过类似《Servlet = Java 时代最底层的 Web 请求处理机制》、《JDBC = Java 访问数据库的最底层规范》这类文章,可以试着这样画一张图:

bash 复制代码
JVM / JDK       → 最底层执行和语言能力
JDBC / Servlet  → 最底层 IO / Web / DB 规范
Spring 全家桶   → 上层框架与生态
Maven           → 把"代码 + 依赖 + 配置"变成"可部署制品"的流水线
CI/CD           → 自动化执行这一套流水线 + 部署到环境
K8s / 云平台     → 运行这些制品的基础设施

你会发现:

1)Maven 不直接参与业务逻辑

但它决定了:

  • 你的项目怎么组织

  • 依赖怎么管理

  • 构建产物长什么样

2)Maven 也不是传统意义上的"框架"

它更像是:

这座"Java 城市"里的物流系统生产流水线标准

当你真正把 Maven 放进心智模型里,很多事情会变得更清晰:

1)当你看到一个陌生项目的 pom.xml,脑子里会自动按 7 个区域去解析:

  • 身份 & 血统(parent / GAV / packaging)

  • 元信息(name / description / scm)

  • 属性(properties)

  • 依赖(dependencies / dependencyManagement)

  • 构建(build / plugins / pluginManagement)

  • 仓库 & 发布(repositories / distributionManagement)

  • 环境(profiles)

2)当你配 CI/CD 时,不再是"瞎写几条脚本让它跑起来",而是很自然地问自己:

  • 这一条 mvn 命令,对应的是哪条生命周期?

  • 我希望在 CI 里做到哪个 Phase?test 还是 verify 还是 install

  • 产物是 jar 还是 war,还是直接打镜像?后面谁会用到这些产物?

3)当你看 Gradle / Bazel / SBT 时,也不会慌:

  • 底层概念基本一样:依赖管理、生命周期、任务、插件、仓库

  • 只是换了一套 DSL 和命令风格而已

4)当你写"工程级"的 Java 项目(而不是 demo)时,Maven 不再是 IDE 帮你生成的那坨 XML,而是:

  • 你团队工程规范的一部分

  • 你简历上"工程能力"的一部分(特别是在讲多模块、CI/CD、云原生的时候)

  • 你能和别的后端 / DevOps 高效协作的基础"共同语言"

学 Maven,不是为了"背命令、背 XML 标签",

而是为了在脑子里多长出一条"从源码到制品"的清晰流水线。

当这条线清晰之后,

你在 Java 世界做的所有工程化选择 ------ 无论是 Spring、Gradle、Docker 还是 K8s ------ 都会有一个更稳的落点。

相关推荐
shayudiandian1 小时前
【Java】内部类
java
老鼠只爱大米1 小时前
Java设计模式之装饰器模式详解
java·设计模式·装饰器模式·decorator·java设计模式
i***l9201 小时前
使用 Spring Boot 实现图片上传
spring boot·后端·状态模式
0***v7771 小时前
springboot 异步操作
java·spring boot·mybatis
LSL666_1 小时前
7 SpringBoot pom.xml解释
java·spring boot·spring
ps酷教程1 小时前
java泛型反射&mybatis的TypeParameterResolver
java·mybatis
b***59431 小时前
springboot+mybaties项目中扫描不到@mapper注解的解决方法
java·spring boot·mybatis
初学者,亦行者1 小时前
【探索实战】监控、安全与边缘场景的深度落地
云原生
i***17181 小时前
SpringBoot Maven 项目 pom 中的 plugin 插件用法整理
spring boot·后端·maven