作为一个常年跟 Java 项目、CI 流水线、私有仓库打交道的人,我对 Maven 的感情一直很复杂。一方面它稳定、可靠,是 Java 生态的基石之一;另一方面,它偶尔冒出来的诡异报错,也实打实地让人头疼。这次把环境从 3.6.3 和 3.8.x 统一升到 Apache Maven 3.9.6,前前后后折腾了一周,踩了配置的坑,也顺手解决了好几个历史遗留的插件兼容问题。这篇文章不聊虚的,把我升级过程中的实测结果、配置细节、踩坑实录全部摊开讲清楚。
先说结论:Maven 3.9.6 不是一个颠覆性的版本,但它更像是 Apache Maven 团队在 3.9.x 这条线上的“稳定交付物”。如果你还在用 3.6.x 或者更老的版本,又恰好遇到资源过滤、插件版本警告、构建输出混乱这类问题,升级到 3.9.6 往往能让你少掉很多头发。这篇文章适合所有用 Maven 的 Java 开发者,尤其是从老版本升级、切换环境、或者刚接触 Maven 想在 IDEA 里正确配置的人。
1. 3.9.6 到底改了些什么:升级前你得知道的版本逻辑
很多同学看到版本号 3.9.6,下意识会觉得这就是一个“补丁版本”,改点 bug 就完事了。这个理解不算错,但在实际迁移的时候,你会发现它背后有一整套版本策略的变化,搞不清楚这些,升级后配置容易踩坑。
1.1 3.9.x 和 3.8.x 是什么关系
Maven 4 已经规划了很久,但一直没正式铺开,所以 3.9.x 实际上是 Apache Maven 在 3.x 主干上的延续版本。它和 3.8.x 的关系,不是简单的“Bugfix”,而是包含了多项面向未来 Maven 4 的铺垫性改动,尤其是构建日志、依赖解析机制、插件版本默认值、仓库元数据缓存策略这几个方向。
3.9.6 发布在 2024 年初,属于 3.9.x 系列里比较成熟的版本。它的 JDK 最低要求依旧是 JDK 8,但我实测在 JDK 17 和 JDK 21 环境下跑 3.9.6,稳定性明显比 3.6.3 好,尤其是编译插件、surefire 测试插件、resources 插件这些基础组件的默认版本,都被拉到了对 JDK 17+ 更友好的版本上。这一点对现在主流的新项目来说非常关键。
1.2 升级后体感最明显的三个变化
第一,构建日志的格式变“整齐”了。3.9.6 对 Maven 自身的日志输出做了大量清理,很多内部调试信息不再默认打印,同时也屏蔽了一堆老的插件警告。如果你之前被那种“一行构建信息夹着五六个 WARNING”的输出搞到崩溃,升到 3.9.6 会舒服很多。
第二,依赖解析时的元数据缓存策略变了。Maven 3.9.x 对远程仓库的 metadata 缓存、快照版本的更新时间判断做了调整。简单说,就是它不会像老版本那样频繁地去远程仓库核对 SNAPSHOT 版本,所以构建速度会快一些。但你如果习惯了老版本那种“每次构建都尝试拉最新快照”的行为,升级后可能会发现某个快照更新不生效,非得加-U参数强制刷新。这个我后面会专门讲。
第三,对 Maven Wrapper 的支持更顺畅。3.9.6 的mvnw脚本兼容性更好,在 Windows、Linux、macOS 上跑起来基本不会出现奇怪的换行符或者权限问题。你要是维护着多个 Java 项目,强烈建议把项目的 Maven Wrapper 也升级到 3.9.6,这样团队协作时大家的构建版本保持一致,能少掉巨多“我本地能编但 CI 上挂了”的扯皮。
1.3 我的升级建议
如果你现在的项目是纯 Java 8、用的插件版本也比较老、不依赖新的构建特性,那升不升 3.9.6 其实影响不大,因为 Maven 3.x 的 API 兼容性一直做得不错。但如果你的项目是 Java 11 以上、用到了 Spring Boot、多模块聚合、或者你频繁被maven-resources-plugin、maven-compiler-plugin的兼容性问题恶心到,那我建议你直接升。
升级的时候有一点要特别注意:Maven 3.9.6 对某些插件的默认版本进行了提升,但这些默认版本不一定和项目里已经显式声明的插件版本兼容。如果项目 POM 里已经写死了某个很老的插件版本,那升级后首先要关注的就是这些插件能不能正常跑起来。
2. 下载、安装与环境变量配置:从零装好 Maven 3.9.6
这一节写给刚接触 Maven 的同学,也写给那些要在一台新机器上部署 Maven 的老手。因为 3.9.6 的安装过程中有一个非常容易忽略的点:JDK 版本和 settings.xml 的归属关系。
2.1 下载渠道与版本选择
Maven 的下载官网是maven.apache.org,进入 Download 页面后直接选apache-maven-3.9.6-bin.tar.gz或者apache-maven-3.9.6-bin.zip。Windows 用户下载 zip,Linux 和 macOS 用户下载 tar.gz。
这里有个很实用的经验:如果你需要下载历史版本,比如公司强制要求某个老版本,不要只在当前官网翻。Apache 的归档地址是archive.apache.org/dist/maven/maven-3/,里面保留了 3.0 到 3.9.x 的所有版本,连二进制包和源码包都完整。热搜词里提到的“apache 官网下载 maven 3.6.x版本”,就是从这个归档地址里找。很多公司内网环境无法访问外网下载,归档地址反而比官网主页更好用。
2.2 环境变量配置:Windows、Linux、macOS 三平台实操
安装包解压后,建议先把目录名改成maven-3.9.6这样的形式,放到/usr/local或D:/dev/这类规范的软件目录里,方便后续维护。
Windows 下的配置流程是:
- 右键“此电脑”进入“属性”,打开“高级系统设置”,点击“环境变量”。
- 新建系统变量
MAVEN_HOME,值设置为 Maven 解压目录,比如D:\dev\maven-3.9.6。 - 在系统变量
Path中新增%MAVEN_HOME%\bin。 - 打开新的命令行窗口,执行
mvn -version验证。
macOS 和 Linux 下,我习惯把配置写到~/.zshrc或~/.bashrc里:
export MAVEN_HOME=/usr/local/maven-3.9.6 export PATH=$MAVEN_HOME/bin:$PATH执行source ~/.zshrc让配置生效,然后mvn -version。
验证时请重点关注第一行的输出,它会同时显示 Maven 版本和当前使用的 Java 版本。如果你看到的是Error: JAVA_HOME is not set correctly,说明 JDK 的环境变量没弄好。Maven 3.9.6 需要 JDK 8 及以上,我建议开发环境统一用 JDK 17 或 JDK 21,兼容性和性能都更稳妥。
2.3 settings.xml 三件套:本地仓库路径、镜像、Profile
Maven 的配置文件有两个层级:全局配置在$MAVEN_HOME/conf/settings.xml,用户级配置在~/.m2/settings.xml。两个文件都存在时,用户级的配置会覆盖全局配置。
这里分享一个我自己坚持的规范:全局配置只保留最基础的安全设置,比如默认镜像、本地仓库路径、代理信息;用户级配置放属于个人或项目的 profile,比如某个私服账号、仓库认证信息。这样多台机器同步配置时,不容易把敏感信息带去公司之外的机器。
本地仓库路径建议从默认的~/.m2/repository改到独立目录,比如/data/maven-repo或D:/maven-repo。原因很简单:当仓库膨胀到几十 GB 时,系统盘空间会非常吃紧,而且重装系统、切换 SSD 时迁移起来也麻烦。用软链接或者直接在 settings.xml 里指定路径,都能解决这个问题。
<settings> <localRepository>/data/maven-repo</localRepository> </settings>2.4 阿里云镜像配置细节与 mirrorOf 的坑
国内网络环境下,不配镜像基本没法愉快地拉依赖。热搜词里“maven配置阿里云仓库”“maven配置多个镜像仓库”这两个词条,对应的就是下面这段操作。
主流的镜像配置是在settings.xml里新增一个 mirror:
<mirrors> <mirror> <id>aliyun</id> <name>Aliyun Maven Mirror</name> <url>https://maven.aliyun.com/repository/public</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors>这里的mirrorOf是关键。central表示只把 Maven 中央仓库的请求转发到阿里云,私服或者其他自定义仓库不受影响。如果你为了省事写成<mirrorOf>*</mirrorOf>,那么所有远程仓库请求都会被转发到阿里云,这在只依赖中央仓库的场景下没问题,但一旦你接入了公司私有 Nexus 或 Artifactory,依赖就会拉不下来或拉到错误快照。
我见过不少人在这个坑里栽跟头,所以要特别提醒:配置多镜像时,最好给每个镜像写清楚mirrorOf的范围,不要图省事用通配符。
如果你要配置多个仓库,另一个思路是使用 profile 里的 repository,而不是镜像:
<profiles> <profile> <id>my-repos</id> <repositories> <repository> <id>aliyun</id> <url>https://maven.aliyun.com/repository/public</url> </repository> <repository> <id>company-nexus</id> <url>https://nexus.company.com/repository/maven-public/</url> </repository> </repositories> </profile> </profiles>这种方式更适合多私有仓库并行存取的场景,激活 profile 后,Maven 会按顺序依次尝试每个仓库解析依赖,直到找到目标构件。
3. IDEA 里集成 3.9.6:最容易翻车的四个设置
IDEA 对 Maven 的集成已经非常成熟,但正因为 IDE 替我们做了太多事情,导致很多人根本不了解自己项目到底用的哪个 Maven 版本、哪份 settings.xml。这里把最容易翻车的几个点列一下。
3.1 解决“IDEA 默认 Maven 版本”问题
IDEA 新版本往往自带一个 Maven,版本可能不是 3.9.6。这就导致一种很尴尬的情况:命令行里mvn -version显示 3.9.6,IDEA 的 Maven 面板里却是自带的 3.8.x 或 3.6.x,构建行为完全不一致。
正确的做法是在 IDEA 里手动指定 Maven 路径:
- 打开
File -> Settings -> Build, Execution, Deployment -> Build Tools -> Maven。 - 在
Maven home path那里,点右侧的文件夹图标,选择你的apache-maven-3.9.6解压目录。 - 下方的
User settings file和Local repository会自动读取对应路径,确认指向你的settings.xml和仓库目录。
改完后建议点一下 Maven 侧边栏里的刷新按钮(两个循环箭头的图标),让项目重新导入。
3.2 Maven Runner 参数与 JDK 设置
另一个很容易被忽略的地方是Runner选项卡。在Maven设置页的最下面,有一个Runner子页面,里面有JRE和VM Options两个字段。
JRE字段决定了 IDEA 里跑 Maven 命令时用的 Java 版本。如果你的项目要求 JDK 17,但这里选的是 JDK 8,那么编译大概率失败。正确做法是选择项目实际使用的 JDK 版本,并且和Project Structure -> Project SDK保持一致。
VM Options里可以设置一些 Maven 运行时的 JVM 参数,比如:
-Xmx2048m -Dfile.encoding=UTF-8大型多模块项目构建时,默认的 512MB 堆内存经常不够,经常导致 OOM 或者 GC 频繁。保守起见设置 2GB 以上会稳很多。
3.3 多模块项目在 IDEA 中的注意点
多模块项目导入 IDEA 后,父 POM 会显示为一个普通的 Maven 模块,所有子模块会聚合在父模块下面。很多新人会犯的错是:单独对某个子模块执行clean install,结果发现依赖的子模块代码没更新,一直用的是本地仓库里的旧版本。
这是因为子模块之间的依赖,Maven 默认也是从本地仓库解析的,不是自动用 IDEA 的模块依赖。解决办法有两个:
- 在 IDEA 的 Maven 面板里,对父工程执行
clean install,一次性把整个聚合工程的所有模块都构建一遍。 - 开发阶段用
mvn -pl 子模块名 -am install,-am参数会同时构建该子模块依赖的其他本地模块。
IDEA 里执行这些命令前,先确保 Maven 面板中父工程下的所有模块都已经正常加载,否则很可能出现 Module not found 之类的导入错误。
4. 打包、私服与生命周期:绕不开的几个进阶点
这个标题看起来很大,但实际问得最多的就几个点:clean install和package有什么区别、jar/war/pom 三种打包方式怎么选、Spring Boot 项目的 repackage 是什么、依赖冲突怎么排查。
4.1 生命周期到底在做什么
Maven 的构建生命周期是三个:clean、default、site。日常用得最多的是default生命周期,它包含多个阶段,按顺序依次执行:validate、compile、test、package、verify、install、deploy。
mvn clean单独执行时只清理 target 目录。mvn package会执行到 package 阶段,也就是编译、跑测试、打 jar/war 包。mvn install则在 package 基础上,把产物安装到本地仓库。mvn deploy则会把包上传到远程私服。
很多人问:我用了mvn package打出来的包为什么不能直接跑?这通常是因为没有执行clean,导致 target 目录里有上次构建的残留文件。所以实际项目操作中,mvn clean package或者mvn clean install才是更严谨的姿势,热搜词里“maven命令行 clean install”说的就是这个。
4.2 jar/war/pom 三种打包方式与 Spring Boot repackage
在<packaging>标签里,最常见的三种值分别是jar、war、pom。纯工具库和普通应用后端服务用jar,传统 Web 应用需要部署到外部 Tomcat 时用war,多模块聚合的父 POM 用pom,它本身不产出可执行文件,只用来统一管理依赖版本和模块清单。
Spring Boot 项目比较特殊。它的默认打包方式是jar,但打出来的 jar 和普通 Java 库不一样,是 Spring Boot 的可执行 fat jar,里面包含了所有依赖和内嵌 Tomcat。实现靠的是spring-boot-maven-plugin的repackagegoal,它在 package 阶段后把原始 jar 重新加工一遍。
如果你在 IDEA 里对 Spring Boot 项目执行mvn package,然后发现 jar 只有几十 KB,根本跑不起来,多半是spring-boot-maven-plugin没有配置<executions>绑定到repackage。正确配置是:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>4.3 依赖作用域与依赖冲突排查
Maven 的依赖常见作用域有compile、provided、runtime、test、system、import。最容易被忽略的是provided,比如 Servlet API、Lombok 这类只在编译期需要、运行期由容器或编译处理器提供的依赖,用provided能避免打进最终产物里。
依赖冲突是 Maven 项目永恒的痛。冲突的根源是 Maven 的“最近优先、先声明优先”解析原则。当两个不同的依赖树引入同一个框架的不同版本时,Maven 默认选择路径最短的那个,而不是版本最新的那个。这会导致各种奇怪的NoSuchMethodError、ClassNotFoundException。
排查依赖冲突最常用的命令是:
mvn dependency:tree它会打印出完整的依赖树,你可以清楚地看到每个依赖是从哪个传递链引入的。比如看到某个 jar 同时存在 2.5.6 和 3.1.0 两个版本,就可以在 pom 里用exclusion排除冲突项,或者用dependencyManagement统一强制版本。
<dependency> <groupId>com.example</groupId> <artifactId>example-lib</artifactId> <version>1.0.0</version> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency>4.4 部署到私服的配置
项目需要发布到公司私服时,光有deploy命令还不够,得在 pom 里配置distributionManagement:
<distributionManagement> <repository> <id>company-releases</id> <url>https://nexus.company.com/repository/maven-releases/</url> </repository> <snapshotRepository> <id>company-snapshots</id> <url>https://nexus.company.com/repository/maven-snapshots/</url> </snapshotRepository> </distributionManagement>私服的账号密码要配置在settings.xml的<servers>里,ID 必须和 pom 里的<id>一致:
<servers> <server> <id>company-releases</id> <username>deploy-user</username> <password>deploy-password</password> </server> <server> <id>company-snapshots</id> <username>deploy-user</username> <password>deploy-password</password> </server> </servers>之所以放在settings.xml而不是 pom 里,是因为distributionManagement在 pom 里是开源可见的,所有人都能看到仓库地址,但账号密码属于敏感信息,放在用户级配置文件里就可以做到“只在本机生效,不进 Git 仓库”。
5. 升级 3.9.6 之后我踩过的坑:报错排查实录
这一节全是真实经历。升级到 3.9.6 后,我的第一反应是“版本号变了,项目应该无感才对”。事实证明,升级带来的插件兼容性问题,比预想中多,但每个问题都能追溯到一个明确的原因。
5.1 NoClassDefFoundError: org/apache/maven/shared/filtering/MavenFilteringException
这个报错在我升级后最先蹦出来,热搜词里也有人在搜,说明它不算罕见。看报错信息,是在执行mvn clean package时,使用了maven-resources-plugin做资源过滤,结果类加载器找不到MavenFilteringException这个类。
根因不是 Maven 3.9.6 本身,而是项目里的maven-resources-plugin版本太老。老版本的maven-resources-plugin依赖的maven-filtering库版本也老,而 Maven 3.9.6 的类加载机制对这种老库的兼容性不如 3.6.x。
解决办法有两种:一是在 pom 里显式升级maven-resources-plugin到 3.3.1 或更高版本;二是在项目的properties里声明maven-filtering的版本,让插件使用新版依赖:
<properties> <maven-resources-plugin.version>3.3.1</maven-resources-plugin.version> </properties>我最后选择了升级插件版本,因为这样更干净,也避免后续再出现其他 shared 组件兼容性问题。
5.2 mvn validate 失败
validate阶段本身做的事情很少,在大多数项目里它不应该失败。当你在 3.9.6 下遇到mvn validate失败,常见原因有三类:父 POM 解析失败、settings.xml 配置错误、本地仓库元数据损坏。
其中“本地仓库元数据损坏”是最难排查的。Maven 在下载依赖时会把.lastUpdated文件和部分下载失败的临时文件留在本地仓库里。当这些文件损坏后,Maven 解析依赖时会直接判定“该依赖不可用”,反复构建都失败,但原因又不在当前项目里。
遇到这种情况,先检查本地仓库里对应目录下的_remote.repositories和.lastUpdated后缀文件,把可疑的一概删除,然后重新执行mvn -U clean validate。如果问题依旧,再看 settings.xml 里是否误配了<offline>true</offline>,这个选项会强制 Maven 不联网,只解析本地仓库已有内容。
5.3 快照版本不更新
这是一类非常容易让人抓狂的问题。项目里依赖了某个内部模块的-SNAPSHOT版本,在 3.6.3 下构建时,Maven 每次都会去远程仓库检查一下快照是否更新。升级到 3.9.6 后,有时同一个版本的快照在远程仓库变了,本地却一直用旧包,构建结果时好时坏。
原理是 Maven 3.9.x 调整了repository system对快照元数据的缓存时间。默认情况下,Maven 不会在每次构建时都去远程仓库核对快照更新时间,而是使用本地缓存的远程元数据。这个改动是为了提升构建速度,但会让很多开发者误以为“升级后拉包有问题”。
解决办法是在执行构建时加上-U参数,强制刷新快照:
mvn clean install -U如果你希望某个项目每次都检查快照,也可以在 pom 里的repository配置中加<snapshots><updatePolicy>always</updatePolicy></snapshots>。
5.4 日常排查工具与思路
除了上面几个具体报错,我还想分享几个 Maven 日常排查思路,因为很多问题都不是“一条命令”能解决的,需要组合工具逐步定位。
先看mvn -X的调试信息。-X参数会输出 Maven 执行过程中的全量调试日志,包括每个依赖是从哪个仓库下载的、哪些 jar 被跳过、哪些插件的哪个 goal 在哪个阶段执行。虽然信息量大,但报错时它往往能直接告诉你定位方向。
其次用mvn help:effective-pom查看最终的生效 POM。这个命令会把当前项目的 POM、父 POM、profile 合并后的完整版本打印出来。有时候配置看起来没问题,但某个 pluginManagement 或 dependencyManagement 在父 POM 里已经写死了老版本,用这个命令一眼就能发现。
最后是mvn dependency:tree和mvn dependency:analyze的组合。前者解决“依赖从哪来”的问题,后者解决“哪些依赖没用到、哪些用了但没声明”的问题。定期执行这两个命令,能有效减少依赖冲突和隐式依赖带来的潜在风险。
还有一个经验之谈:如果你在 IDEA 里反复执行mvn clean install,但本地仓库的 jar 包内容一直不对,可以关掉 IDEA 的 Maven 自动导入,手动执行一次命令行构建,再用 IDEA 的Reload All Maven Projects重新导入。这一步看着粗暴,却能解决 IDEA 的 Maven 缓存和本地仓库脱节的许多奇怪问题。
我在实际项目里把开发机、CI 服务器、测试服务器全部统一到 Maven 3.9.6 之后,最大的感受是“安静了”。构建日志清爽,依赖解析稳定,插件的默认版本也跟上了 Java 17 的节奏。如果你正在老版本和日常报错之间反复挣扎,与其继续打补丁,不如直接花一个下午把版本升到 3.9.6。最后再分享一个小技巧:升级完别急着删旧版本,先用mvn help:effective-settings对比一下新旧两个版本的配置文件差异,确认没有遗漏的私有仓库和认证信息,再清理旧版。这样切换环境的动作会更平滑,也不容易在第二天上班时被同事一个“仓库拉不下来”的问题打断。