Maven 用了这么多年,真正让我停下来认真读一遍 settings.xml 文档的契机,是去年公司换了一次 Nexus 私服地址。运维发了个新 settings.xml 让全员替换,结果我一拉代码——所有依赖全部 404。排查了半小时发现是新配置里 mirrorOf 写了 *,把项目 pom 里声明的第三方仓库也一并劫持到了新私服上,而新私服还没同步完。
这件事之后我意识到,settings.xml 这个文件大部分人只是"抄来能用就行",但对它的加载顺序、mirror 匹配规则、profile 激活机制其实一知半解。出了问题只能靠删文件重装来修。
这篇把 settings.xml 每个配置项讲清楚,重点放在镜像匹配规则、密码加密的完整操作流程、多环境 profile 切换这三个最容易出错的地方。所有示例基于 Maven 3.9.x,低版本(3.6+)行为一致。
文件位置与加载优先级
settings.xml 有两份,Maven 会合并加载:
| 级别 | 路径 | 作用域 |
|---|---|---|
| Global | ${maven.home}/conf/settings.xml | 当前机器所有用户 |
| User | ${user.home}/.m2/settings.xml | 当前用户 |
Windows 上 User 级别就是 C:\Users\<你>\.m2\settings.xml。
加载规则:User 级别优先。如果两边都定义了同一个 <server> 的 <id>,User 级别覆盖 Global。如果两边都定义了 <mirror>,则两份都会生效(不是覆盖关系),匹配时按声明顺序。
建议:日常开发只维护 User 级别的 ~/.m2/settings.xml,不要动 Maven 安装目录下的 conf。原因:升级 Maven 版本时 conf 会被覆盖,你的配置就丢了。
用命令确认 Maven 实际读的是哪个文件:
mvn --version # Maven home: D:\apache-maven-3.9.6 ← conf/settings.xml 在这下面 mvn help:effective-settings # 输出最终合并后的生效配置(最准)基本结构
一份 settings.xml 的骨架长这样:
<?xml version="1.0" encoding="UTF-8"?> <settings xmlns="http://maven.apache.org/SETTINGS/1.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd"> <localRepository/> <interactiveMode/> <offline/> <pluginGroups/> <servers/> <mirrors/> <proxies/> <profiles/> <activeProfiles/> </settings>下面逐个讲。
localRepository:本地仓库路径
<localRepository>D:/maven-repo</localRepository>默认是 ~/.m2/repository。建议改到非系统盘,原因:
- 一个中等 Java 项目跑几次 mvn clean install,仓库就能到 5~10 GB
- 多项目累积下来 30~50 GB 很正常
- C 盘满了会导致各种诡异问题(IDEA 索引失败、Git 操作报错等)
路径注意:Windows 上用正斜杠 / 或双反斜杠 \\,单反斜杠 \ 在 XML 里虽然大部分时候也能识别,但不保证所有 Maven 插件都兼容。
改完之后验证:
mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout # 输出:D:/maven-repomirrors:镜像配置(重点)
基本语法
<mirrors> <mirror> <id>aliyun-public</id> <name>Aliyun Maven Mirror</name> <url>https://maven.aliyun.com/repository/public</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors>四个字段:
- id:唯一标识,也是认证时关联 <server> 的 key(如果镜像需要登录)
- name:描述,随便写
- url:镜像地址
- mirrorOf:匹配规则——决定哪些仓库的请求会被这个镜像拦截
mirrorOf 匹配规则(核心)
这是 settings.xml 里最容易出错的地方,我见过至少五种写法,行为完全不同:
| mirrorOf 值 | 含义 | 使用场景 |
|---|---|---|
| central | 只代理 Maven Central | 最安全,推荐 |
| * | 代理所有仓库(包括 pom 里声明的第三方仓库) | 危险,容易劫持私服 |
| external:* | 代理所有非 localhost、非 file:// 的仓库 | 比 * 稍安全 |
| repo1,repo2 | 代理 id 为 repo1 或 repo2 的仓库 | 精确控制 |
| *,!company-nexus | 代理所有仓库,但排除id 为 company-nexus 的 | 有私服时的推荐写法 |
血泪教训:如果你有公司私服,mirrorOf 千万不要用 *。否则 pom.xml 里声明的私服仓库请求会被镜像拦截,镜像上没有你公司的内部 jar,直接 404。
正确做法:
<mirror> <id>aliyun-public</id> <name>Aliyun Maven Mirror</name> <url>https://maven.aliyun.com/repository/public</url> <mirrorOf>central,!company-nexus</mirrorOf> </mirror>或者更严格一点,只代理 central:
<mirrorOf>central</mirrorOf>多镜像配置
可以配多个 mirror,Maven 按声明顺序匹配,第一个命中的生效:
<mirrors> <!-- 优先走公司内网镜像(速度快、有内部包) --> <mirror> <id>company-mirror</id> <url>https://nexus.company.com/repository/maven-public/</url> <mirrorOf>central</mirrorOf> </mirror> <!-- 公司镜像挂了或在外网时的备选 --> <mirror> <id>aliyun-public</id> <url>https://maven.aliyun.com/repository/public</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors>注意:同一个 mirrorOf 目标如果声明了多个 mirror,Maven 3.9+ 的行为是只用第一个匹配的,不会 fallback 到第二个(这跟 Docker 的 registry-mirrors 不一样)。想要 failover 效果,需要用 Maven 3.9 的 <blocked> 配合或者在 CI 里用脚本切换。
常用国内镜像地址(2026 年 9 月仍可用)
<!-- 阿里云(推荐,稳定) --> <url>https://maven.aliyun.com/repository/public</url> <!-- 华为云 --> <url>https://repo.huaweicloud.com/repository/maven/</url> <!-- 腾讯云 --> <url>https://mirrors.cloud.tencent.com/nexus/repository/maven-public/</url>阿里云还有个 Spring 专用仓库,拉 Spring 的 snapshot 版时需要单独配:
<mirror> <id>aliyun-spring</id> <url>https://maven.aliyun.com/repository/spring</url> <mirrorOf>spring-milestones,spring-snapshots</mirrorOf> </mirror>servers:认证配置(含密码加密)
基本用法
当你要往私服 deploy jar,或者访问需要登录的仓库时,需要配认证:
<servers> <server> <id>company-nexus</id> <username>zhangsan</username> <password>MyP@ssw0rd</password> </server> </servers>id 必须跟 pom.xml 里 <distributionManagement> 的仓库 id,或者 settings.xml 里 <mirror> / <repository> 的 id完全一致(区分大小写)。不一致的话认证不生效,报 401 Unauthorized。
明文密码的问题
上面那种写法,密码是明文存在 ~/.m2/settings.xml 里的。问题:
- 你 dotfiles 如果同步到 GitHub,密码就泄露了
- 团队共用一台构建机时,所有人都能看到你的密码
- 安全审计过不了
密码加密完整流程
Maven 提供了两层加密:master password加密server password。
第一步:创建 master password
mvn --encrypt-master-password 'YourMasterP@ss123'输出类似:{Qe+3kF5bJ7mN2pR8sT1uV4wX6yZ0aB3cD5eF7gH9iJ0=}
把这个输出写到 ~/.m2/settings-security.xml(没有就新建):
<settingsSecurity> <master>{Qe+3kF5bJ7mN2pR8sT1uV4wX6yZ0aB3cD5eF7gH9iJ0=}</master> </settingsSecurity>第二步:用 master password 加密 server password
mvn --encrypt-password 'MyP@ssw0rd'输出类似:{aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT0uV1wX2yZ3aB4c=}
第三步:把加密后的密码写到 settings.xml
<server> <id>company-nexus</id> <username>zhangsan</username> <password>{aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT0uV1wX2yZ3aB4c=}</password> </server>验证:
mvn deploy -DaltDeploymentRepository=company-nexus::default::https://nexus.company.com/repository/maven-releases/不报 401 就说明认证通过了。
注意事项:
- settings-security.xml 的权限建议设为只有当前用户可读(Linux/Mac:chmod 600 ~/.m2/settings-security.xml)
- master password 忘了的话,删掉 settings-security.xml 重来就行,它不影响已经加密的密码(只要你不改 master)
- 如果改了 master password,之前所有 --encrypt-password 生成的密文都要重新生成
- CI 环境(Jenkins/GitLab CI)通常用环境变量注入而不是 settings-security.xml,后面 profile 那节会讲
proxies:代理配置
公司内网需要走 HTTP 代理才能出外网时配这个:
<proxies> <proxy> <id>company-proxy</id> <active>true</active> <protocol>http</protocol> <host>proxy.company.com</host> <port>8080</port> <username>zhangsan</username> <password>{加密后的密码}</password> <nonProxyHosts>*.company.com|localhost|127.0.0.1|192.168.*</nonProxyHosts> </proxy> </proxies>nonProxyHosts 用 | 分隔,支持 * 通配。公司内网的私服地址一定要加进去,否则访问私服也走代理,绕一圈不说还可能鉴权失败。
大部分开发者不需要配这个——只有你的机器不能直连外网、必须通过代理服务器出网时才用。如果配了代理但拉包还是超时,先确认 nonProxyHosts 是不是漏了私服域名。
profiles + activeProfiles:多环境切换
为什么需要 profile
典型场景:
- 公司内网用 Nexus 私服,在家用阿里云公共镜像
- 开发环境用 snapshot 仓库(实时更新),发布环境只走 release 仓库
- 不同项目用不同的 JDK 版本或编译参数
- CI 和本地用不同的仓库配置
基本结构
<profiles> <profile> <id>company</id> <activation> <!-- 激活条件 --> </activation> <repositories> <!-- 依赖仓库 --> </repositories> <pluginRepositories> <!-- 插件仓库 --> </pluginRepositories> <properties> <!-- 自定义属性 --> </properties> </profile> </profiles> <activeProfiles> <activeProfile>company</activeProfile> </activeProfiles>实战示例:公司内网 + 外网两套配置
<profiles> <!-- 公司内网环境 --> <profile> <id>office</id> <repositories> <repository> <id>company-nexus</id> <url>https://nexus.company.com/repository/maven-public/</url> <releases><enabled>true</enabled></releases> <snapshots><enabled>true</enabled><updatePolicy>always</updatePolicy></snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>company-nexus</id> <url>https://nexus.company.com/repository/maven-public/</url> </pluginRepository> </pluginRepositories> </profile> <!-- 外网/家庭环境 --> <profile> <id>home</id> <repositories> <repository> <id>aliyun</id> <url>https://maven.aliyun.com/repository/public</url> <releases><enabled>true</enabled></releases> <snapshots><enabled>false</enabled></snapshots> </repository> </repositories> </profile> </profiles> <!-- 默认激活公司环境 --> <activeProfiles> <activeProfile>office</activeProfile> </activeProfiles>切换方式:
方式一:改 <activeProfiles> 里的值(永久切换)
方式二:命令行临时指定:
# 临时用 home profile 构建 mvn clean package -P home # 临时禁用 office profile mvn clean package -P !office方式三:用激活条件自动切换(后面讲)
自动激活:基于文件或环境变量
<profile> <id>ci</id> <activation> <!-- 当环境变量 CI=true 时自动激活 --> <property> <name>env.CI</name> <value>true</value> </property> </activation> <properties> <maven.test.skip>true</maven.test.skip> </properties> </profile><profile> <id>jdk17</id> <activation> <!-- 当 JDK 版本为 17 时自动激活 --> <jdk>17</jdk> </activation> <properties> <maven.compiler.release>17</maven.compiler.release> </properties> </profile><profile> <id>has-settings-file</id> <activation> <!-- 当某个文件存在时自动激活 --> <file> <exists>${user.home}/.company-vpn-active</exists> </file> </activation> <!-- 只有连了 VPN(创建了标记文件)才走内网仓库 --> </profile>最后这个 file 激活的玩法挺实用:连上 VPN 后 touch ~/.company-vpn-active,断开后 rm ~/.company-vpn-active,Maven 自动切换仓库源,不用手动改 settings.xml。
用 properties 统一版本号
profile 里可以定义 properties,然后在 pom.xml 里用 ${xxx} 引用:
<profile> <id>dev</id> <properties> <deploy.env>dev</deploy.env> <spring.profiles.active>dev</spring.profiles.active> </properties> </profile> <profile> <id>prod</id> <properties> <deploy.env>prod</deploy.env> <spring.profiles.active>prod</spring.profiles.active> </properties> </profile>配合 maven-resources-plugin 的 filtering 功能,可以在打包时把 application.yml 里的占位符替换成对应环境的值。不过现在 Spring Boot 项目更推荐用 spring.profiles.active 运行时切换,而不是编译期替换——编译期替换意味着每个环境要单独打一次包,容易出错。
完整示例:我目前在用的 settings.xml
下面是一份整合了上面所有内容的完整文件,改改 URL 和账号就能直接用:
<?xml version="1.0" encoding="UTF-8"?> <settings xmlns="http://maven.apache.org/SETTINGS/1.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd"> <!-- 本地仓库 --> <localRepository>D:/maven-repo</localRepository> <!-- 镜像:只代理 central,排除私服 --> <mirrors> <mirror> <id>aliyun-public</id> <name>Aliyun Maven Public</name> <url>https://maven.aliyun.com/repository/public</url> <mirrorOf>central,!company-nexus</mirrorOf> </mirror> </mirrors> <!-- 认证 --> <servers> <server> <id>company-nexus</id> <username>zhangsan</username> <password>{aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT0uV1wX2yZ3aB4c=}</password> </server> <!-- deploy 用的认证,id 必须和 pom 的 distributionManagement 一致 --> <server> <id>nexus-releases</id> <username>zhangsan</username> <password>{aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT0uV1wX2yZ3aB4c=}</password> </server> <server> <id>nexus-snapshots</id> <username>zhangsan</username> <password>{aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT0uV1wX2yZ3aB4c=}</password> </server> </servers> <!-- Profile:公司环境 --> <profiles> <profile> <id>company</id> <repositories> <repository> <id>company-nexus</id> <url>https://nexus.company.com/repository/maven-public/</url> <releases><enabled>true</enabled></releases> <snapshots> <enabled>true</enabled> <updatePolicy>interval:5</updatePolicy> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>company-nexus</id> <url>https://nexus.company.com/repository/maven-public/</url> </pluginRepository> </pluginRepositories> </profile> </profiles> <activeProfiles> <activeProfile>company</activeProfile> </activeProfiles> </settings><updatePolicy>interval:5</updatePolicy> 表示 snapshot 版本每 5 分钟检查一次更新,不会每次构建都去拉。开发阶段频繁改 snapshot 时可以改成 always,稳定后改成 daily。
常见问题排查
拉包报 404 Not Found:
先确认这个包在 Maven Central 上存不存在(去 https://search.maven.org 搜)。如果 Central 上没有、只在公司私服上有,那多半是 mirrorOf=* 把私服请求劫持到了公共镜像。改成 mirrorOf=central,!私服id。
拉包报 401 Unauthorized:
<server> 的 id 和仓库的 id 不匹配。用 mvn help:effective-settings 看最终生效的 server 列表,再跟 mvn dependency:resolve -X 输出的仓库 id 对比。
snapshot 版本拉不到最新:
Maven 默认对 snapshot 的更新策略是 daily——一天只检查一次。强制刷新:
mvn clean package -U-U 参数强制更新所有 snapshot 依赖。
IDEA 和命令行行为不一致:
IDEA 可能用了自己 bundled 的 Maven 和另一份 settings.xml。检查:Settings → Build Tools → Maven → Maven home path 和 User settings file,确保跟命令行用的是同一套。
mvn deploy 报 400 Bad Request:
往 release 仓库重复 deploy 同一个版本号会被拒绝(Nexus 默认策略)。要么升版本号,要么在 Nexus 上把对应仓库的 Deployment Policy 改成 Allow redeploy(不推荐,release 版本应该不可变)。
下载速度慢:
跑 mvn dependency:resolve -X,看日志里实际请求的 URL 是不是你配的镜像地址。如果不是,说明 mirror 没生效——检查 mirrorOf 是否匹配了目标仓库的 id。
仓库路径含 _remote.repositories 文件导致本地 jar 不生效:
本地仓库每个目录下有个 _remote.repositories 文件,记录了 jar 是从哪个远程仓库下载的。如果你手动把 jar 复制到本地仓库,Maven 可能因为它不认这个来源而忽略它。解法:删掉那个目录下的 _remote.repositories,或者用 mvn install:install-file 正规安装:
mvn install:install-file \ -Dfile=path/to/your.jar \ -DgroupId=com.example \ -DartifactId=your-lib \ -Dversion=1.0.0 \ -Dpackaging=jar小结
settings.xml 配置的核心逻辑就三件事:从哪里拉包(mirrors + repositories)、用什么身份拉(servers + 加密)、什么条件下走哪套配置(profiles + activation)。
日常开发建议只维护一份 User 级别的 ~/.m2/settings.xml,用 profile 做环境切换而不是维护多份文件。密码一定要加密,settings-security.xml 不要提交到版本控制。
如果你现在的 settings.xml 是从别人那里抄来的,花十分钟跑一下 mvn help:effective-settings,看看实际生效的配置是不是你以为的那样——很多时候问题就出在"以为配了但其实没生效"。