ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Maven配置全攻略:settings.xml、镜像仓库与依赖冲突排查

Maven配置全攻略:settings.xml、镜像仓库与依赖冲突排查 1. 先把底子打好settings.xml与pom.xml的职能边界1.1 三个配置文件到底谁说了算很多人用Maven用了两三年一遇到改了配置不生效还是靠重启IDE硬扛。先说结论Maven的配置分三层全局配置文件、用户配置文件、项目配置文件三者不是简单的覆盖关系而是各有各的管辖范围。全局配置${MAVEN_HOME}/conf/settings.xml管的是这台机器上所有用户、所有项目的行为用户配置~/.m2/settings.xml管的是当前这个操作系统用户的所有项目行为项目配置项目根目录下的pom.xml只管当前这个项目从配置优先级来说pom.xml 用户settings.xml 全局settings.xml。但这里有个关键陷阱镜像mirror、代理、本地仓库路径只能定义在settings.xml里pom.xml里的repositories根本管不到镜像那一层。反过来依赖版本、插件版本、构建参数这些项目级的东西写在settings.xml里虽然语法合法但几乎没人这么干可维护性太差。我见过最典型的翻车场景是这样的项目里明明配了阿里云私服地址中央仓库的依赖也能拉下来但发布内网私服时怎么都不走内网最后发现是settings.xml里一个mirrorOf*/mirrorOf把私服地址给劫持了。这个后面讲镜像的时候详细展开。1.2 环境变量与settings.xml的配合逻辑环境变量负责告诉操作系统Maven装在哪儿settings.xml负责告诉Maven该怎么干活。两者各管一段但配合起来有几个容易忽略的点。先看环境变量本身。Windows上常见的是配MAVEN_HOME然后在PATH里加%MAVEN_HOME%\bin。macOS和Linux则是在~/.bash_profile或~/.zshrc里配export MAVEN_HOME/usr/local/apache-maven-3.8.8 export PATH$MAVEN_HOME/bin:$PATH这里有个老生常谈但值得强调的细节JAVA_HOME必须先于Maven配置好因为Maven本质上是跑在JVM上的Java程序启动脚本会优先找JAVA_HOME。如果你机器上同时装了JDK 8和JDK 17JAVA_HOME指错了版本Maven启动时显示的Java版本就会不对编译出的class文件版本也会跟着错这在多项目切换时特别坑。还有一个环境变量导致的隐蔽问题PATH里同时存在多个Maven版本。很多时候不是版本不对而是命令解析到了另一个目录下的mvn。排查方式很简单# Linux/macOS which -a mvn # Windows where mvn如果输出里出现两个路径检查PATH顺序把想用的那个放到前面。这个问题在装了多个开发工具、被各种安装教程往PATH里追加过多次的环境里特别常见。1.3 我见过最多的配置翻车现场翻车现场一本地仓库路径带了空格或中文。Windows下面很多人喜欢把本地仓库放到D:\我的仓库\repo这种路径Maven虽然在大多数情况下能跑但某些插件处理路径时会出现诡异的编码问题尤其是做Web项目打包、依赖注入插件时。本地仓库路径建议只用英文小写和连字符。翻车现场二settings.xml写成了UTF-8带BOM格式。Maven解析settings.xml对BOM很敏感有时候会出现第一个标签被解析成乱码的情况报错信息还不直观。遇到这种问题用编辑器另存为UTF-8无BOM格式或者干脆用IDEA自带的Maven配置文件编辑器去改它保存的时候会自动处理编码。翻车现场三IDEA里显示的Maven和你命令行手动跑的Maven不是同一个。IDEA配置里有Maven home path、User settings file、Local repository三项默认会使用IDEA自带的Maven版本可能和你在命令行里用的系统Maven不一样。防火墙内网环境最容易翻车命令行里能拉私服IDEA里构建却直接超时因为IDEA用的自带Maven读的settings.xml可能是它内置的那份根本没走你配置好的私服地址。这里给一个我自己项目的settings.xml骨架包含私有仓库地址、镜像、本地仓库位置照着自己改即可?xml version1.0 encodingUTF-8? settings xmlnshttp://maven.apache.org/SETTINGS/1.2.0 !-- 本地仓库路径默认是 ~/.m2/repository -- localRepositoryD:/dev/maven-repo//localRepository !-- 内部私服账号密码配serverId与pom里的repository id对应 -- servers server idmy-nexus/id usernamedevuser/username passworddevpass/password /server /servers profiles !-- 在企业内网环境让所有项目默认走私服这里只是声明地址 -- profile idinternal-repo/id repositories repository idmy-nexus/id urlhttp://nexus.internal.example.com/repository/maven-public//url /repository /repositories /profile /profiles !-- 如果只是简单用阿里云公共仓库镜像是最省事的方式 -- mirrors mirror idaliyun-public/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror /mirrors /settings提示localRepository不要写在注释里就当配过了这个节点必须真实存在且目录要先建好或者让Maven自动建路径末尾建议加斜杠。2. 镜像仓库那些事阿里云、多镜像与仓库合并实战2.1 mirrorOf匹配规则一个字符都不能错镜像配置看起来简单就一个id、一个mirrorOf、一个url但mirrorOf的匹配规则值得认真说因为很多人死在这上面。mirrorOf的值常见的有这几种mirrorOf取值含义适用场景*匹配所有仓库拦截一切外部请求内网完全隔离环境所有请求走代理仓库central只拦截中央仓库只想加速中央仓库其他私服不受影响external:*匹配所有非localhost的仓库需要拦截外网但不干扰本机私服repo1,repo2匹配合法的仓库id逗号分隔精确控制哪些仓库走镜像*,!internal匹配所有仓库但排除internal大多数企业场景的推荐写法这里有个容易被忽略的点mirrorOf不是负载均衡多个mirror匹配同一个仓库时排在前面的生效后面的直接忽略不会做失败切换。我之前维护过一个老项目配置里同时写了阿里云镜像和公司私服镜像阿里云在前面结果私服上才有的内部构件在公司外网环境死活拉不下来——因为所有去私服的请求都被第一个镜像拦截住了。配多个镜像的正确姿势是让mirrorOf的匹配范围不重合。比如一个镜像专门匹配内部构件仓库用internal-repo另一个镜像拦截中央仓库用central这样两条通道互不干扰。阿里云公共仓库的地址用起来最稳定的是这一个mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror注意历史上很多教程里写的是http://maven.aliyun.com/nexus/content/groups/public这个地址已经废弃了新项目直接用/repository/public结尾的地址。另外如果机器在内网、走代理才能访问外网镜像地址前也要加上代理配置否则光改mirrorOf没有任何意义。2.2 多镜像仓库的优先级与回退机制很多人在多镜像上理解有偏差以为多配几个镜像能互相兜底、第一个挂了自动切第二个。实际上Maven的镜像机制不存在自动降级。它只是把对一个仓库的请求重新定位到镜像地址一次请求只匹配一个镜像。真正需要多个仓库地址时正确做法是在settings.xml的profile里配置多个repository或者在pom.xml里配置多个repositories。Maven解析依赖时会按声明顺序去找第一个仓库找到就直接用找不到再找第二个。这里的回退机制才是真正意义上的A仓库没有就去B仓库拿。但要注意节奏问题如果第一个仓库连接超时Maven不会立刻跳到第二个而是会一直等到超时时间耗尽这个时间默认很长。所以多仓库配置里第一个仓库一定要放你认为命中率最高、响应最快的那个别把冷备仓库放前面。一个实用的多仓库写法配合内部私服和中央仓库回退profiles profile idmulti-repo/id repositories repository idnexus/id urlhttp://nexus.internal.example.com/repository/maven-public//url releasesenabledtrue/enabled/releases snapshotsenabledfalse/enabled/snapshots /repository repository idcentral/id urlhttps://repo.maven.apache.org/maven2/url /repository /repositories /profile /profiles注意snapshots标签的选择私服上如果开启了snapshot代理一般建议释放snapshot开关否则每次构建都要去私服检查快照版本慢且不稳定。稳定的release依赖可以放心缓存到本地。2.3 两个本地repository合并的完整步骤这个话题是真实需求很多人从旧电脑拷贝了repository目录手头有两个甚至三个本地仓库新工程配置了新的本地仓库路径但老仓库里有一堆有特殊用途的构件怎么合并网上有各种说法有说直接把整个目录覆盖合并的有说必须重新下载的——都不准确。先说结论本地仓库就是一个按坐标分层的目录结构绝大部分情况可以手动合并但有一个关键文件要注意——_remote.repositories。这个文件记录了每个构件是从哪个远程仓库拉下来的。直接拷贝目录时如果两个仓库里同名的jar版本不同拷贝后者会覆盖前者但_remote.repositories里的记录可能还指向旧仓库导致Maven认为该构件来源不对构建时重新去远程仓库校验甚至报错。我实测可行的合并步骤# 1. 先备份目标仓库 cp -r ~/.m2/repository ~/.m2/repository_backup_$(date %Y%m%d) # 2. 把源仓库的所有内容同步过去重复的构件分别保留版本目录 rsync -av --ignore-existing /old/repo/ ~/.m2/repository/--ignore-existing参数很关键它保证目标仓库已有的文件不被覆盖。两个仓库里同样的groupId和artifactId、不同version会各自保留在各自的版本目录下互不干扰。真正需要做决策的是同一版本但内容不同的情况——这种情况很罕见说明你手动改过jar或者装过非标准构件建议别合并手动把多的那版单独拿出按需使用。合并完有个通用做法清理掉所有_remote.repositories文件让Maven把已存在的jar当作本地已有的构件不再验证来源。find ~/.m2/repository -name _remote.repositories -delete find ~/.m2/repository -name *.lastUpdated -delete做完这个清理本地仓库就只剩纯粹的jar、pom、源码包远程仓库校验信息全部重置。然后跑一次项目构建加-U参数让Maven重新解析需要更新的构件其余的直接用本地缓存速度飞快。注意合并前确认两个仓库的Maven版本差距不大老仓库如果是Maven2时代的产物目录结构是groupId/artifactId/version里带/的旧格式可能不兼容Maven3的解析这种情况建议直接放弃合并重新拉取。3. 依赖管理的底层逻辑从报错到精准排错3.1 依赖仲裁最短路径与声明顺序的博弈依赖冲突是所有Maven使用者的噩梦那句我什么也没改昨天还能编今天突然报NoSuchMethodError基本都跟依赖版本仲裁有关。Maven仲裁依赖版本只有两条规则第一依赖路径最短优先第二路径长度相同先声明的优先。没有别的了不存在版本号大的优先不存在最常用版本优先。举个例子你的项目直接依赖了A和BA又依赖了C的1.0版本B又依赖了DD又依赖了C的2.0版本。你的项目到C-1.0的路径长度是2你-A-C到C-2.0的路径长度是3你-B-D-C这时候就算C-2.0是更新的版本Maven也毫不犹豫选C-1.0。这个机制带来一个问题直接依赖总是赢。如果想强制用某个版本直接在当前pom.xml里声明这个依赖就行因为路径长度最短就是1。这也是为什么很多团队要求基础依赖必须在根pom里显式声明——用最短路径锁死版本。排查依赖冲突最有力的工具还是查看依赖树mvn dependency:tree -Dverbose-Dverbose参数会把被省略的传递依赖也显示出来能清楚看到每个节点版本是怎么仲裁出来的。实际使用中这个命令的输出可能非常长建议配合过滤mvn dependency:tree -Dincludesorg.apache.commons:commons-lang3这样只显示跟commons-lang3相关的路径一眼就能看出谁的版本把谁压制了。3.2 可排除的传递依赖exclusion的正确打开方式依赖冲突里最经典的场景是你的项目用Fastjson 1.2.x另一个库内部传了一个捆绑了旧版Fastjson的依赖。Maven仲裁后选了不可控的版本那就要用exclusion主动剔除传递依赖。dependency groupIdcom.example/groupId artifactIdsome-sdk/artifactId version2.5.0/version exclusions exclusion groupIdcom.alibaba/groupId artifactIdfastjson/artifactId /exclusion /exclusions /dependency剔除之后你的项目里fastjson的版本就只由你自己的声明决定。这种做法是可控的但有一个坑exclusion写错groupId或artifactId时Maven不会报错只是静默忽略。所以每次改exclusion后建议立刻看一眼依赖树验证效果。另外很多冲突并不需要exclusion。当一个库直接把一个依赖标成optional说明它不想让这个依赖传递下去。这种依赖在你项目里不会被自动引入需要你自己按需声明。所以你看到别人的pom里有个库没给依赖居然能用很可能是那个库的optional依赖恰好你也配了这种情况不算异常。3.3 依赖报错的四个常见原因与排查链路依赖报错分很多种我总结下来八成集中在四类错误现象常见原因排查方向Could not find artifact仓库里没有这个版本或者坐标写错先上仓库网页版查坐标和版本搜不到说明不存在或者被删了Failed to resolve dependencies源仓库不可达或者镜像配置路径配错检查镜像地址、网络、代理Missing POM/Missing parent POM依赖的父pom没有下载下来在本地仓库对应目录下看有没有pom文件奇怪的Checksum validation failed本地缓存了损坏的jar常见于上次下载被中断删掉本地对应的*.lastUpdated重新加-U更新有一个排查链路是我每次必用的基本能覆盖90%的问题第一步看报错信息里提到的jar包在本地仓库的路径下有没有。路径对应关系是groupId里的.变成目录分隔符artifactId/version再拼进去。比如com.alibaba:fastjson:1.2.83对应路径是~/.m2/repository/com/alibaba/fastjson/1.2.83/。第二步如果目录存在但缺jar或者有个xxx.jar.lastUpdated文件说明上次下载没成功。删除这个lastUpdated文件重新执行mvn clean install -U-U会强制检查远程仓库把上次没拉完整的构件重新拉回来。这里注意一个细节-U只对snapshot和lastUpdated状态的构件生效对release构件不一定强制更新。第三步如果-U之后还是失败那就是远程仓库本身没有这个构件或者镜像地址不对。这时候我习惯用一个命令直接测试某个构件能不能拉到本地mvn dependency:get -Dartifactcom.alibaba:fastjson:1.2.83这个命令会绕过项目pom直接去远程仓库下载指定构件。成功说明网络和镜像没问题问题出在pom坐标或传递依赖上失败说明问题出在网络、镜像或账号权限上。整个排查流程走下来大多数依赖问题都能定位到具体环节。我处理过的项目里最后发现是settings.xml里把私服的release仓库配成了snapshot仓库地址的也有这种属于配置层面的低级错误但报错信息会伪装成依赖找不到光看字面容易被带偏。4. 命令行构建实战clean install 之外的高频操作4.1 生命周期与命令行参数的对应关系Maven的命令行核心其实是三个生命周期和它们的阶段。很多人只会mvn clean install其实搞清楚生命周期阶段的对应关系很多东西可以更精准地控制。clean生命周期独立于构建生命周期专门负责清理default构建生命周期才是真正干活的阶段顺序从validate开始到compile、test、package、verify、install、deploy结束。你执行mvn package时它不会只打包而是会先把compile、test都执行一遍因为阶段之间有先后依赖。这是很多人对我明明只想打包为什么还要跑测试的答案。常用参数组合# 只编译不跑测试 mvn compile # 打包并跳过所有测试编译测试代码也跳过 mvn clean package -DskipTests # 打包但不跳过测试代码编译测试仍然编译只是不执行 mvn clean package -Dmaven.test.skiptrue # 安装到本地仓库 mvn clean install # 只编译被改动的模块及其依赖模块 mvn clean install -pl module-b -am这里要单独拆解一下-DskipTests和-Dmaven.test.skiptrue的区别这是两个经常被搞混的参数。-DskipTests只是不执行测试但测试代码仍然会编译-Dmaven.test.skiptrue是直接跳过测试代码的编译和运行编译时间能省一大截。如果只是想快速验证构建链路用-Dmaven.test.skiptrue如果不想跑测试但希望测试代码语法正确用-DskipTests。多模块项目的-pl和-am也是高频操作。-pl module-b代表只构建module-b-am代表同时构建module-b依赖的其他模块避免它依赖的模块还没装进本地仓库导致构建失败。还有两个参数经常被忽略但很实用# 构建之前先离线检查所有构件都从本地仓库取不访问网络 mvn clean install -o # 临时指定settings.xml位置 mvn clean install -s /path/to/your-settings.xml-o离线模式在出差、断网环境下特别有用。它会强制不联网所有依赖用本地缓存。坏处是一旦某个构件本地没有构建立刻失败但至少不会卡在超时上等半天。4.2 多模块项目构建与-D参数传参技巧多模块项目aggregator模式里的一个常见场景是父pom和子模块都用了同一个属性变量比如版本号但你不想改pom想临时从命令行覆盖这个值。这个需求靠-D参数就能实现。在pom里先定义属性properties revision1.0.0-SNAPSHOT/revision /properties然后所有用到版本号的地方引用${revision}。构建时如果想临时换个版本mvn clean install -Drevision2.0.0-SNAPSHOT这个技巧在做多分支并行开发、要发布不同版本号的CI流水线里是真正的神器。不用改pom不用改配置构建参数里带一下就行。注意如果子模块pom里用了${revision}但父pom没有定义或者父pom没有把revision暴露给子模块子模块会解析失败。多模块构建时传参数还有一个细节-D参数默认只传给当前执行的Maven进程子模块的fork生命周期里不一定继承尤其是使用了一些会fork新进程的插件比如部分代码生成插件。这种情况下参数会丢失需要在插件配置里显式声明属性透传否则构建结果会违背预期。4.3 离线构建与本地仓库补包离线构建做完后如果本地仓库缺构件最常遇到的就是ArtifactResolutionException。这时候有几个处理思路第一种缺构建工具插件本身。这种情况需要找一个有网的机器或者有代理的环境先跑一次mvn dependency:go-offline。这个命令会把当前项目所有可能用到的插件和依赖全部拉进本地仓库之后就能离线构建了。需要注意的是go-offline并不会拉全所有插件某些测试插件、报告插件还是可能会遗漏。第二种手工放置jar。某些内部构件不在任何公共仓库里需要在离线环境手工放进本地仓库。可以手动建目录也可以用一个外部jarmvn install:install-file \ -Dfile/path/to/your-custom.jar \ -DgroupIdcom.example \ -DartifactIdcustom-lib \ -Dversion1.0.0 \ -Dpackagingjar这个命令会把指定jar安装进本地仓库顺便生成对应的pom文件。以后项目里直接按坐标引用就能找到。这也是处理公司内部自研SDK发布到本地的标准姿势。第三种处理lastUpdated残留文件。本地仓库中如果某个构件下载失败会留一个xxx.lastUpdated文件Maven再次构建时看到这个文件会认为刚才下载失败了不尝试重新下载就跳过导致死循环式的构建失败。修复方式就是删掉这些标记文件find ~/.m2/repository -name *.lastUpdated -delete然后加-U重新构建。这个操作本身很安全Maven会自动重新拉取需要的构件不需要把整个本地仓库删掉重来。5. IDE里的MavenIDEA、Eclipse、VSCode配置差异与常见问题5.1 IDEA中的Maven配置与内置Maven陷阱IDEA默认会使用它自己内置的Maven好处是开箱即用坏处是版本相对固定、且不读你系统里那份settings.xml配置这在认真配置过系统Maven的人手里就成了隐形坑。IDEA里正确的配置路径Settings - Build, Execution, Deployment - Build Tools - Maven。需要盯住三个字段Maven home path尽量改成你系统安装的Maven目录不要用Bundled (Maven 3)这种内置选项User settings file这是IDEA读取配置的关键字段勾选Override之后手动指定~/.m2/settings.xml路径Local repository勾选Override后会自动读取settings.xml里的配置一般不用手填配好之后点右下角的Reload All Maven Projects刷新。如果改过settings.xml但IDEA不生效把这个reload操作再跑一次IDEA会重新解析所有工程的依赖。IDEA里还有个高频问题依赖已经改了但IDE里红叉不消失。先确认是不是本地仓库依赖没有刷新成功在IDEA的Maven工具窗口里选Reload All Projects还不行就用命令行跑一次mvn clean install再回IDE里刷新。IDEA内部不会自动感知外部命令行操作对本地的构件改动必须手动刷新一次。5.2 Eclipse与VSCode的配置路径Eclipse的Maven插件m2e配置路径是Window - Preferences - Maven - Installations点击Add...选择系统Maven安装目录重点勾选User Settings选项卡下的Settings path同样建议显式指向~/.m2/settings.xml不要让Eclipse用默认值。Eclipse里新建Maven工程时会生成一个src/main/java和src/test/java的目录结构但实际项目的包路径、编码、Java版本都要在pom.xml里显式配置。Eclipse对pom.xml的改动反应比IDEA慢改完记得右键项目Maven - Update Project快捷键AltF5这一点很多人漏掉导致改完pom后Eclipse里的编译等级还是旧的。VSCode里配置Maven相对简单装好maven for java扩展后在settings.json里配置{ maven.executable.path: /usr/local/apache-maven-3.8.8/bin/mvn, maven.settingsFile: ~/.m2/settings.xml, java.configuration.maven.userSettings: ~/.m2/settings.xml }VSCode的Maven插件在Java语言服务里走的路径和命令行不同某些场景下会复用java.configuration.maven.userSettings而不是maven.settingsFile所以两个字段建议都配同一个文件避免出现命令行走了私服、VSCode却连中央仓库的诡异问题。IDEA系的其他编辑器比如Trae以及一些国产IDE配置思路基本同IDEA一致找设置里的Maven菜单照着上面三个字段配就行。编辑器的UI可能不同但底层读的都是同一套settings.xml和本地仓库只要这两项指对了IDE之间的依赖解析结果应该完全一致。5.3 仓库位置在IDE里的映射关系最后这节说一个所有人都遇到过但少有人深究的问题IDEA右侧的External Libraries里显示的依赖到底是哪里来的答案很简单它就是你本地仓库里实际下载的那些jar包和pom文件IDEA不会把依赖藏到别处。IDEA的依赖解析过程是这样的项目pom里的依赖声明 - 结合settings.xml里的镜像和仓库配置 - 拉取构件到本地仓库 - IDE从这个本地仓库路径里读jar包展示在External Libraries里。所以一个依赖在IDE里显示有版本A但项目构建时用的却是版本B多半是因为IDE的本地仓库路径和命令行构建的本地仓库路径不一致或者IDE在reload时把某个传递依赖的仲裁结果解析得和命令行不一样。本地仓库的目录结构本身就说明了构件信息不需要任何IDE辅助~/.m2/repository/ └── com/ └── alibaba/ └── fastjson/ ├── 1.2.83/ │ ├── fastjson-1.2.83.jar │ ├── fastjson-1.2.83.pom │ └── _remote.repositories └── maven-metadata-aliyunmaven.xml看到这个结构你就知道为啥之前建议合并仓库时强调保留version目录了——本地仓库的本质就是一个大的文件缓存按groupId/路径加artifactId加version三级定位理解了这个映射关系所有依赖找不到的问题都能从文件系统层面找到解释。IDE里如果出现依赖已下载但项目里还是红的情况我第一反应都是去本地仓库对应的version目录看一眼jar包是否真实存在、大小是否为0KB、有没有lastUpdated文件。这一步比任何IDE刷新功能都管用因为IDE只是读文件文件有问题刷新一万次也是同样的结果。我个人现在的习惯是这样所有Maven相关的问题先看本地仓库文件再查settings.xml最后才动IDE。这个顺序从根上解决问题也不容易改乱环境。尤其是在多团队协作、不同开发工具混用的情况下文件系统和配置文件是大家唯一的共识层IDE的刷新机制只是表象。
返回列表