服务端5月31日 23:58
什么是 Maven?它解决了 Java 项目构建里的哪些问题?Maven 是 Java 生态里最常用的项目构建和依赖管理工具之一。它用一个 `pom.xml` 描述项目坐标、依赖、插件和构建流程,然后按约定完成编译、测试、打包、安装和发布。以前手工下载 jar、手写编译脚本、每个项目目录结构都不一样,Maven 解决的正是这些重复又容易出错的问题。
Maven 的核心概念有四个:POM、坐标、仓库和生命周期。POM 是项目对象模型,坐标由 `groupId`、`artifactId`、`version` 标识一个构件。仓库分本地仓库和远程仓库,依赖会先查本地,找不到再从远程下载。生命周期则规定构建顺序,插件负责在具体阶段执行真正的任务。
```xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>hello-maven</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>
</project>
```
## Maven 的工作方式
Maven 的一个重要价值是让项目可以被重复构建。只要源码、POM、仓库和 JDK 环境一致,团队成员和 CI 理论上应该得到相同产物。它不会要求每个人手动下载同一批 jar,也不会让编译命令散落在各个脚本里。对团队协作来说,这种确定性比少写几行 XML 更重要。
依赖解析是 Maven 最常被使用、也最容易被误解的能力。Maven 会根据坐标下载直接依赖,也会解析这些依赖背后的传递依赖,并按规则选择最终版本。好处是接入一个框架很快,代价是你必须会看依赖树,否则遇到版本冲突时只能靠猜。
Maven 也不是只适合简单项目。多模块、父子 POM、BOM、插件管理和 profile 能支撑很复杂的工程,但复杂度应该逐步引入。一个新项目先保持标准目录和少量依赖,等确实出现版本统一、模块拆分或多环境构建需求时,再把高级配置加进去,维护成本会低很多。
学习 Maven 时不要只背命令,更要理解它把项目描述、依赖解析和构建流程拆开了。POM 负责描述项目是什么,仓库负责找到需要的构件,生命周期负责规定什么时候做什么。这个模型一旦理解,后面看多模块、scope 或插件配置就不会觉得零散。实际排错时也按这三块拆开看,定位会快很多。这也是 Maven 面试题常从概念追到依赖、生命周期和插件的原因。
## 追问
### Maven 和手动管理 jar 最大区别是什么?
手动管理 jar 看似直接,但版本来源、传递依赖、冲突排查都靠人记,很容易失控。Maven 把依赖声明成坐标,并通过仓库自动解析传递依赖,团队成员拿到代码后可以用同一套方式构建。它的取舍是引入了 POM、生命周期、插件这些学习成本。项目越大、协作越多,Maven 的标准化收益越明显。
### POM 里最重要的配置有哪些?
最基础的是 `groupId`、`artifactId`、`version`,它们决定项目产物的唯一坐标。然后是 `<dependencies>`,用于声明项目真正需要的依赖。复杂项目还会用 `<dependencyManagement>` 管版本,用 `<build>` 和插件配置编译参数、打包方式和测试行为。踩坑点是把所有配置都塞进一个 POM,短期省事,后期多模块复用和排查会很痛苦。
### Maven 的“约定优于配置”有什么边界?
Maven 默认假设源码在 `src/main/java`,测试在 `src/test/java`,资源在 `src/main/resources`,产物放到 `target`。遵守这些约定时,POM 可以很短,团队也容易理解项目结构。边界是老项目或特殊生成代码项目可能不符合默认目录,这时可以配置插件,但配置越多,项目越不容易被新人接手。除非确实有历史包袱,否则不要轻易挑战 Maven 的默认目录。
```bash
mvn compile
mvn test
mvn clean package
mvn install
```
### Maven 插件到底负责什么?
生命周期只是流程名,插件才是真正执行编译、测试、打包、复制资源的组件。比如编译由 compiler 插件完成,测试常由 surefire 插件执行,打包 jar 由 jar 插件完成。你可以通过插件配置 Java 版本、编码、测试包含规则等细节。常见坑是 Maven 版本、插件版本和 JDK 版本不匹配,导致本地和 CI 的构建结果不同。
### Maven 有哪些优势,又有哪些不适合的地方?
它的优势是标准项目结构、成熟仓库生态、自动依赖解析、多模块构建和 CI 友好。对于绝大多数 Java 后端项目,这些能力已经足够稳定。它的不足是 XML 配置较啰嗦,复杂插件调试不够直观,极端定制构建可能没有 Gradle 灵活。取舍上,如果团队更看重稳定、统一和生态兼容,Maven 仍然是很稳的选择;如果构建逻辑高度动态,才需要认真比较其他工具。标签
Maven
Maven 是一个流行的自动化构建工具,由 Apache Software Foundation 管理。它主要被用于 Java 项目的构建、依赖管理和项目信息管理。Maven 使用一种被称为 Project Object Model (POM) 的 XML 文件来描述项目的结构和内容,包括项目依赖、插件、目标等。

服务端5月31日 23:58
Maven scope 有哪些类型?什么时候该用 compile、provided 或 runtime?Maven 的 scope 决定一个依赖在哪些阶段可见:编译、测试、运行、打包,以及是否会传递给下游项目。它不是简单的分类标签,而是在控制 classpath 边界。scope 配错时,轻则最终包变大,重则本地能跑、服务器启动就报类找不到。
最常见的是 `compile`,它是默认范围,编译、测试、运行都可用,也会传递。`provided` 表示编译和测试需要,但运行时由 JDK、容器或平台提供。`runtime` 表示编译时不需要,运行和测试时需要。`test` 只在测试代码中可见,不会进入正式产物。`system` 需要指定本地 jar 路径,几乎不推荐。`import` 只能用于 `dependencyManagement`,常用来导入 BOM。
```xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
```
## 选择 scope 的判断方法
判断 scope 时可以先问三个问题:编译主代码是否需要它,运行产物是否需要它,下游项目是否应该感知它。三个答案都是“是”,通常就是 compile;只在测试里用,就是 test;编译要用但运行环境提供,就是 provided;编译不直接用但运行要加载,就是 runtime。这个判断比死记六种 scope 更可靠。
打包方式也会影响 scope 的效果。普通 jar 项目不会把依赖直接塞进 jar,Spring Boot 这类可执行包会重新打包依赖,Web 项目生成 war 时又要考虑容器提供的库。比如 servlet-api 在传统 war 中适合 provided,但在嵌入式容器的可执行 jar 场景下,相关依赖可能由 starter 间接带入。边界不是某个依赖永远用某个 scope,而是看运行环境谁负责提供它。
排查 scope 问题时,除了看 POM,还要看最终产物。可以用 `mvn dependency:tree` 看解析结果,用 `jar tf target/app.jar` 或查看 `WEB-INF/lib` 确认依赖是否被打包。很多线上问题不是依赖没声明,而是 scope 让它没有进入最终运行路径。
在多模块项目里,scope 还会影响下游模块的 classpath。一个公共 api 模块如果把实现库设为 compile,下游会被迫继承这个选择;如果设成 provided 或 optional,又可能需要下游自己补依赖。这个边界要在模块设计时想清楚,不能等打包失败后再临时改。对库作者来说,scope 既是构建配置,也是对使用方的一种契约。因此评审 POM 时,scope 应该和依赖版本一样被认真检查。尤其是从 war 迁移到可执行 jar 时,原来合理的 provided 可能就需要重新评估。否则 scope 看起来只是一个小字段,实际却会改变整个运行包的内容。
## 追问
### compile scope 为什么是默认值?
大多数业务代码依赖的库既要参与编译,也要在运行时使用,所以 Maven 把 `compile` 设为默认值。比如工具类库、核心框架 API、业务 SDK 通常都适合 compile。它的代价是会向下游传递,库项目如果滥用 compile,会把很多不必要的依赖带给使用方。取舍原则是:只有公共 API 暴露出去的类型,才更有理由使用 compile。
### provided 适合哪些场景?
`provided` 适合运行环境已经提供该依赖的情况,例如 Servlet API 由 Tomcat 提供,Lombok 只在编译期生成代码。这样可以避免把容器已有的 jar 再打进应用,减少冲突。边界是你必须确定运行环境真的有这个依赖,而且版本兼容。最常见的坑是本地测试靠 Maven classpath 能跑,部署到精简容器后缺包。
```xml
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
```
### runtime 和 compile 怎么区分?
如果代码编译时只依赖标准接口,具体实现到运行时才需要,就适合 `runtime`。JDBC 驱动是典型例子,业务代码引用的是 `java.sql` 接口,不需要在编译期直接使用 MySQL 驱动类。这样做能让编译 classpath 更干净,也避免把实现细节误暴露给上游模块。踩坑点是某些框架启动时会反射加载驱动,scope 写成 test 或漏配时,编译没问题但启动失败。
### test scope 会不会传递给正式代码?
不会,`test` 只在测试编译和测试运行阶段可见,不会参与正式打包,也不会传递给依赖你的项目。JUnit、Mockito、AssertJ 这类测试库都应该放 test。取舍点是测试工具不要污染生产运行环境,尤其是 mock、内嵌服务器和测试容器库。常见坑是把测试辅助类放进 `src/main/java`,导致主代码反过来依赖 test scope,编译直接失败。
### import scope 为什么只能配合 dependencyManagement?
`import` 的作用是导入另一个 POM 的依赖管理配置,本质上是把一组版本约束合并进当前项目。它不是普通 jar 依赖,所以不能放在常规 `<dependencies>` 中使用。Spring Boot 项目常通过导入 BOM 统一 Spring、日志、Jackson 等版本,避免手动拼版本矩阵。边界是 BOM 只管版本,不保证你的代码兼容升级后的行为,升级前仍然要跑测试和依赖树检查。服务端5月31日 23:58
Maven 依赖传递如何工作?冲突版本该怎么排查?Maven 依赖传递的意思是:你的项目依赖 A,而 A 又依赖 B,那么 B 通常会自动进入你的项目 classpath。这个机制省掉了大量手工拷 jar 的工作,但也会带来版本冲突、重复依赖和运行时类不兼容。排查 Maven 依赖问题时,不要先猜哪个 jar 错了,先看依赖树。
Maven 决定冲突版本时主要看两个规则:路径近的优先,路径一样近时先声明的优先。比如项目同时通过两条路径引入 `guava:30` 和 `guava:32`,Maven 只会选一个版本进入最终依赖。这个选择不一定符合业务预期,所以大型项目通常会用父 POM 或 BOM 统一锁版本。
```bash
mvn dependency:tree
mvn dependency:tree -Dincludes=com.google.guava:guava
mvn dependency:analyze
```
## 排查依赖冲突的顺序
依赖冲突不要从删除 jar 开始,应该先确定最终 classpath 里到底用了哪个版本。第一步看 `mvn dependency:tree`,确认冲突依赖来自哪条路径;第二步看业务报错,是编译期缺类、运行时缺方法,还是类重复导致的加载问题;第三步再决定是直接声明版本、使用 `dependencyManagement`,还是对某个上游依赖做 exclusion。
版本统一也不是越新越好。安全漏洞修复通常要求升级,但框架生态会对版本组合有约束,例如 Spring Boot 管理的 Jackson、Netty、Logback 版本最好优先跟随官方 BOM。强行把其中一个依赖单独升到很新的版本,短期能过扫描,长期可能引入二进制兼容问题。
在库项目里尤其要注意依赖暴露。你的公共 API 如果返回了某个第三方类型,下游就被迫感知这个依赖版本;如果只是内部实现使用,尽量不要让它泄漏到 API 签名中。这个边界处理不好,依赖冲突会从一个项目扩散到所有使用方。
还有一种隐蔽问题是依赖范围带来的传递差异。上游把某个库标成 optional 或 provided,下游可能并不会自动拿到它。此时不要盲目责怪 Maven,先确认这个依赖到底是不是上游希望暴露给你的运行时依赖。这类依赖最好在模块说明里写清楚,否则使用方很难判断该不该自己声明版本。
## 追问
### Maven 解决依赖冲突时为什么是“最近优先”?
最近优先可以让直接依赖更容易表达项目意图,因为离当前项目越近,通常越能代表当前项目真正需要的版本。它的好处是规则简单、可预测,坏处是版本选择可能被某个中间依赖无意改变。比如新增一个 starter 后,依赖路径变短,最终使用的日志库版本就可能变了。踩坑时不要只看 `pom.xml` 的直接依赖,要用依赖树确认最终生效版本。
### `dependencyManagement` 能不能直接引入依赖?
不能,它只负责管理版本和范围,不会把依赖自动加入项目。子模块仍然需要在 `<dependencies>` 中声明 groupId 和 artifactId,才会真正使用这个依赖。这个设计的取舍是牺牲一点书写便利,换来模块依赖边界清晰。常见错误是只在父 POM 的 `dependencyManagement` 里加了版本,然后发现代码里还是找不到类。
```xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.2.1-jre</version>
</dependency>
</dependencies>
</dependencyManagement>
```
### 什么时候该用 exclusions 排除传递依赖?
当某个传递依赖明确不应该进入当前项目,或者它引入了冲突版本时,可以用 `<exclusions>`。但排除依赖不是越多越好,因为你可能把上游库运行时真正需要的类排掉,最后变成 `ClassNotFoundException`。更稳的做法是先用依赖树确认来源,再排除具体坐标,而不是大面积排掉一组库。边界是安全类、日志实现、旧版 JSON 库这类常见冲突点适合谨慎排除。
```xml
<dependency>
<groupId>com.example</groupId>
<artifactId>legacy-sdk</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
```
### BOM 和父 POM 管版本有什么区别?
父 POM 可以管理依赖版本、插件版本、属性和构建配置,适合公司内部统一工程规范。BOM 通常只通过 `import` scope 导入一批依赖版本,适合 Spring Boot、Spring Cloud 这类生态统一版本矩阵。取舍上,如果你只想复用版本,不想继承别人的插件和构建约定,BOM 更轻。踩坑点是多个 BOM 同时导入时,顺序会影响相同依赖的最终版本。
### 依赖冲突一定会在编译期暴露吗?
不一定,很多冲突只会在运行时出现,例如 `NoSuchMethodError`、`NoClassDefFoundError` 或序列化行为异常。编译期只说明当前源码能找到方法,不代表运行时加载的 jar 版本完全匹配。边界在于 Java classpath 最终只加载一个同名类,多个版本并存时谁先生效会影响结果。生产事故里最怕的就是测试环境和线上依赖树不同,所以发布前最好在 CI 中固定 Maven 版本、仓库源和构建参数。服务端5月31日 23:58
Maven 生命周期有哪些阶段?执行命令时到底跑了什么?Maven 生命周期可以理解为一条已经排好顺序的构建流水线。你输入的不是“执行某个脚本”,而是告诉 Maven 要跑到哪个阶段,Maven 会把这个阶段之前的阶段一起执行。最常用的是 default 生命周期,另外还有 clean 生命周期和 site 生命周期,它们彼此独立。
clean 生命周期负责清理构建产物,典型命令是 `mvn clean`,会删除 `target` 目录。default 生命周期负责从校验、编译、测试到打包、安装、发布的主流程。site 生命周期负责生成项目站点文档,业务项目里用得少,但开源库发布文档时仍然有价值。
default 生命周期里高频阶段包括 `validate`、`compile`、`test`、`package`、`verify`、`install`、`deploy`。执行 `mvn package` 会先跑到测试再打包,执行 `mvn install` 会在打包后把产物安装到本地仓库,执行 `mvn deploy` 则继续上传到远程仓库。
```bash
mvn clean
mvn test
mvn clean package
mvn clean install -DskipTests
mvn clean deploy -Prelease
```
## 命令执行时要看清边界
Maven 命令由生命周期阶段、插件目标、参数和 profile 共同决定。`mvn clean package` 里既有 clean 生命周期,也有 default 生命周期的 package 阶段;`mvn dependency:tree` 则是直接执行插件目标,不会自动跑完整编译流程。理解这个区别后,很多“为什么这个命令没有编译代码”的疑问就能解释清楚。
CI 里通常不会只跑 `package`,而会选择 `verify` 或 `deploy`。`verify` 更适合质量门禁,因为它给集成测试、静态检查、覆盖率校验留了位置;`deploy` 则适合发布流水线,前提是仓库凭证和版本策略已经配置好。取舍点是本地越快越好,CI 越确定越好,所以两边命令不必完全一样。
还有一个容易忽略的点是 profile。相同的 `mvn clean package`,加上 `-Pprod` 后可能启用不同资源、插件或依赖,最终产物也会不同。排查构建差异时,要同时记录 Maven 版本、JDK 版本、命令参数和激活的 profile,不要只说“我也是 package”。
如果项目里有集成测试,还要区分 surefire 和 failsafe。前者通常跑单元测试,后者更适合绑定到 integration-test 和 verify。这样可以把快速反馈和完整验证拆开,避免每次本地小改动都等待慢测试。本地调试时可以先用较短阶段确认问题,合并前再交给 CI 跑完整验证。
## 追问
### `mvn package`、`install` 和 `deploy` 有什么区别?
`package` 只是在当前项目里生成 jar、war 等产物,通常放在 `target` 目录下。`install` 会把产物安装到本机 Maven 仓库,供本机其他项目依赖,所以多模块以外的本地联调经常需要它。`deploy` 会把产物发布到远程仓库,影响团队其他人和 CI 环境,边界更重。踩坑点是把 `install` 当成发布,结果别人机器上根本拿不到你的包。
### 为什么执行某个阶段会连前面的阶段一起跑?
生命周期的设计就是保证构建状态可靠,不能在没有编译的情况下直接测试,也不能在没测试的情况下默认打包。比如 `mvn verify` 会跑过 validate、compile、test、package 等前置阶段。这个规则减少了遗漏步骤,但也带来耗时问题,所以本地开发会用 `-DskipTests` 或 `-DskipITs` 做取舍。注意跳过测试只适合本地快速验证,CI 主线不建议长期这么做。
### 生命周期阶段和 Maven 插件是什么关系?
阶段本身只是一个抽象节点,真正干活的是绑定到阶段上的插件 goal。比如 jar 项目的 `compile` 阶段通常由 `maven-compiler-plugin:compile` 执行,`test` 阶段由 `maven-surefire-plugin:test` 执行。你也可以直接运行插件目标,例如 `mvn dependency:tree`,它不一定属于完整生命周期。常见踩坑是以为改了生命周期就等于改了插件行为,实际上很多细节要在插件配置里改。
```xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
</plugins>
</build>
```
### `clean` 和 `package` 为什么经常一起用?
`clean` 会删除旧的 `target`,可以避免历史产物、生成代码或资源文件残留影响结果。`package` 则负责重新编译测试并打包,两者组合能得到更干净的构建。取舍在于速度:本地频繁改代码时不一定每次都 clean,否则增量编译优势会被浪费。遇到“本地能跑、CI 不能跑”或资源文件莫名不更新时,再用 `mvn clean package` 排查更合适。
### 多模块项目执行生命周期有什么边界?
在多模块项目根目录执行命令时,Maven reactor 会按模块依赖顺序跑同一个阶段。用 `-pl` 可以限制模块范围,用 `-am` 可以补上被依赖模块,这对大仓库节省时间很有用。边界是 Maven 只理解 POM 里的模块和依赖关系,不会自动知道你运行时通过反射、脚本或配置文件间接依赖了哪个模块。遇到这种隐式依赖,最好把模块依赖显式化,否则局部构建很容易漏东西。服务端5月31日 23:58
Maven 多模块项目如何管理?聚合和继承该怎么取舍?Maven 多模块项目通常要同时解决两件事:一次构建多个模块,以及让多个模块共享同一套版本、插件和属性配置。聚合解决的是“构建顺序和构建入口”,继承解决的是“公共配置从哪里来”。两者经常放在同一个父 POM 里,但它们不是一回事,也不是必须绑定使用。
一个典型结构会把根目录作为 parent,打包类型设为 `pom`,再通过 `<modules>` 声明子模块。执行根目录的 `mvn clean install` 时,Maven 会读取各模块之间的依赖关系,按 reactor 顺序编译,而不是简单按目录顺序跑。
```xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>demo-api</module>
<module>demo-service</module>
<module>demo-web</module>
</modules>
</project>
```
继承则写在子模块的 `pom.xml` 中。子模块通过 `<parent>` 继承父 POM 里的 `properties`、`dependencyManagement`、`pluginManagement` 等配置,但是否被父项目聚合构建,要看父 POM 有没有把它放进 `<modules>`。
```xml
<parent>
<groupId>com.example</groupId>
<artifactId>demo-parent</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>demo-service</artifactId>
```
## 实际管理思路
在真实项目里,根 POM 最好只承担“统一入口”和“统一约束”两类职责。版本号、编码、Java release、插件版本适合放在父 POM,因为这些配置越分散,越容易出现本地和 CI 不一致。业务模块自己的依赖、打包方式和资源配置,应该留在模块内部,否则父 POM 会慢慢变成谁也不敢改的公共垃圾箱。
聚合模块的边界也要控制清楚。一个仓库里可以有多个聚合入口,例如 `pom.xml` 构建全量模块,`pom-service.xml` 只聚合后端服务模块,但不要让开发者必须记住十几个入口。模块之间依赖要保持单向,常见方向是 web 依赖 service,service 依赖 api 或 domain,底层模块不要反过来引用上层模块。
版本管理上,团队通常会把第三方依赖放进父 POM 的 `dependencyManagement`,把内部模块版本统一成 `${project.version}`。这样发布时能保证同一批模块版本一致,也能减少“api 是 1.0.1、service 还是 1.0.0”的联调问题。边界是公共 parent 如果被很多仓库继承,升级就要更谨慎,最好配合 release notes 和兼容性测试。
另外,父 POM 不一定要发布业务代码,但它本身也要有清晰版本。团队里如果把 parent 当成随手可改的配置文件,子模块升级时就会出现不可追踪的差异。更稳的做法是把公共约束当成产品维护,每次改动都说明影响范围。
## 追问
### 聚合和继承最大的区别是什么?
聚合关心“从哪个入口构建哪些模块”,继承关心“子模块复用哪些配置”。一个模块可以继承某个公司级 parent,但不出现在当前仓库的 `<modules>` 中,这在公共 parent 单独发布时很常见。反过来,一个聚合 POM 也可以只负责组装构建,不给子模块继承配置。踩坑点是把两者混为一谈,结果改了父 POM 版本却发现某些模块根本没有继承到。
### 父 POM 里应该放 dependencies 还是 dependencyManagement?
公共依赖确实可以放进 `<dependencies>`,但这样所有子模块都会自动引入,容易让模块边界变脏。更稳妥的做法是把版本放在 `<dependencyManagement>`,子模块需要时再显式声明依赖。取舍标准很简单:所有模块都必需的基础依赖可以直接放,业务库、驱动、Web 框架这类只给部分模块用的依赖应只管理版本。这样排查冲突时也能看清依赖到底是谁主动引入的。
### 多模块构建时只想编译一个模块怎么办?
可以用 `-pl` 指定模块,用 `-am` 顺带构建它依赖的本仓库模块。例如 `mvn clean install -pl demo-web -am` 会构建 `demo-web` 以及它依赖的 `demo-api`、`demo-service`。如果还想构建依赖它的模块,可以用 `-amd`,但在大仓库里会拉起很多模块,耗时可能反而更长。常见踩坑是只跑 `-pl demo-web`,本地仓库里的上游模块还是旧版本,导致问题看起来像代码没生效。
```bash
mvn clean install -pl demo-web -am
mvn test -pl demo-service
mvn clean install -pl '!demo-web'
```
### 模块应该按业务拆还是按技术层拆?
小项目按技术层拆成 api、service、web 很直观,团队沟通成本低。业务复杂后,更推荐按领域或边界上下文拆模块,否则所有业务都挤在一个 service 模块里,最后还是一个大泥球。取舍点在于依赖方向是否稳定:底层公共模块不应该反向依赖上层业务模块。模块拆得太细也会让构建、发布和版本管理变复杂,所以不要为了“看起来架构好”硬拆。
### 多模块项目最容易遇到什么坑?
第一个坑是循环依赖,例如 `demo-api` 依赖 `demo-service`,`demo-service` 又依赖 `demo-api`,Maven reactor 会直接报错。第二个坑是父 POM 的 `<relativePath>` 写错,IDE 里可能还能识别,命令行构建却解析到仓库里的旧 parent。第三个坑是插件版本没有放进 `<pluginManagement>`,不同模块各跑各的插件版本,CI 上才暴露差异。处理这类问题时,先用 `mvn -q help:effective-pom` 和 `mvn dependency:tree` 看最终配置,比盯着源码猜更可靠。服务端5月31日 23:47
Maven 仓库和镜像该怎么配置才不会拉错依赖?Maven 仓库看似只是下载依赖的地方,实际会影响构建速度、依赖安全和产物可追溯性。一个项目通常会同时接触本地仓库、中央仓库、公司私服和镜像;配置不当时,最常见的问题不是“下载不到”,而是下载到了不该用的版本。理解仓库配置时要分清三个概念:repository 是依赖来源,mirror 是替代某个来源的镜像,server 是认证信息。三者 id 对不上,Maven 不会好心提醒你设计错了,只会在构建时用各种 401、404 或 checksum 错误折磨你。
## Maven 会按什么顺序找依赖?
Maven 先查本地仓库,默认在 `~/.m2/repository`。本地没有时,再根据有效 POM 和 settings 中的远程仓库配置去下载;中央仓库是默认存在的远程仓库。公司项目一般会让所有请求先走 Nexus 或 Artifactory,由私服代理中央仓库并托管内部包,这样既能加速,也能保留依赖审计记录。
```xml
<settings>
<localRepository>/data/maven-repository</localRepository>
</settings>
```
自定义本地仓库适合 CI 缓存或磁盘隔离,但不建议团队成员随意改路径。路径太分散会导致“我本地有、你本地没有”的错觉,尤其是内部 SNAPSHOT 包没有正确发布时。
## 私有仓库应该写在 POM 还是 settings?
项目必须知道的仓库地址可以放 POM,但账号密码应该放 `settings.xml`。更常见的企业做法是 POM 只声明公司仓库 id,settings 里放认证信息和镜像规则。注意 `<server><id>` 必须和仓库 id 对应,否则认证不会套上去。
```xml
<repositories>
<repository>
<id>company-releases</id>
<url>https://repo.example.com/repository/maven-releases/</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
<repository>
<id>company-snapshots</id>
<url>https://repo.example.com/repository/maven-snapshots/</url>
<releases><enabled>false</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
```
```xml
<settings>
<servers>
<server>
<id>company-releases</id>
<username>${env.MAVEN_REPO_USER}</username>
<password>${env.MAVEN_REPO_PASSWORD}</password>
</server>
<server>
<id>company-snapshots</id>
<username>${env.MAVEN_REPO_USER}</username>
<password>${env.MAVEN_REPO_PASSWORD}</password>
</server>
</servers>
</settings>
```
## mirrorOf 怎么配置才安全?
镜像会替代匹配到的仓库,最容易踩坑的是把 `<mirrorOf>*</mirrorOf>` 指向一个不完整的公共镜像。这样内部私服、插件仓库甚至快照仓库都可能被错误替代。企业内网可以用 `*`,前提是私服已经代理了所有需要的远程仓库;个人开发更稳妥的是只镜像 central。
```xml
<mirrors>
<mirror>
<id>central-mirror</id>
<mirrorOf>central</mirrorOf>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
```
## 企业私服配置的安全边界
公司内网通常会把中央仓库、第三方仓库和内部发布仓库统一代理到一个私服入口,这样便于缓存、审计和阻断高风险依赖。问题是私服不是万能兜底,如果没有同步插件仓库或禁用了 snapshot,Maven 仍然会失败。更稳的做法是在 settings 中配置统一 mirror,在 POM 中只声明项目确实需要的仓库,并把发布地址放到 `distributionManagement`。账号密码不要写入 POM,也不要写进团队文档截图里;CI 使用环境变量注入,个人电脑可以用 Maven 的密码加密机制或系统凭据管理。
这样配置之后,依赖从哪里来、谁有权限发布、出了问题该查哪一层,都会清楚很多。
## 追问
### repository 和 mirror 到底有什么区别?
repository 是项目声明“我可以从哪里找依赖”,mirror 是 settings 声明“访问某些仓库时改走这个地址”。mirror 的优先级很高,一旦匹配,原仓库 URL 会被替换。取舍是镜像能统一加速和管控,但配置过宽会导致依赖来源被悄悄改掉。边界是 POM 表达项目需求,settings 表达当前机器或组织的访问策略。
### releases 和 snapshots 为什么要分开?
release 应该稳定、不可变,snapshot 天生表示还在变化。两者混在一个仓库里,缓存策略、清理策略和审计都会变得混乱。分开后的代价是配置多一点,但能避免生产构建误拉 SNAPSHOT。踩坑最多的是依赖版本写了 `1.0-SNAPSHOT`,仓库却禁用了 snapshots。
### 私服认证明明配了,为什么还是 401?
先检查 `<server><id>` 是否和 repository 或 distributionManagement 里的 id 完全一致。Maven 不是按 URL 匹配认证,而是按 id 匹配。另一个坑是密码里有特殊字符,手写 XML 时转义不正确,或者 CI 环境变量没有注入。排查时加 `-X` 可以看到 Maven 选择了哪个仓库,但不要把带密码的日志贴到公共渠道。
### 为什么本地仓库里删了依赖还是拉不到新版本?
可能远程仓库本身没有新版本,也可能 mirror 把请求转到了另一个代理仓库。SNAPSHOT 还受 updatePolicy 影响,Maven 不一定每次都检查远端。取舍上,频繁检查能拿到最新包,但会拖慢构建并增加私服压力。临时排查可以用 `mvn -U clean package`,长期还是要规范发布版本。
### 插件仓库需要单独配置吗?
需要时可以配置 `<pluginRepositories>`,因为插件解析和普通依赖解析是两条配置。很多公司只代理了依赖仓库,却忘了代理 Maven 插件仓库,结果 `maven-compiler-plugin` 或第三方插件下载失败。边界是常用官方插件通常中央仓库能解决,内部自研插件或受限网络环境才需要显式配置。配置后同样要注意 mirror 是否把它覆盖掉。服务端5月31日 23:47
Maven 插件是怎样绑定生命周期并执行构建任务的?Maven 真正干活的是插件。`mvn package` 看起来只是一条生命周期命令,背后会按阶段触发一串插件目标,比如编译、复制资源、运行测试、打包。理解插件时不要只背“plugin、goal、phase”三个词,关键是看它什么时候执行、执行几次、配置从哪里来。很多构建问题并不是 Maven 神秘,而是某个插件目标绑定错阶段,或者父 POM 里的默认配置被子模块悄悄继承了。
## 插件、目标和生命周期是什么关系?
插件是一组能力,目标是插件里的一个具体动作,生命周期阶段只是 Maven 预设的一条时间线。比如 `maven-compiler-plugin` 提供 `compile` 目标,通常绑定在 `compile` 阶段;`maven-surefire-plugin` 负责单元测试,通常在 `test` 阶段运行。你可以直接执行 `mvn compiler:compile`,也可以执行 `mvn test` 让 Maven 按生命周期自动调用绑定好的目标。
```xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>17</release>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
```
`source`、`target` 和 `release` 的选择也有边界。JDK 9 以后更推荐 `release`,它会同时约束语言级别和可用 API;只配 `source/target` 时,代码仍可能误用高版本 JDK 的类库,运行到低版本环境才炸。
## pluginManagement 和 plugins 不要混用错
父 POM 里常用 `pluginManagement` 统一版本,但它只提供默认配置,不会让插件自动执行。真正生效还需要在子模块的 `<plugins>` 中声明,或者由打包类型的默认生命周期绑定触发。这个边界很重要:`pluginManagement` 像菜单,`plugins` 才是点菜。大型项目推荐父 POM 锁版本,子模块按需启用,避免每个模块复制一堆插件配置。
```xml
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
```
## 如何把插件目标绑定到阶段?
需要额外产物时,用 `executions` 明确绑定目标。例如发布 SDK 时经常附带源码包,这可以让 `source:jar-no-fork` 在 `verify` 阶段执行。坑在于同一个插件可以有多个 execution,如果 id、phase、goal 写得含糊,最后可能重复打包或漏执行。
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
```
## 常用插件怎么取舍?
`compiler`、`surefire`、`jar`、`resources` 属于大多数 Java 项目的基础插件,应该在父 POM 里锁定版本。`shade`、`assembly`、`spring-boot-maven-plugin` 这类会重组产物的插件要谨慎使用,因为它们不仅影响构建过程,还会改变最终包的结构。比如 shade 能把依赖合进一个 fat jar,部署方便,但也可能带来类冲突、资源文件覆盖和许可证合规问题。团队里最好把“产物形态”写清楚:普通库只发布 jar,应用服务才打可执行包,不要每个模块都复制同一套打包插件。
这类约定看起来保守,却能减少很多“本地能跑、发布包不能跑”的隐性问题。
## 追问
### 为什么有些插件不用配置也会执行?
因为 Maven 对不同 packaging 有默认生命周期绑定,例如 `jar` 项目默认会编译、测试、打包。这个默认行为降低了入门成本,但也让人误以为所有插件都是“自动发现”的。边界是默认绑定只覆盖常见任务,像生成源码包、Docker 镜像、代码覆盖率通常需要自己声明。排查时可以用 `mvn help:effective-pom` 看最终绑定。
### `pluginManagement` 里配置了插件,为什么没有生效?
`pluginManagement` 只是管理版本和默认配置,不等于启用插件。子模块没有在 `<plugins>` 声明时,它通常不会凭空执行。这样设计的取舍是父 POM 可以统一标准,又不会强迫每个模块跑不需要的插件。踩坑时先看插件到底出现在 effective POM 的哪个位置。
### 跳过测试应该用 `skipTests` 还是 `maven.test.skip`?
`skipTests` 通常只跳过测试执行,测试代码仍可能编译;`maven.test.skip=true` 连测试编译也跳过。前者适合临时加快打包,后者更激进,可能掩盖测试代码长期编译失败的问题。CI 主线不建议长期使用任何跳过测试的开关,除非有单独阶段补回。边界是本地调试可以求快,发布流水线要保守。
### Maven 插件版本要不要每次都写?
建议写,至少在父 POM 里统一锁定。完全依赖 Maven 或超级 POM 的默认版本,会让不同 Maven 版本、不同时间创建的项目表现不一致。取舍是维护版本矩阵多一点工作,但构建可复现性更好。插件升级也要看 release note,特别是 surefire、compiler、shade 这类会影响产物的插件。
### 直接执行 `mvn plugin:goal` 和跑生命周期有什么区别?
直接执行目标只跑你点名的动作,生命周期命令会从前置阶段一路执行到指定阶段。比如 `mvn jar:jar` 可能不会先编译最新代码,而 `mvn package` 会按顺序处理资源、编译、测试、打包。前者适合诊断单个插件,后者适合日常构建。踩坑点是手动执行目标得到一个包,并不代表完整构建流程是健康的。服务端5月31日 23:47
Maven Profile 如何管理多环境配置才不容易出错?Maven Profile 适合解决“同一套项目,在不同构建场景下需要少量差异配置”的问题,比如 dev、test、prod 使用不同资源过滤值,或者 CI 构建时额外打开跳过集成测试的开关。它不适合承载所有运行时配置,更不应该把数据库密码、生产密钥直接写进 `pom.xml`。判断边界很简单:影响构建产物、依赖、插件行为的配置可以放 Profile;应用启动后才读取的配置,优先交给配置中心、环境变量或部署平台。
## Profile 放在哪里更合适?
项目级 Profile 通常写在 `pom.xml`,便于团队共享,适合资源过滤、插件参数、可选依赖这类和项目强相关的内容。用户级 Profile 写在 `~/.m2/settings.xml`,适合仓库、账号、个人本地路径,不建议提交到仓库。全局 Maven 配置也能写 Profile,但团队协作里很少依赖它,因为不同机器的 Maven 安装目录不一致,排查起来很费劲。
```xml
<profiles>
<profile>
<id>dev</id>
<properties>
<app.env>dev</app.env>
<db.url>jdbc:mysql://localhost:3306/demo_dev</db.url>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<app.env>prod</app.env>
<db.url>${env.PROD_DB_URL}</db.url>
</properties>
</profile>
</profiles>
```
如果要让这些属性进入资源文件,需要显式开启 filtering。踩坑最多的是以为定义了 Profile 属性,`application.properties` 就会自动替换;实际上没有资源过滤时,`${app.env}` 会原样留在文件里。
```xml
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
```
## 常见激活方式怎么选?
命令行 `-Pdev` 最直观,也最适合 CI/CD,因为构建脚本里能清楚看到使用了哪个环境。自动激活适合本地便利配置,比如根据 JDK 或操作系统调整插件参数,但它的代价是隐蔽:同一条命令在两台机器上可能得到不同结果。多人项目里,生产 Profile 最好显式激活,不要依赖文件存在、环境变量这种容易被忽略的条件。
```bash
mvn clean package -Pdev
mvn clean package -Pdev,skip-it
mvn help:active-profiles
```
```xml
<profile>
<id>jdk17</id>
<activation>
<jdk>[17,)</jdk>
</activation>
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
</profile>
```
## 实战里怎样控制边界?
Profile 最好只处理“构建时必须确定”的差异,例如资源过滤值、是否附加源码包、是否启用某个测试阶段。像连接池大小、灰度开关、第三方接口地址这类运行期变量,放在应用配置或部署系统里更合适。否则一次重新打包就可能被误认为一次配置变更,回滚时也分不清到底是代码问题还是环境值问题。团队里可以约定只有 `dev`、`test`、`prod` 这类环境 Profile 允许改产物内容,`skip-test`、`local` 这类辅助 Profile 只能影响本地效率,不参与正式发布。
因此,Profile 的价值不是把所有环境差异都塞进 Maven,而是让构建结果可解释、可复现。
## 追问
### Profile 能不能直接管理生产数据库密码?
不建议,尤其不要把密码明文提交到 `pom.xml`。Profile 可以引用环境变量,例如 `${env.PROD_DB_URL}`,但真正的密钥仍应由 CI 密文变量、部署平台或配置中心管理。这样做的取舍是本地复现稍麻烦,但换来的是仓库泄露时不会连带泄露生产凭据。边界在于 Maven 只负责构建,不应该变成运行时密钥仓库。
### `pom.xml` 里的 Profile 和 `settings.xml` 里的 Profile 有什么区别?
`pom.xml` 更适合项目共享配置,任何开发者拉下代码都能得到一致构建规则。`settings.xml` 更适合个人或公司内部环境,比如私服地址、认证账号、本机路径。踩坑点是 `settings.xml` 不随代码提交,CI 机器如果没配置对应 Profile,本地能过、流水线会失败。一般做法是项目必需配置放 POM,机器差异配置放 settings。
### 多个 Profile 同时激活时谁覆盖谁?
同名属性后生效的配置可能覆盖先生效的配置,但不要把业务正确性建立在这种隐式顺序上。多个 Profile 同时改同一个属性,很容易让排查变成猜谜。更稳妥的方式是拆清职责,比如 `dev` 管环境,`skip-it` 管测试开关,避免两个 Profile 都写 `db.url`。如果确实要覆盖,至少用 `mvn help:effective-pom` 看最终结果。
### 为什么资源文件里的占位符没有被替换?
最常见原因是忘了给 resource 开启 `<filtering>true</filtering>`。另一个坑是占位符语法和框架自己的变量冲突,例如 Spring 配置里也常用 `${}`,Maven 过滤可能提前替换掉本该运行时解析的值。取舍上,构建期固定的值可以过滤,运行期变化的值不要过滤。遇到问题先看打包后的 `target/classes` 文件,而不是只看源码。
### CI/CD 里应该怎么用 Maven Profile?
CI 里推荐显式写出 `mvn clean package -Pprod`,不要依赖某个文件是否存在来自动激活。这样日志可追溯,回滚或复现构建时也更清楚。边界是 Profile 只决定构建差异,不应该决定部署到哪台机器;部署目标应该由流水线参数或发布系统控制。踩坑最多的是 dev Profile 被默认激活,结果生产包里混入了测试配置。服务端5月31日 23:47
Maven 和 Gradle 有什么区别?项目该怎么选?Maven 和 Gradle 都能完成 Java 项目的依赖管理、编译、测试和打包,但它们解决问题的方式不一样。Maven 更像一套稳定的约定:目录怎么放、生命周期怎么走、插件怎么绑定,都有明确规则。Gradle 更像可编程的构建平台:你可以用 Groovy 或 Kotlin DSL 写任务、组合逻辑、做缓存和增量构建。选择哪一个,不是看谁更“先进”,而是看项目需要稳定约定,还是需要更强的构建表达能力。
## 配置方式有什么差别?
Maven 使用 XML 写 `pom.xml`,结构清晰,但配置稍显冗长。Gradle 使用 `build.gradle` 或 `build.gradle.kts`,表达更短,也更容易写条件逻辑。XML 的好处是团队成员一眼能看出依赖和插件,DSL 的好处是复杂构建可以少写重复配置。
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
```
```kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
}
```
取舍点在于可读性和自由度。Maven 的限制让项目更标准,Gradle 的自由让构建更灵活,但也可能让构建脚本变成另一套业务代码。团队如果没有人维护 Gradle 脚本,灵活性很快会变成隐性成本。
## 性能和生态有什么区别?
Gradle 在增量构建、构建缓存、配置缓存和多模块构建上通常更有优势,Android 生态也基本围绕 Gradle 展开。Maven 的优势是生态成熟、企业项目存量大、插件行为稳定,出问题时更容易搜索到解决方案。
Maven 的生命周期固定:`validate`、`compile`、`test`、`package`、`verify`、`install`、`deploy`。Gradle 是任务图模型,任务之间可以声明依赖,构建时按图执行。前者适合标准 Java 服务,后者适合需要自定义代码生成、多语言混编、复杂打包流程的项目。
## 依赖管理谁更好?
Maven 的传递依赖规则比较固定,冲突时遵循“最短路径优先、路径相同则先声明优先”。Gradle 的依赖能力更丰富,支持版本约束、能力冲突处理、平台依赖和更细的配置范围。能力越强,规则也越多;如果团队只需要稳定引入依赖,Maven 已经足够。
```bash
mvn dependency:tree
./gradlew dependencies --configuration runtimeClasspath
```
排查依赖时,Maven 的树更直观;Gradle 的配置维度更细,需要知道 `implementation`、`api`、`runtimeOnly` 等作用范围。迁移时最容易踩坑的是把 Maven 的 scope 机械翻译成 Gradle 配置,结果编译期或运行时 classpath 发生变化。
## 追问
### 新项目默认应该选 Maven 还是 Gradle?
普通 Spring 后端服务、团队偏稳定交付、构建逻辑不复杂时,Maven 是省心选择。Android、多语言仓库、大型多模块、需要构建缓存时,Gradle 更值得考虑。边界是团队经验:一个理论上更快的 Gradle 项目,如果没人懂脚本维护,长期成本可能高于 Maven。工具选择首先要服务团队,而不是服务技术偏好。
### Gradle 一定比 Maven 快吗?
不一定。Gradle 的增量构建和缓存配置得当时确实很快,但冷启动、首次下载依赖、脚本配置阶段也会消耗时间。小型项目里 Maven 和 Gradle 的差距可能不明显,甚至 Maven 更简单直接。踩坑点是只看一次本地构建耗时就做结论,应该比较 CI、全量构建、增量构建和失败重试的整体成本。
### Maven 项目有必要迁移到 Gradle 吗?
只有当现有构建真的成为瓶颈时才值得迁移,例如多模块构建太慢、代码生成逻辑复杂、Android 或 Kotlin 生态强依赖 Gradle。迁移会带来插件替换、依赖范围变化、CI 脚本重写和团队学习成本。比较稳妥的做法是先选一个边界清楚的子模块试点,而不是一次性改完整仓库。否则构建工具迁移很容易变成长期悬空的技术债。
### Maven 的 XML 冗长是不是明显缺点?
冗长是缺点,但也带来确定性。很多企业项目宁愿接受重复 XML,也不愿让构建脚本出现太多条件分支和动态逻辑。Gradle DSL 写得好会很优雅,写得差会比 XML 更难读。判断边界很简单:如果构建规则主要是声明依赖和插件,Maven 的冗长通常可以接受;如果规则本身需要编程,Gradle 更合适。
### 两个工具能在同一个团队里并存吗?
可以,但要有清楚边界。比如后端服务统一 Maven,Android 或构建平台模块使用 Gradle,这样大家知道去哪套规范里找答案。最怕的是同类项目一半 Maven、一半 Gradle,CI、依赖升级、安全扫描都要维护两套。并存不是问题,无规则并存才是问题。
如果项目追求标准化、稳定、低学习成本,Maven 仍然很合适;如果项目需要更快的增量构建、更灵活的任务编排和跨语言支持,Gradle 更有空间。真正的选择标准不是工具名,而是构建复杂度、团队经验和未来维护成本。服务端5月31日 23:47
Maven 常用命令怎么用?哪些参数能提高构建效率?Maven 命令看起来很多,其实大部分都围绕三件事:按生命周期构建项目、查看依赖问题、控制构建范围。日常开发不需要背完整手册,但要知道每个命令会跑到哪个阶段、会不会执行测试、会不会把产物写进本地仓库或远程仓库。命令用错,轻则构建慢,重则把不该发布的版本推到仓库。
## 生命周期命令怎么选?
最常用的是 `clean`、`compile`、`test`、`package`、`install` 和 `deploy`。Maven 会从生命周期起点一直执行到你指定的阶段,所以 `mvn package` 不只是打包,它会先编译并运行测试;`mvn install` 会在打包后把产物安装到本地仓库;`mvn deploy` 则会发布到远程仓库,通常只应由 CI 执行。
```bash
mvn clean # 删除 target 目录
mvn compile # 编译主代码
mvn test # 运行单元测试
mvn package # 生成 jar/war
mvn install # 安装到本地仓库
mvn deploy # 发布到远程仓库
```
开发阶段想快速验证语法和依赖,`mvn test` 往往比 `clean install` 更合适。`clean install` 很完整,但它会清理增量结果并写入本地仓库,在大项目中成本不低。只有需要验证全量构建、给其他本地模块引用产物,或 CI 做最终检查时,才更适合用它。
## 依赖排查命令怎么用?
依赖冲突、版本不一致、包重复,是 Maven 项目里最常见的构建问题。`dependency:tree` 用来查看依赖来源,`dependency:analyze` 用来发现声明了但没用、用了却没声明的依赖。前者适合排查版本冲突,后者适合清理 POM。
```bash
mvn dependency:tree
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core
mvn dependency:analyze
mvn dependency:resolve
mvn dependency:sources
```
如果本地依赖损坏,可以用 `dependency:purge-local-repository` 清理后重新下载,但不要把它当成日常加速手段。它会让下一次构建重新拉依赖,网络慢时反而更耗时。遇到 SNAPSHOT 依赖没更新,可以加 `-U` 强制检查远程更新。
```bash
mvn clean test -Dtest=OrderServiceTest
mvn test -DfailIfNoTests=false
mvn help:effective-pom
mvn help:active-profiles
```
定位问题时,`help:effective-pom` 很有用。它会把父 POM、当前 POM、Profile 和默认配置合并后展示出来,比直接盯着一个 `pom.xml` 更接近 Maven 实际看到的内容。边界是输出很长,适合排查“配置到底从哪来”,不适合每次构建都跑。
## 多模块项目怎么少构建一点?
多模块工程最有用的参数是 `-pl`、`-am` 和 `-rf`。`-pl` 指定构建哪些模块,`-am` 会顺带构建这些模块依赖的上游模块,`-rf` 可以从失败模块继续构建。它们的价值不在“命令高级”,而在少跑无关模块。
```bash
mvn test -pl order-service -am
mvn package -pl '!legacy-module'
mvn install -rf payment-service
mvn clean install -T 1C
```
`-T` 可以并行构建,例如 `-T 1C` 表示每个 CPU 核心一个线程。边界是并行只对模块间依赖清楚、插件线程安全的项目效果好。如果某些插件会写同一个文件,或者测试依赖共享端口,并行构建可能把偶发失败放大。
## 追问
### `-DskipTests` 和 `-Dmaven.test.skip=true` 有什么区别?
`-DskipTests` 会跳过测试执行,但通常仍会编译测试代码。`-Dmaven.test.skip=true` 会跳过测试编译和测试执行,速度更快,但也更容易掩盖测试代码编译失败。日常临时打包可以用前者,CI 主流程不建议长期跳测试。踩坑点是测试工具类被主代码误引用时,跳过测试编译可能让问题延后暴露。
### 什么时候用 `mvn install`,什么时候只用 `mvn package`?
如果只是看当前项目能不能打包,`package` 就够了。`install` 会把产物写入 `~/.m2/repository`,适合本地另一个项目要引用这个 SNAPSHOT 包的场景。它的副作用是本地仓库可能残留旧快照,让你误以为代码已经同步。多人协作时,真正共享的版本应该走远程仓库,而不是靠各自本地 install。
### 为什么 `dependency:tree` 查到了冲突,运行时还是报错?
`dependency:tree` 只能告诉你 Maven 解析出的依赖路径,不保证运行环境和它完全一致。应用服务器、自带 lib、Spring Boot 打包插件、shade 插件都可能改变最终 classpath。排查时要结合最终包内容,例如查看 `BOOT-INF/lib` 或运行时启动日志。边界是:构建期依赖正确,不等于部署形态一定正确。
### `-o` 离线模式适合所有场景吗?
不适合。`mvn -o` 只使用本地仓库,网络不可用或 CI 缓存充分时很有用。它的前提是所有依赖、插件和父 POM 都已经存在本地,否则构建会直接失败。常见踩坑是只缓存了依赖,没缓存插件,结果离线构建卡在插件解析。
### 命令参数写得越多越专业吗?
不是。参数越多,构建语义越难复现,尤其是 `-D` 覆盖属性、`-P` 激活 profile、跳过插件这些参数。团队里最好把稳定规则写进 POM 或 CI 脚本,把临时参数留给本地排查。否则同一个项目会出现“我的命令能过、你的命令不过”的情况。
把 Maven 命令分成构建、依赖排查、多模块加速三类,基本就能覆盖日常 80% 的问题。真正值得记住的不是命令数量,而是每个命令会改变哪些边界:是否清理、是否测试、是否安装、是否发布、是否只构建局部模块。服务端5月31日 23:47
Maven POM 文件怎么配置才清晰?核心元素有哪些?POM 是 Maven 项目的说明书,也是构建时最先被读取的配置入口。一个项目能不能稳定编译、依赖会不会乱、插件版本是否可控,很多时候不取决于命令写得多熟,而取决于 `pom.xml` 有没有把边界写清楚。POM 不需要堆满配置,真正重要的是坐标、依赖、版本管理和构建插件这几块各司其职。
## POM 的核心元素怎么分工?
最基础的是项目坐标:`groupId`、`artifactId`、`version` 和 `packaging`。`groupId` 通常用组织域名倒序,`artifactId` 是模块名,`version` 表示当前产物版本,`packaging` 决定最终打成 `jar`、`war` 还是只作为父工程的 `pom`。坐标一旦发布到仓库,就会被其他项目当作依赖引用,所以不要随意改名。
```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>
<groupId>com.example</groupId>
<artifactId>order-service</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
</project>
```
`properties` 适合放 Java 版本、编码、依赖版本这类会被复用的值。它的好处是集中修改,代价是过度抽象后不容易追踪真实版本。团队项目里可以把 Spring、MyBatis、JUnit 等版本写成属性,但不要把每个只用一次的小依赖都抽成变量。
`dependencies` 声明当前模块真的要用的依赖;`dependencyManagement` 只管理版本,不会自动引入依赖。这是很多人踩坑的地方:父 POM 里写了 `dependencyManagement`,子模块仍然需要在 `dependencies` 中声明依赖,只是可以省略版本。
```xml
<properties>
<java.version>17</java.version>
<spring.boot.version>3.2.5</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>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
```
## build 和插件应该怎么写?
`build` 负责资源处理、编译、测试、打包等构建行为。插件版本最好固定在父 POM 的 `pluginManagement` 中,子模块按需启用。这样既能避免不同机器拉到不同插件版本,也不会让每个模块都复制一大段配置。
```xml
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>${java.version}</release>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
```
父子 POM 适合多模块工程。父 POM 用 `packaging=pom`,统一管理依赖版本、插件版本和公共属性;子模块通过 `parent` 继承。取舍点在于:公共配置越集中越容易维护,但父 POM 过重会让所有模块被迫背上无关配置。
```xml
<parent>
<groupId>com.example</groupId>
<artifactId>platform-parent</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
```
`profiles` 可以处理环境差异,例如本地、测试、生产使用不同资源过滤或插件开关。不过它不适合承载业务配置,更不应该把数据库密码这类敏感信息直接写进 POM。更稳妥的做法是让 POM 只决定构建差异,运行时配置交给环境变量、配置中心或部署平台。这样构建产物更容易复现,也能减少“换个环境就重新打包”的问题。
## 追问
### dependencies 和 dependencyManagement 有什么区别?
`dependencies` 会真正把依赖加入当前模块的 classpath,编译和运行都可能用到。`dependencyManagement` 只是提供版本约束,子模块没有声明依赖时不会自动生效。这个设计的好处是父 POM 能统一版本,又不会强行污染每个模块。踩坑最多的是以为导入 BOM 后依赖就存在,结果编译时报类找不到。
### parent、modules 和 dependencyManagement 能互相替代吗?
不能。`parent` 是继承配置,`modules` 是聚合构建,`dependencyManagement` 是版本管理。一个父工程可以同时做继承和聚合,但大型仓库里也常把它们拆开,避免构建范围过大。边界是:如果只是统一版本,不一定要把所有项目都放进同一个聚合工程。
### POM 里应该直接写仓库 repositories 吗?
一般不建议在业务项目里随意写公共仓库地址,企业项目更适合在 `settings.xml` 或私服里统一配置镜像。POM 中写仓库会影响所有使用该项目的人,甚至让构建依赖某个不稳定的外部源。只有项目确实依赖特殊仓库,且团队接受这个约束时,才应该写进 POM。否则后续排查“为什么我机器能构建、CI 不行”会很麻烦。
### 版本号放 properties 还是直接写在依赖上?
被多个依赖或多个模块共享的版本适合放进 `properties`,例如 Spring Boot、Jackson、JUnit。只出现一次的小依赖直接写版本反而更清楚。过度属性化会让读 POM 的人频繁跳转,维护成本上升。一个实用边界是:能被统一升级的版本集中管理,临时依赖不要为了“整齐”硬抽象。
### POM 文件越完整越好吗?
不是。Maven 的默认约定已经覆盖了目录结构、编译阶段和基础生命周期,很多配置不写反而更稳定。真正需要显式写的是版本、插件行为差异、资源过滤和发布规则。把网上模板整段复制进来,常见后果是插件互相覆盖、构建阶段重复执行,最后没人敢改。
POM 写得好不是 XML 多,而是项目坐标稳定、依赖边界清楚、版本来源可追踪。先让默认约定发挥作用,再把团队确实需要统一的内容放进父 POM,通常比一开始就写“大而全模板”更可靠。服务端5月31日 23:47
Maven 资源过滤怎么管理多环境配置?Maven 资源过滤是在构建时把资源文件里的占位符替换成真实值,例如把 `${env}`、`${jdbc.url}` 写进配置文件,再由 Maven 根据 profile 或属性替换。它适合处理少量构建期差异,比如应用名、版本号、环境标识、默认地址。它不适合管理密码、密钥和频繁变化的运行时配置,因为这些内容一旦打进包里,修改就要重新构建。
## 基本配置
在 POM 里开启过滤时,最好只针对明确的文本文件:
```xml
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*.properties</include>
<include>**/*.yml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>**/*.properties</exclude>
<exclude>**/*.yml</exclude>
</excludes>
</resource>
</resources>
</build>
```
资源文件中可以这样写:
```properties
app.name=${project.artifactId}
app.version=${project.version}
app.env=${deploy.env}
```
多环境值可以放在 profile 里:
```xml
<profiles>
<profile>
<id>prod</id>
<properties>
<deploy.env>prod</deploy.env>
<api.baseUrl>https://api.example.com</api.baseUrl>
</properties>
</profile>
</profiles>
```
构建时指定环境:
```bash
mvn clean package -Pprod
```
## 追问
### Maven 资源过滤和 Spring Profile 有什么区别?
Maven 资源过滤发生在构建期,打包后内容已经被替换。Spring Profile 发生在运行期,同一个包可以通过启动参数切换环境。取舍上,如果配置跟构建产物强相关,例如版本号、构建时间,可以用 Maven;如果配置跟部署环境相关,例如数据库地址、开关状态,更适合运行期 Profile 或配置中心。把所有环境都打成不同包会增加发布复杂度,也容易出现“测试包和生产包不是同一个产物”的问题。
### 为什么不建议过滤整个 resources 目录?
因为 resources 里不一定全是文本文件。图片、字体、证书、Excel 模板等二进制文件如果被过滤,可能会被改坏,错误还不一定在构建阶段暴露。更稳妥的方式是只 include `properties`、`yml`、`xml` 这类确实需要替换的文件。边界是 XML 里如果包含 `${}` 但不是 Maven 变量,也要小心误替换。
### 占位符和业务框架的 `${}` 冲突怎么办?
这是资源过滤最常见的坑。Spring、Logback、Shell 模板里也大量使用 `${}`,Maven 过滤可能提前把它们处理掉。可以改用自定义分隔符,或者只对少数配置文件开启过滤。示例做法是在 resources 插件中配置 delimiter,让 Maven 只替换 `@name@` 形式的变量。
```xml
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<delimiters>
<delimiter>@</delimiter>
</delimiters>
<useDefaultDelimiters>false</useDefaultDelimiters>
</configuration>
</plugin>
```
### 密码和 Token 能不能放进过滤文件?
不建议。资源过滤会把值写进最终 jar、war 或镜像里,任何能拿到产物的人都可能反编译或解压看到配置。密码、Token、证书路径这类敏感信息应该来自环境变量、密钥服务、Kubernetes Secret 或配置中心。Maven 可以写默认占位符,但不要把真实密钥放进 POM、filter 文件或 Git 仓库。
### 多环境配置应该打多个包还是一个包多环境运行?
如果是传统部署,多个 profile 打多个包看起来简单,但会增加制品数量,也让“哪个包上线了”变得难追踪。现代 CI/CD 更推荐一次构建生成一个不可变制品,部署时通过环境变量或配置中心注入差异。Maven 资源过滤适合补充构建元信息,不适合作为完整环境管理系统。除非你的发布流程明确要求按环境出包,否则一个包多环境运行更稳。
资源过滤很好用,但它解决的是构建期替换,不是配置治理。把它用在版本号、环境名、少量默认地址上,会让包更清晰;把它用来塞满所有环境变量和密钥,后面维护成本会很高。服务端5月31日 23:47
Maven Archetype 怎么用,什么时候该自定义模板?Maven Archetype 可以理解成 Maven 的项目模板:它把目录结构、POM、示例代码和默认配置打包起来,让新项目不用从空文件夹开始搭。它适合重复创建结构相似的项目,比如公司内部的 Spring Boot 服务、SDK 工程、插件工程。真正要注意的是,Archetype 不是越复杂越好,模板太重会把过时配置复制到每个新项目里。
## 快速生成项目
使用官方 quickstart 模板可以快速创建一个普通 Java 项目:
```bash
mvn archetype:generate -DgroupId=com.example -DartifactId=demo-app -Dversion=1.0.0-SNAPSHOT -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
```
Web 项目也可以使用 `maven-archetype-webapp`,但现在很多团队更倾向用 Spring Initializr 创建 Spring Boot 项目。Archetype 的价值更多体现在公司内部模板:统一包名、日志配置、基础依赖、CI 文件和代码规范。
## 自定义 Archetype 的关键文件
模板工程通常包含 `archetype-metadata.xml`,用来声明哪些文件参与生成、哪些文件需要变量替换。
```xml
<archetype-descriptor name="service-template">
<fileSets>
<fileSet filtered="true" packaged="true">
<directory>src/main/java</directory>
<includes>
<include>**/*.java</include>
</includes>
</fileSet>
<fileSet filtered="true">
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
</includes>
</fileSet>
</fileSets>
</archetype-descriptor>
```
生成和安装模板常用命令如下:
```bash
mvn archetype:create-from-project
cd target/generated-sources/archetype
mvn install
```
## 追问
### Maven Archetype 和直接复制项目模板有什么区别?
直接复制最快,但容易把旧包名、旧 artifactId、无用配置一起复制过去。Archetype 会在生成时替换 `groupId`、`artifactId`、`package` 等变量,更适合标准化创建。它的边界是模板维护成本更高,尤其当基础依赖升级时,需要同步更新模板。小团队项目少时复制模板够用,项目数量多或规范要求强时再上 Archetype 更划算。
### 自定义 Archetype 里哪些文件应该做过滤?
需要替换变量的文本文件适合过滤,比如 Java 类、POM、YAML、properties。二进制文件、图片、证书和压缩包不要开启过滤,否则可能被 Maven 当文本处理后损坏。一个常见坑是对所有 resources 都开 `filtered=true`,结果字体、图片或 keystore 构建后不可用。更稳的做法是只 include 明确需要替换的文件类型。
### Archetype 适合生成 Spring Boot 项目吗?
可以,但不一定是首选。Spring Boot 项目如果只是选择依赖和版本,Spring Initializr 更方便,生态支持也更及时。Archetype 更适合加入公司内部规范,例如统一异常结构、日志埋点、健康检查、Dockerfile 和 CI 模板。取舍点是你要通用生态模板,还是要强绑定企业工程规范。
### 模板里要不要放很多示例业务代码?
不建议放太多。示例代码越多,新项目删除成本越高,也更容易留下没人维护的假业务逻辑。可以保留一个最小可运行的 Controller、配置类或单元测试,用来证明生成项目能启动。边界是模板应表达工程骨架,而不是替业务团队提前设计业务分层。
### 自定义 Archetype 发布后怎么让团队使用?
可以先把模板安装到本地仓库验证,再发布到公司 Nexus 或 Artifactory。使用时指定 archetype 坐标即可:
```bash
mvn archetype:generate -DarchetypeGroupId=com.company -DarchetypeArtifactId=service-archetype -DarchetypeVersion=1.2.0 -DgroupId=com.company.demo -DartifactId=order-service -DinteractiveMode=false
```
模板版本也要像普通依赖一样管理。每次更新基础依赖或目录规范,都应发布新版本,而不是覆盖旧版本;否则老项目排查生成来源时会很痛苦。服务端5月31日 23:47
Maven 依赖版本到底该怎么管理?Maven 依赖版本管理的核心目标不是“写得越新越好”,而是让团队能稳定复现同一次构建。一个项目里常见的问题是:直接依赖写了版本,传递依赖又带来另一个版本,父 POM 或 BOM 还会覆盖一部分版本。最后代码能不能跑,往往取决于 Maven 的冲突调解规则,而不是你以为的那个版本。
## 推荐的版本管理方式
普通业务项目建议把版本集中放到 `dependencyManagement`,业务模块只声明依赖,不重复写版本。
```xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
```
排查版本时,先看依赖树,不要靠猜:
```bash
mvn dependency:tree -Dincludes=org.slf4j
mvn help:effective-pom
```
Maven 遇到版本冲突时通常采用“路径最近者优先”,路径一样近时再看声明顺序。这意味着传递依赖的版本可能被更近的直接依赖覆盖,也可能因为父 POM 的管理而变化。
## 追问
### `dependencyManagement` 和 `dependencies` 有什么区别?
`dependencyManagement` 只负责规定版本和范围,本身不会把依赖加入项目。真正引入依赖的是 `dependencies`,只是它可以省略已经被管理的版本。这样做的好处是多模块项目版本统一,坏处是新同事容易误以为写进 `dependencyManagement` 就已经生效。边界很清楚:要用就必须在模块的 `dependencies` 里声明。
### Maven 版本范围能不能用于生产项目?
技术上可以,比如 `[1.0,2.0)` 表示使用 1.x 范围内的版本。但生产项目一般不建议这么做,因为同一份代码在不同时间可能解析出不同依赖,构建不可复现。它更适合内部库探索或临时验证,不适合发布链路。真正上线时应固定版本,并把升级动作变成一次明确的代码变更。
### SNAPSHOT 版本适合长期依赖吗?
不适合。SNAPSHOT 的语义是“还在变化”,远程仓库里的内容可能被重新发布,同一个版本号对应的二进制不一定相同。团队内部联调可以短期使用,但发布分支和生产构建应切到 release 版本。常见踩坑是 CI 缓存了旧 SNAPSHOT,本地又拉到了新 SNAPSHOT,问题很难复现。
### BOM 和父 POM 该怎么取舍?
父 POM 能继承插件、属性、依赖管理和构建配置,适合公司内部统一工程规范。BOM 只导入依赖版本管理,不强迫项目继承某个父结构,更适合库或多技术栈项目。Spring Boot 项目如果已经继承了公司父 POM,可以用 import BOM 的方式管理 Spring 生态版本。取舍点在于你要的是“全套构建规范”,还是“只统一依赖版本”。
### 发现依赖冲突时怎么处理更稳?
先用 `mvn dependency:tree` 找到冲突来源,再决定是显式声明直接依赖、在 `dependencyManagement` 里统一版本,还是排除某个传递依赖。不要一上来就到处写 `<exclusions>`,排除太多会让依赖图变得难维护。示例:
```xml
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
```
版本管理做得好,升级会变成可审查、可回滚的小变更;做得差,项目会被传递依赖牵着走。Maven 的规则并不复杂,关键是把版本来源集中起来,并让每次升级都有明确理由。服务端5月31日 23:47
Maven 构建速度慢该怎么优化?Maven 构建慢,通常不是某一个开关没打开,而是依赖下载、测试执行、插件配置、多模块顺序和 CI 缓存一起拖慢了速度。优化时不要先急着跳过所有检查,先用 `mvn -X`、CI 日志或构建耗时统计看瓶颈在哪:是下载依赖慢、单元测试慢、编译慢,还是打包插件慢。真正稳妥的做法,是把“本地开发更快”和“流水线发布可靠”分开配置。
## 常用优化配置
本地开发可以优先使用并行构建和跳过非必要步骤:
```bash
mvn clean install -T 1C
mvn test -pl user-service -am
mvn package -DskipTests
```
`-T 1C` 表示按 CPU 核心数并行构建,适合多模块项目;`-pl` 指定模块,`-am` 自动构建它依赖的上游模块。CI 中更推荐缓存本地仓库,并固定 Maven、JDK 和插件版本,避免每次构建都重新解析依赖。
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<reuseForks>true</reuseForks>
<forkCount>1C</forkCount>
</configuration>
</plugin>
```
测试很慢时,不要只知道 `-DskipTests`。它会编译测试但不执行;`-Dmaven.test.skip=true` 连测试编译也跳过,速度更快,但更容易把测试代码编译错误带到后面才暴露。
## 追问
### Maven 并行构建一定会更快吗?
不一定。多模块项目之间如果依赖链很长,Maven 仍然要按依赖顺序执行,能并行的部分有限。并行构建还会放大线程安全问题,一些老插件或自定义插件没有标注 thread-safe,可能出现偶发失败。取舍上,本地开发可以大胆用 `-T 1C`,发布流水线则要先跑几轮稳定性验证,再决定是否启用。
### `-DskipTests` 和 `-Dmaven.test.skip=true` 怎么选?
`-DskipTests` 只跳过测试执行,测试源码仍会编译,因此能发现测试代码里的编译错误。`-Dmaven.test.skip=true` 会跳过测试编译和执行,适合临时打包或验证非测试相关改动。坑在于它可能掩盖测试代码与主代码 API 不兼容的问题,等到合并或发布阶段才炸。团队里一般把前者用于普通开发,后者限制在明确知道风险的临时场景。
### 多模块项目只构建一个模块会不会漏东西?
如果只写 `-pl order-service`,Maven 只构建这个模块,依赖的本地模块可能不会自动更新。通常要配合 `-am`,让 Maven 把它依赖的上游模块一起构建。反过来,`-amd` 会构建依赖当前模块的下游模块,更适合底层公共模块改动后的影响面验证。边界是模块关系必须在 reactor 里可见,跨仓库依赖仍然需要先安装或发布到仓库。
### CI 里优化 Maven 构建最值得做什么?
最先做缓存,而不是先砍测试。缓存 `~/.m2/repository` 能减少大量依赖下载时间,尤其是依赖多、网络不稳定的流水线。其次固定插件版本,否则 Maven 可能解析到不同插件版本,构建时间和结果都不稳定。需要注意的是,缓存也可能带来脏依赖问题,遇到无法解释的构建错误时要支持手动清缓存重跑。
### Maven 编译插件有哪些容易忽略的配置?
最常见的是没有固定 `maven-compiler-plugin` 版本和 Java release,导致本地和 CI 使用不同 JDK 时行为不一致。推荐用 `release` 而不是只写 `source`、`target`,它能同时限制可用 API。示例配置如下:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
```
速度优化最终要服务于稳定交付。开发机上可以用并行、局部构建和跳过测试节省时间;主干和发布流水线则要保留必要校验,只把依赖缓存、插件固定和模块拆分做到位。服务端5月31日 22:22
Maven 项目如何接入 CI 才能稳定构建和发布?Maven 接入 CI 的目标不是把本地命令搬到流水线上,而是让每次提交都在干净、可重复、可追踪的环境里完成编译、测试、打包和必要的检查。好的 Maven CI 会固定 JDK 和 Maven 版本,缓存依赖但不污染构建,上传测试报告和构建产物,并把 deploy 限制在受保护分支或手动发布阶段。做不到这些,流水线就只是另一台“同事电脑”。
## 最小可用的 Maven CI
GitHub Actions 可以从这份配置开始:
```yaml
name: Maven CI
on:
push:
branches: [ main, master, develop ]
pull_request:
branches: [ main, master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven
- name: Build and test
run: ./mvnw -B --no-transfer-progress clean verify
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: surefire-reports
path: '**/target/surefire-reports/*.xml'
```
如果项目还没加 Maven Wrapper,也可以用 `mvn -B clean verify`,但长期建议改成 `./mvnw`。`verify` 比 `test` 更适合作为默认阶段,因为它会覆盖集成测试、打包前检查和部分插件绑定目标。
GitLab CI 里要注意本地仓库路径和缓存:
```yaml
stages: [build, test, package]
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
cache:
key: maven-${CI_COMMIT_REF_SLUG}
paths:
- .m2/repository
build:
image: maven:3.9.6-eclipse-temurin-17
stage: build
script:
- mvn -B --no-transfer-progress clean verify
artifacts:
when: always
reports:
junit: '**/target/surefire-reports/TEST-*.xml'
paths:
- target/*.jar
```
Jenkins 用 Docker agent 时,最好挂载 Maven 仓库缓存,但不要让多个项目共享到互相污染:
```groovy
pipeline {
agent {
docker {
image 'maven:3.9.6-eclipse-temurin-17'
args '-v $WORKSPACE/.m2:/root/.m2'
}
}
stages {
stage('Verify') {
steps { sh 'mvn -B --no-transfer-progress clean verify' }
}
}
post {
always { junit '**/target/surefire-reports/*.xml' }
}
}
```
发布阶段要和普通 CI 分开。PR 只跑 verify,主分支可以打包并上传 artifact,deploy 应只在 tag、release 分支或人工审批后执行。涉及私服发布时,用 CI Secret 生成临时 `settings.xml`,不要把账号密码写进仓库。
质量检查可以按成本分层。每个 PR 都跑单元测试、编译和基础静态检查;依赖漏洞扫描、集成测试、性能测试可以放在主分支或夜间任务。这样反馈速度和质量保障能兼顾,不会因为流水线太慢让开发者绕开它。Maven 里可以把慢测试绑定到 Failsafe,再通过 profile 控制是否执行,例如 PR 跳过外部依赖集成测试,合并后再跑完整验证。
发布制品时还要区分 snapshot 和 release。普通分支可以部署到 snapshot 仓库,正式 tag 才允许部署到 releases 仓库。`distributionManagement` 负责声明地址,CI 条件负责限制何时执行 `mvn deploy`。如果公司要求制品可追溯,还可以在构建时写入 Git commit、构建号和时间,但这些信息最好进入 manifest 或 build-info 文件,不要影响依赖坐标本身。
流水线还应该保留足够的失败现场。测试报告、构建日志、关键产物和覆盖率报告最好在失败时也上传,否则排查只能重新跑。对于偶发失败,保留 surefire dump、failsafe report 和容器日志尤其重要。CI 的价值不只是挡住坏代码,也要让失败原因尽快暴露。
## 追问
### CI 中应该用 clean install 还是 clean verify?
多数项目默认用 `clean verify` 更合适。`install` 会把产物写进本地仓库,可能掩盖模块依赖声明不完整的问题,也会污染缓存。只有后续步骤确实需要从本地仓库解析当前项目产物时,才考虑 install。常见坑是上一次流水线留下的本地 SNAPSHOT 被复用,本次代码其实没构建对也能通过。
### Maven 依赖缓存会不会带来风险?
会,但可以控制。缓存能显著减少下载时间,尤其是多模块项目和频繁 PR 构建;风险是缓存损坏、SNAPSHOT 过期或不同分支互相影响。取舍上,release 构建应更保守,必要时清理 SNAPSHOT 或使用独立 cache key。踩坑点是缓存整个 `.m2` 后私服里已删除的坏包仍被 CI 使用,导致问题复现不了。
### 多模块项目如何加快 CI?
可以用 Maven 并行构建,比如 `mvn -T 1C clean verify`,也可以根据变更模块做增量构建。并行适合测试隔离做得好的项目,增量适合模块边界清晰的大仓。边界是不要为了速度牺牲可信度,公共模块、父 POM 或依赖管理变化时必须扩大构建范围。常见坑是测试共用端口、临时目录或数据库,并行后偶发失败,看起来像代码不稳定,其实是测试隔离差。
### CI 里如何处理私服账号和 settings.xml?
账号密码应放在 CI Secret,通过模板生成 settings.xml,或者使用平台提供的 Maven server 配置。POM 只写 repository id 和 distributionManagement,不写凭据。取舍是配置稍麻烦,但安全边界清楚,离职和轮换密钥也容易处理。坑在于 settings 里的 server id 和 POM 里的 repository id 不一致,deploy 阶段才报 401。
### 测试、质量检查和发布应该放在同一条流水线吗?
可以在同一个 workflow 里分 job,但触发条件要分开。PR 阶段跑编译、单测、静态检查;主分支可以产出制品;发布需要 tag、手动触发或审批。这样既不拖慢每个 PR,也能保证发布入口受控。常见坑是 push 到任意分支都执行 `mvn deploy`,轻则私服堆满无意义版本,重则把未审核代码发布出去。
## 小结
Maven CI 的稳定性来自几条基本规则:固定 JDK 和 Maven,优先跑 `clean verify`,缓存依赖但隔离发布,报告测试结果,凭据只走 Secret。流水线越复杂,越要把“验证”和“发布”分开,否则出问题时很难判断是代码失败、环境失败,还是发布策略本身有漏洞。服务端5月31日 22:22
Maven Wrapper 如何保证团队和 CI 构建一致?Maven Wrapper 的作用很直接:项目自己带一套启动脚本,第一次执行时下载指定版本的 Maven,以后团队成员和 CI 都用同一个 Maven 版本构建。它解决的不是依赖版本问题,而是构建工具版本问题。很多“我本地能构建、你那里不行”的问题,根源不是代码,而是 Maven 3.6、3.8、3.9 对插件解析、HTTP 仓库拦截、传输协议的行为不同。
## 如何添加 Maven Wrapper
在已有 Maven 的机器上执行一次:
```bash
mvn -N wrapper:wrapper -Dmaven=3.9.6
```
生成后的文件应提交到版本库:
```text
.mvn/wrapper/maven-wrapper.properties
.mvn/wrapper/maven-wrapper.jar
mvnw
mvnw.cmd
```
`maven-wrapper.properties` 里最关键的是 `distributionUrl`:
```properties
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
```
之后本地和 CI 都不要再直接写 `mvn clean install`,而是写:
```bash
./mvnw -B clean verify
```
Windows 使用:
```bat
mvnw.cmd -B clean verify
```
如果项目需要固定 JVM 参数,可以放在 `.mvn/jvm.config`;如果需要默认 Maven 参数,可以放在 `.mvn/maven.config`。例如:
```text
文件:.mvn/maven.config
--no-transfer-progress
-Dstyle.color=always
```
CI 配置也很简单:
```yaml
name: Maven Wrapper CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven
- run: chmod +x mvnw
- run: ./mvnw -B clean verify
```
企业内网环境常见问题是无法访问 Maven 官方下载地址。可以把 `distributionUrl` 改成内部 Nexus 或 Artifactory 的 Maven zip 地址,但要保证这个地址长期可用。不要把下载后的 Maven 目录提交进仓库,`.mvn/wrapper/dists/` 应该忽略。
Wrapper 还应该和 JDK 版本一起管理。Maven 固定为 3.9.6,但 CI 用 JDK 11、本地用 JDK 17,构建结果仍可能不同。可以在 README、CI 配置、Dockerfile 或 `.java-version` 中明确 JDK,必要时用 Maven Enforcer Plugin 检查 Java 和 Maven 版本。这样失败会发生在构建早期,而不是编译一半后报出一串难懂的插件错误。
安全方面也不要完全忽略。`distributionUrl` 指向的是构建工具下载地址,如果被改到不可信源,等于所有构建机器都会执行来路不明的工具链。企业项目可以使用内部制品库代理 Maven distribution,并配合代码评审关注 wrapper 文件变更。升级 Wrapper 时不要只看能不能构建,还要确认下载地址、版本号和脚本内容都符合团队规范。
在多人协作里,README 也要同步改成 Wrapper 命令。文档写 `mvn test`,CI 写 `./mvnw verify`,新人通常会照文档执行,最后又回到工具版本不一致的问题。更好的做法是所有示例命令都用 `./mvnw`,Windows 用户旁边补一行 `mvnw.cmd`,避免靠口头提醒。
如果仓库启用了 Dependabot 或 Renovate,也要把 Wrapper 升级纳入规则。依赖升级只改 POM,工具链升级改 wrapper,两类变更最好分开提交,回滚时才清楚影响范围。 这样 CI 失败时,定位会更快。评审时看到 wrapper 文件变化,不要只看版本号,还要确认 distributionUrl 没被改到临时地址。
## 追问
### Maven Wrapper 和固定 pom 里的插件版本有什么区别?
Wrapper 固定的是 Maven 运行器版本,POM 固定的是项目依赖和插件版本。两者解决的问题不同,不能互相替代。取舍上,小项目只固定插件版本也能跑,但团队协作和 CI 最好连 Maven 本身一起固定。坑在于 Maven 版本升级后传输策略变了,比如旧 HTTP 仓库被拦截,POM 没动但构建突然失败。
### maven-wrapper.jar 要不要提交到 Git?
多数团队会提交,这样首次构建不依赖额外下载 wrapper jar,只下载 Maven distribution。也可以不提交 jar,使用下载器模式,但这增加了首次构建的不确定性。取舍点是仓库洁癖和构建稳定性,企业项目通常更偏向稳定。踩坑最多的是只提交 `mvnw`,没提交 `.mvn/wrapper`,新人执行脚本时才发现缺文件。
### Maven Wrapper 能不能解决依赖下载慢?
不能直接解决。它只保证用哪个 Maven 版本,依赖下载速度仍取决于仓库镜像、网络和缓存。可以配合 `settings.xml` 的 mirror、CI cache、公司制品仓库来加速。边界要分清:Wrapper 管工具,settings 管仓库;把镜像地址硬塞进 wrapper properties 只能影响 Maven 本体下载,不会影响项目依赖。
### 什么时候需要升级 Maven Wrapper?
当 Maven 版本存在安全修复、公司统一升级、插件要求更高 Maven 版本,或者旧版本和当前 JDK 不兼容时需要升级。升级命令可以用 `./mvnw -N wrapper:wrapper -Dmaven=3.9.6`,然后检查 properties 和脚本变化。取舍是新版本带来修复,也可能带来更严格的校验。常见坑是只改 `distributionUrl`,没有更新 wrapper 相关脚本,导致不同系统上的行为不一致。
### CI 里已经装了 Maven,还需要 Wrapper 吗?
仍然建议用 Wrapper。CI 镜像会升级,托管平台也可能调整预装工具版本,用 Wrapper 可以让构建入口由项目控制。代价是第一次下载 Maven 会多一点时间,但缓存后影响很小。坑在于 CI 脚本一部分用 `mvn`,一部分用 `./mvnw`,排查问题时很难确认到底是哪套 Maven 在执行。
## 小结
Maven Wrapper 是构建一致性的低成本保险。把 wrapper 文件提交进仓库,本地和 CI 统一使用 `./mvnw`,再配合固定 JDK、Maven cache 和公司 settings,构建环境就不会靠口头约定维持。它不解决所有 Maven 问题,但能先把“工具版本不一致”这类噪音降下来。服务端5月31日 22:22
Maven Assembly Plugin 如何打出可交付分发包?Maven Assembly Plugin 解决的是“交付物不只是一个 JAR”的问题。很多命令行工具、离线部署包、传统服务程序需要 `bin/`、`conf/`、`lib/`、`README`、启动脚本一起交付,这时单纯的 `mvn package` 不够,Assembly 可以按描述符把文件和依赖组织成 zip、tar.gz 或 jar-with-dependencies。它不负责解决依赖冲突,也不负责容器化部署;它负责把已经构建好的东西按你指定的目录结构装箱。
## 什么时候该用 Assembly
如果你只是做 Spring Boot 应用,优先使用 `spring-boot-maven-plugin` 打可执行 JAR。Assembly 更适合通用 Java 程序、批处理工具、带脚本和配置文件的服务包,或者需要同时产出二进制包和源码包的项目。最常见目录是:
```text
demo-1.0.0/
bin/start.sh
bin/stop.sh
conf/application.yml
lib/demo-1.0.0.jar
lib/*.jar
README.md
```
POM 中引用自定义描述符:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<configuration>
<descriptors>
<descriptor>src/assembly/distribution.xml</descriptor>
</descriptors>
<appendAssemblyId>true</appendAssemblyId>
</configuration>
<executions>
<execution>
<id>make-dist</id>
<phase>package</phase>
<goals><goal>single</goal></goals>
</execution>
</executions>
</plugin>
```
描述符决定分发包长什么样:
```xml
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 https://maven.apache.org/xsd/assembly-2.1.0.xsd">
<id>dist</id>
<formats>
<format>zip</format>
<format>tar.gz</format>
</formats>
<includeBaseDirectory>true</includeBaseDirectory>
<baseDirectory>${project.artifactId}-${project.version}</baseDirectory>
<fileSets>
<fileSet>
<directory>src/main/scripts</directory>
<outputDirectory>bin</outputDirectory>
<fileMode>0755</fileMode>
</fileSet>
<fileSet>
<directory>src/main/config</directory>
<outputDirectory>conf</outputDirectory>
<filtered>true</filtered>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>lib</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<scope>runtime</scope>
</dependencySet>
</dependencySets>
</assembly>
```
构建命令是 `mvn clean package`,产物会出现在 `target/demo-1.0.0-dist.zip` 一类文件中。CI 里可以把这个 zip 作为 artifact 上传,或者在 release 阶段发布到制品仓库。
```yaml
- name: Build distribution
run: mvn -B clean package
- name: Upload dist
uses: actions/upload-artifact@v4
with:
name: demo-dist
path: target/*-dist.*
```
分发包最好在构建后做一次“解压即运行”检查。比如 CI 里执行 `unzip target/*-dist.zip -d target/check`,再检查 `bin/start.sh`、`conf/`、`lib/` 是否存在,必要时启动 `java -cp 'lib/*' com.example.Main --version`。这一步看似简单,却能提前发现脚本权限、路径拼接、依赖遗漏等问题。很多团队只验证 Maven 构建成功,真正交付给运维后才发现包结构不符合部署脚本预期。
文件命名也要保持稳定。`appendAssemblyId=true` 会生成带 `dist` 后缀的包,适合同时产出多个 assembly;如果只需要一个主分发包,可以根据仓库规范调整。不要让 CI 用模糊路径上传一堆临时包,否则发布页面里会出现 sources、javadoc、dist 混在一起的情况。正式发布时最好对分发包生成校验和,至少保留 SHA-256,方便部署端确认下载完整性。
如果分发包要给客户或离线环境使用,还要提前处理许可证和第三方依赖清单。Assembly 可以把 `LICENSE`、`NOTICE`、`THIRD-PARTY.txt` 放进包里,但清单内容通常要由 license 插件或内部合规流程生成。别等交付前一天再补这些文件,依赖一多,许可证核对会非常耗时。
## 追问
### Assembly 和 Shade Plugin 应该怎么选?
Assembly 擅长做分发包,能保留目录结构,把脚本、配置、依赖分开放。Shade Plugin 擅长做 uber-jar,并且可以重定位包名,缓解依赖冲突。取舍点是交付形式:要 zip/tar.gz 选 Assembly,要一个可运行胖 JAR 且担心依赖冲突选 Shade。常见坑是用 `jar-with-dependencies` 打包复杂项目,多个依赖的同名资源被覆盖,运行时才发现配置或 SPI 丢失。
### dependencySet 的 scope 应该怎么写?
大多数运行包使用 `runtime`,这样会包含运行必需依赖,不会把 test 依赖打进去。如果是开发调试包,可能需要额外包含源码或文档,但正式分发包应尽量干净。边界在于 provided 依赖是否由目标环境提供,比如 Servlet 容器或应用服务器。坑在于把 provided 也打进 lib,线上类加载顺序变化后出现版本冲突。
### 配置文件要不要 filtered?
可以过滤构建号、版本号这类非敏感值,但不要把数据库密码、生产密钥过滤进包里。分发包通常应该携带模板配置,真正的环境值由部署系统注入。取舍是开箱即用和安全隔离之间的平衡:内部工具可以多给默认值,生产服务应少塞秘密。常见坑是开启 filtering 后,YAML 里的 `${...}` 被 Maven 当占位符处理,Spring 运行时反而拿不到原本想保留的变量。
### fileMode 在 Windows 上还有意义吗?
对 zip 来说意义有限,对 tar.gz 在 Linux 部署场景很重要。启动脚本如果没有 `0755` 权限,解压后第一步就是 chmod,自动化部署会多一个失败点。边界是目标平台全是 Windows 时可以不强调 fileMode,但跨平台包最好同时提供 `.sh` 和 `.bat`。坑在于脚本在 Git 里有执行权限,本地测试没问题,CI 重新打包后权限丢了。
### 多模块项目里 Assembly 应该放在哪个模块?
通常放在专门的 distribution 模块,而不是塞进业务模块。这样它可以依赖多个子模块产物,再统一组装成一个包。取舍是结构稍微多一层,但职责更清楚,避免每个模块都知道最终分发目录。常见坑是在父 POM 里绑定 assembly,结果所有子模块都执行一次,产物重复、路径混乱,还拖慢构建。
## 小结
Assembly Plugin 的关键是先想清楚交付目录,再写描述符。应用 JAR、脚本、配置、依赖各归其位,CI 只负责稳定产出和上传。不要把它当依赖冲突修复工具,也不要把环境秘密打进包里,这两个边界守住,分发包会好维护很多。服务端5月31日 22:22
Maven Release Plugin 如何安全完成版本发布?Maven Release Plugin 用来把“准备版本、打标签、发布制品、推进下一个快照版本”这组动作自动化。它适合仍以 Maven 仓库发布 JAR、WAR 或内部 SDK 的团队,尤其是多模块项目,不适合把所有发布逻辑都塞进一个临时脚本。它的价值在于可追溯:哪个 Git tag 对应哪个 Maven version,哪个构建产物进入了 releases 仓库,都能对上。
## 发布前要先把 POM 配完整
Release Plugin 依赖 SCM 和 distributionManagement。没有 SCM,它不知道标签打到哪里;没有发布仓库,它只能改版本和打 tag,不能把制品 deploy 出去。
```xml
<scm>
<connection>scm:git:https://github.com/acme/demo.git</connection>
<developerConnection>scm:git:git@github.com:acme/demo.git</developerConnection>
<url>https://github.com/acme/demo</url>
<tag>HEAD</tag>
</scm>
<distributionManagement>
<repository>
<id>company-releases</id>
<url>https://repo.example.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>company-snapshots</id>
<url>https://repo.example.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
```
插件配置可以从保守开始:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<tagNameFormat>v@{project.version}</tagNameFormat>
<autoVersionSubmodules>true</autoVersionSubmodules>
<releaseProfiles>release</releaseProfiles>
<goals>clean deploy</goals>
</configuration>
</plugin>
```
仓库认证不要写进 POM,应放到 `~/.m2/settings.xml` 或 CI Secret 注入的 settings 文件里:
```xml
<servers>
<server>
<id>company-releases</id>
<username>${env.MAVEN_REPO_USER}</username>
<password>${env.MAVEN_REPO_PASSWORD}</password>
</server>
</servers>
```
发布命令通常分两步。先 `mvn -B release:prepare`,它会检查工作区、确认没有 SNAPSHOT 依赖、把版本从 `1.2.0-SNAPSHOT` 改成 `1.2.0`、提交、打 tag,再改成下一个快照版本。再执行 `mvn -B release:perform`,它会检出 tag 并执行 deploy。第一次接入一定先跑 `mvn -B release:prepare -DdryRun=true`,确认版本号、tag 名称和提交内容没问题。
CI 示例可以这样写:
```yaml
name: Maven Release
on:
workflow_dispatch:
inputs:
releaseVersion:
required: true
developmentVersion:
required: true
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven
- run: mvn -B release:prepare release:perform -DreleaseVersion=${{ inputs.releaseVersion }} -DdevelopmentVersion=${{ inputs.developmentVersion }}
```
发布流程还要考虑分支模型。主干开发团队通常在 main 上保持可发布状态,releaseVersion 由人工输入或从 tag 推导;Git Flow 团队可能从 release 分支执行 prepare,再把版本提交合回主干。两种方式都能用,但不要让插件一边改版本一边跨分支合并,这会让冲突难以理解。比较稳的做法是发布分支只做版本冻结和缺陷修复,功能变更不要混进来。
多模块项目更要提前决定版本策略。`autoVersionSubmodules=true` 会让所有子模块使用同一个版本,简单、清晰,也适合一起发布的服务组。如果子模块各自独立发布,Release Plugin 的交互式流程会变得很重,甚至不如使用 flatten-maven-plugin、versions-maven-plugin 或专门的发布脚本。边界在于模块之间是否必须同进同出,不能只因为“这是多模块项目”就默认全部一起发。
还有一个现实问题是发布说明。Release Plugin 不会自动替你判断哪些变更值得写进 changelog,它最多保证版本和 tag 可追溯。团队如果需要面向用户的发布说明,最好在 CI 中根据 PR 标题或 conventional commits 生成草稿,再由负责人确认。不要把“构建成功”等同于“发布准备充分”。
## 追问
### release:prepare 和 release:perform 到底差在哪?
`prepare` 改的是源码仓库状态:检查、改版本、提交和打标签。`perform` 面向发布仓库:从标签检出干净代码,然后构建并 deploy。边界很清楚,prepare 成功不代表制品已经发布,perform 失败也不应该随手删 tag。常见坑是把两步放在本地执行,中途网络失败后工作区、tag 和远程仓库状态不一致,恢复成本很高。
### 为什么发布前不能有 SNAPSHOT 依赖?
正式版本需要可重复构建,而 SNAPSHOT 依赖随时可能变化。今天同一个 tag 构建出的包和明天不一致,发布审计就失去意义。取舍是内部快速迭代阶段可以用 SNAPSHOT,但进入 release 分支前必须替换为正式版本。踩坑点是传递依赖里藏着 SNAPSHOT,表面 POM 看不出来,需要 `mvn dependency:tree | grep SNAPSHOT` 检查。
### Maven Release Plugin 适合所有项目发布吗?
不适合。它适合 Maven 制品发布和版本号强绑定的项目,比如 Java SDK、内部 starter、多模块服务。容器镜像发布、Helm Chart 发布或前后端混合发布,通常需要 CI 编排更多步骤,Release Plugin 只能负责 Maven 这部分。边界在于“最终交付物是不是 Maven artifact”,如果不是,就不要强行让它接管整个发布流程。
### 发布失败后应该 rollback 还是手动修?
如果失败发生在 `prepare` 阶段且还没推送远程,`mvn release:rollback` 通常够用。如果 tag 已推送或制品已 deploy,就要按团队发布规范处理,不能简单回滚本地文件。取舍点是保持历史真实还是追求清爽,正式发布场景通常宁可追加修复版本,也不要改写已公开的 tag。常见坑是删除远程 tag 后重新发布同版本,结果仓库里已有不可覆盖的 release artifact。
### 在 CI 中运行 Release Plugin 要注意什么?
CI 必须能推送 tag,也必须能访问 Maven 发布仓库,所以 Git 凭据和 Maven settings 都要提前准备。`actions/checkout` 要设置 `fetch-depth: 0`,否则插件可能拿不到完整 SCM 信息。为了减少误发布,建议只允许手动触发或受保护分支触发,并把 releaseVersion、developmentVersion 显式输入。坑在于使用默认 token 权限不足,prepare 到 push tag 时才失败,前面版本文件已经被改过。
## 小结
Maven Release Plugin 的重点不是命令多熟,而是发布边界清楚:POM 里声明 SCM 和仓库,settings 里放凭据,CI 中固定触发入口。先 dryRun,再 prepare,最后 perform;失败后先判断已经影响到本地、远程 tag 还是制品仓库,再决定回滚方式。服务端5月31日 22:22
Spring Boot 项目如何配置 Maven 才不容易踩坑?Spring Boot 项目里,Maven 的核心作用不是“把依赖写进 pom.xml”这么简单,而是把 Java 版本、依赖版本、打包方式、测试插件和运行命令统一到一套可重复的构建规则里。最省心的做法是使用 `spring-boot-starter-parent`,让 Spring Boot 帮你管理常见依赖和插件版本;如果公司已有统一父 POM,则用 `spring-boot-dependencies` 做 dependencyManagement。两种方式都能用,区别在于谁来当父 POM,这一点在企业项目里经常被忽略。
## Spring Boot 和 Maven 集成的关键配置
典型单体服务可以直接继承 Spring Boot 父 POM:
```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</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>
```
如果不能继承它,比如公司已经有 `company-parent`,就导入 BOM:
```xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```
打包可执行 JAR 时必须关注 `spring-boot-maven-plugin`。它会把普通 JAR 重新打成包含启动器和依赖布局的 Boot JAR,否则 `java -jar` 可能找不到主类或依赖。
```xml
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.Application</mainClass>
</configuration>
<executions>
<execution>
<goals><goal>repackage</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
```
常用命令保持简单即可:`mvn clean package` 打包,`mvn spring-boot:run` 本地启动,`java -jar target/app.jar --spring.profiles.active=prod` 按环境运行。Profile 可以放在 Maven 里控制构建参数,但业务环境更推荐由 Spring 的 `application-dev.yml`、环境变量或启动参数控制,避免“构建一次只能用于一个环境”。
还有一个容易被忽略的点是测试插件。Spring Boot 父 POM 会管理 Surefire 和 Failsafe 的常见版本,但如果企业父 POM 覆盖过这些插件,JUnit 5 测试可能出现本地能跑、CI 不识别测试用例的情况。排查时不要只看依赖,先执行 `mvn -X test` 或检查 `target/surefire-reports` 是否生成。对于多模块项目,建议在父 POM 统一插件版本,在应用模块只保留差异化配置。
资源过滤也要克制使用。`src/main/resources` 全量开启 filtering 后,某些二进制资源、证书文件或包含 `${}` 的配置文件可能被 Maven 意外处理。更稳妥的方式是只对 `application-build.properties` 这类构建信息文件开启过滤,把运行环境变量留给 Spring Boot 自己解析。这样构建产物更通用,也更适合 Docker 镜像和多环境部署。
版本升级时也要先读 Spring Boot 的 release notes。比如从 2.x 升到 3.x,Java 基线、Jakarta 包名、部分自动配置条件都会变化,单靠 Maven 改版本并不能完成迁移。稳妥做法是先在分支里升级父 POM 或 BOM,再跑完整测试和依赖树检查,最后处理运行时配置差异。
## 追问
### starter-parent 和 spring-boot-dependencies 应该选哪个?
`starter-parent` 适合新项目或没有统一父 POM 的项目,因为它连插件默认配置也一起给了。`spring-boot-dependencies` 更适合企业项目,它只导入依赖版本,不抢父 POM 位置。取舍点在于控制权:前者省配置,后者更容易接入公司构建规范。常见坑是导入 BOM 后以为插件版本也被完整管理了,结果编译器插件、surefire 插件仍受公司父 POM 或 Maven 默认值影响。
### 为什么不建议给每个 starter 手动写 version?
Spring Boot 的 BOM 已经验证过一组依赖组合,手动写版本会破坏这套兼容矩阵。偶尔覆盖版本可以解决安全漏洞,但要配合 `mvn dependency:tree` 看传递依赖有没有被带歪。边界是底层库存在紧急 CVE 或公司必须统一版本时才覆盖,而不是为了“用最新版”。踩坑最多的是 Jackson、Tomcat、Hibernate 这类深度集成依赖,单独升级后编译通过,运行时才暴露方法签名不兼容。
### Maven Profile 能不能直接管理 Spring 的 dev、prod 环境?
可以,但不建议把它当唯一方案。Maven Profile 更适合控制构建时差异,比如是否开启资源过滤、是否打包前端资源、是否替换构建号。Spring Profile 更适合控制运行时差异,比如数据库、缓存、日志级别。坑在于把生产配置过滤进 JAR,后面同一个包就没法在不同环境复用,也容易把敏感信息带进构建产物。
### spring-boot-maven-plugin 和 maven-jar-plugin 有什么区别?
`maven-jar-plugin` 生成普通 JAR,主要负责 manifest、包含文件等基础打包。`spring-boot-maven-plugin` 的 `repackage` 会把普通 JAR 改造成可执行 Boot JAR,并处理依赖嵌套和启动类。取舍上,库项目不应该打 Boot JAR,应用项目才需要。常见坑是多模块项目在公共模块也启用 repackage,导致公共模块无法作为普通依赖被其他模块正确引用。
### CI 里构建 Spring Boot 项目有哪些必要检查?
至少要跑 `mvn -B clean verify`,让测试、编译和打包在非交互模式下完成。可以加 `mvn dependency:tree` 或 OWASP Dependency Check 做依赖排查,但安全扫描会增加耗时,适合按分支或定时任务分层执行。CI 中最好固定 JDK 和 Maven 版本,否则本地能过、流水线失败很难排查。一个常见坑是 CI 使用 JDK 21,本地使用 JDK 17,Spring Boot 版本、插件版本和字节码目标不一致时会出现莫名其妙的编译或测试问题。
## 小结
Spring Boot 与 Maven 集成的重点是版本统一、插件正确、环境边界清晰。新项目优先用 `starter-parent`,企业项目用 BOM 接入现有父 POM;应用模块启用 `spring-boot-maven-plugin`,公共模块保持普通 JAR。只要这几条边界守住,后面的依赖升级、打包和 CI 才不会变成反复试错。