Maven 3.6.2 内网离线构建配置与排错实战

发布时间:2026/11/4 14:54:23
Maven 3.6.2 内网离线构建配置与排错实战

简介:Apache Maven 3.6.2 的完整发行压缩包,面向 Java 开发者、构建工程师以及需要统一项目依赖与生命周期管理的技术团队。它提供由标准化 POM 驱动的构建能力,可用于依赖解析、编译测试、打包部署及多模块工程管理,适合从入门配置到企业级构建流程搭建等场景。包内共 68 个文件,以 42 个 jar 核心库与插件依赖为主,辅以 license、notice 等许可说明,cmd、mvn 等跨平台启动脚本,以及 xml、conf、properties 等配置与日志文件,整体约 8.77MB,解压后可直接通过命令行使用。目前已有 325 人浏览学习。压缩包保留 boot、bin、conf、lib 等完整目录结构,便于读者快速了解 Maven 自身的组成与启动机制,也可作为配置全局参数、排查依赖冲突和理解插件加载流程的参考样本,对希望掌握 Java 项目构建体系的开发者具有实用价值。

1. 断网构建机上的那个 zip:Maven 3.6.2 到底解决什么问题

上周要把一个三年前的老工程重新出包,构建机是一台不能连外网的内网虚拟机,装不了在线安装器,能带进去的只有一个apache-maven-3.6.2.zip。解压、配两个环境变量、跑一条mvn -v,二十分钟后构建脚本就重新跑起来了。这就是 Maven 这类构建工具最朴素的价值:它把「依赖从哪来、源码怎么编、产物怎么打」这三件事写成了一份可重复执行的说明书,而 zip 分发包恰好是这份说明书最不需要权限的载体——不写注册表、不依赖包管理器、换台机器拷过去照样能用。这份资源适合三类人:需要在隔离环境里搭构建链路的人、要复现一个老项目构建过程的人,以及想把 Maven 的目录、配置、生命周期一次摸清的人。下面按「解压能用 → 配置调优 → 工程落地 → 排错 → 固化」的顺序走一遍。

2. 解压到能用:目录结构、环境变量与第一条 mvn 命令

拿到 zip 之后最容易犯的错是双击解压到桌面、然后在任意目录敲mvn,结果报「不是内部或外部命令」。这不是包坏了,是路径没进环境变量。先把目录看懂,再谈配置。

2.1 为什么很多团队还停在 3.6.2

版本选择这件事在真实项目里往往不是「越新越好」,而是「和现有插件、JDK、私服协议对得上」。3.6.x 这条线的一大特点是它对 JDK 8 时代的插件生态非常宽容,很多老项目的maven-compiler-plugin、maven-resources-plugin版本都停留在那个年代,换到更新的 3.x 尾号版本或 4.x 之后,插件默认行为、参数校验严格程度会变,原本能过的构建可能直接报插件参数非法。我在内网环境里一般会遵循一条原则:构建工具版本跟着项目走,不跟着个人喜好走。如果整个代码库的 pom 里写的插件版本都在 3.6 时代,那就把 3.6.2 作为基线固定下来,用第 6 章讲的 wrapper 把它钉死,避免某台机器上装了别的版本导致「我这里能过你那里不过」。

另一个现实理由是分发包形态。zip 是二进制分发包,解压即得一份完整的、自包含的运行时:lib下的依赖 jar 都在里面,不需要联网拉引导程序。这一点在隔离网络里是决定性的。

2.2 目录里到底放了什么

解压后得到的目录结构是固定的,把每个目录的职责搞清楚,后面出问题才知道去哪找答案。常见结构如下:

目录/文件内容我一般动不动它
bin/mvn(Linux/macOS 脚本)、mvn.cmd(Windows)、m2.conf不动,除非要改 JVM 启动参数默认值
boot/类加载器引导 jar,负责把lib下的组件拼起来绝对不动
conf/settings.xml全局配置:本地仓库、镜像、profile、认证会改,但更推荐用用户级配置
conf/logging/日志输出级别配置排错时临时调成 debug
lib/Maven 自身运行所需的全部 jar不动,也不要手动往里塞 jar

conf/settings.xml和用户目录下的~/.m2/settings.xml是两份配置,后者优先级更高。多人共用的机器上改全局配置会影响别人,所以我习惯只在conf/settings.xml里做「兜底默认值」,把和个人环境相关的东西(比如本地仓库路径、私服账号)放到用户级配置里。排错时如果发现「明明改了配置却不生效」,第一反应就是去看是不是改错了层级——用mvn help:effective-settings可以直接把合并后的最终配置打出来,比猜快得多。

2.3 环境变量与验证

Linux/macOS 上的做法很直接,把解压目录固定到一个不会随手删的位置,再把bin挂到 PATH 上:

# 解压到 /opt,注意 -d 指定目标目录,避免解压出一堆散文件 unzip apache-maven-3.6.2.zip -d /opt # MAVEN_HOME 指向解压出来的那一层,不要指到 bin 里面 export MAVEN_HOME=/opt/apache-maven-3.6.2 export PATH=$MAVEN_HOME/bin:$PATH # JVM 参数:堆给足一点,同时把默认编码钉成 UTF-8,避免日志乱码 export MAVEN_OPTS="-Xms256m -Xmx1024m -Dfile.encoding=UTF-8" # 验证:能打出 Java 版本、Maven 版本、Maven 主目录三行就说明通了 mvn -v

这段脚本的关键点在三个地方。MAVEN_HOME必须指向包含bin、conf、lib的那一层,指向bin会导致后续某些插件找不到conf下的配置;PATH的追加顺序决定当机器上装了多个 Maven 时哪个优先,写在前面的生效,所以排查「版本不对」时先which mvn再mvn -v;MAVEN_OPTS里的-Dfile.encoding=UTF-8是老项目里最常见的一处预防性设置,源文件是 UTF-8、系统默认编码是 GBK 的环境里,不加这一条很容易在控制台看到一片问号,然后误以为是代码问题。

Windows 上不需要手写 PATH,把解压目录配到系统变量的MAVEN_HOME,再在Path里追加%MAVEN_HOME%\bin,重开一个命令行窗口敲mvn -v即可。注意必须重开窗口,已经打开的终端不会自动读到新的环境变量,这一点每年都能坑到人。输出里如果只有 Maven 版本没有 Java 版本,那说明 JAVA_HOME 没配对,Maven 找不到 JDK,先解决 JDK 再回来看 Maven。

提示:验证阶段不要急着建工程,先在空目录跑一次mvn -v和mvn help:system。后者会把系统属性、环境变量、本地仓库路径全部打印出来,等于给这套安装做一次体检,后面所有诡异问题都能拿这份输出对照。

3. settings.xml 改三处:本地仓库、镜像匹配与编译 JDK

装好之后不改配置就用,会遇到两个必然结果:本地仓库默认落在用户目录下,一个稍大的工程就能把系统盘吃满;依赖从默认的公共仓库拉取,在受限网络里慢到让人怀疑人生。配置层面要动的其实只有三处地方,但每一处的细节都能写出一堆坑。

3.1 localRepository:把仓库挪出系统盘

<localRepository>是唯一一个真正意义上的路径配置。它决定所有下载下来的 jar 缓存在哪。默认值在用户目录的.m2/repository下,一台开发机上这个目录长到十几 G 是常态。换到独立数据盘的做法很简单,把绝对路径写进去即可。这里有个细节:路径分隔符在 Windows 上写成/也能识别,写成\反而在某些版本的解析里出问题,所以统一用正斜杠是省事的做法。

另一个常被忽略的点是多版本共存时的仓库共享。同一台机器上装了 3.6.2 和别的版本,如果它们指向同一个localRepository,缓存是共用的。这通常是好事(省磁盘、省下载),但代价是当某个版本写入了一半的元数据、另一个版本读到时可能报「找不到某个 snapshot」。所以在做版本切换调试时,我一般会临时用命令行参数把仓库指到另一个目录:mvn -Dmaven.repo.local=/data/repo-test clean package,问题复现完再切回来,比反复改配置文件干净。

3.2 mirror 的 mirrorOf 写法决定命中范围

镜像配置是问题最集中的地方。<mirrorOf>的值不是随便写的,它决定这个镜像拦截哪些仓库请求,几个常用取值差别很大:

mirrorOf 取值含义适用场景
central只拦截 id 为 central 的仓库只想给中央仓库换源,其他自定义仓库保持原样
*拦截所有仓库请求内网只有唯一出口,所有依赖必须走私服
external:*拦截所有非本机、非文件的仓库既想走内网私服,又允许file://本地仓库
*,!internal拦截全部,但排除 id 为 internal 的仓库私服自己也是被依赖的仓库时避免自循环

写*的时候最容易出事:如果 pom 里或某个插件的配置里声明了一个file://协议的本地仓库,被*一并拦掉之后会报找不到构件,这时候换成external:*就通了。反过来,如果公司内网私服的 id 恰好是central,那你写的central镜像会覆盖它,表现是「明明配了私服却还是去公网拉」,用mvn help:effective-settings一看便知。

镜像的<id>还有一个隐藏作用:它是认证信息的关联键。私服需要账号时,用户名密码要写在<servers>里、<id>与镜像的<id>严格一致,差一个字符就认证失败,报 401。这是我见过最多的低级错误之一,且报错信息完全不会提示你去检查 id 匹配。

3.3 profile 里锁 JDK 与源文件编码

第三处是编译参数。很多人喜欢在 pom 里写maven-compiler-plugin的source/target,这在单项目里没问题,但在一个父 pom 管着几十个子模块的仓库里,逐个改 pom 不现实,改父 pom 又要走评审。折中做法是在 settings 的 profile 里把maven.compiler.source、maven.compiler.target、project.build.sourceEncoding三个属性设成默认值,各模块的 pom 里如果显式写了就会覆盖,没写就继承这里的值。

profile 的激活方式需要注意一点:<activeByDefault>true</activeByDefault>和<jdk>同时写是自相矛盾的。activeByDefault的语义是「没有别的 profile 被激活时我才生效」,一旦你同时写了 jdk 条件,某些场景下它会被判定为「已被其他条件激活」,行为就不符合预期了。我的写法是只用<jdk>条件激活,简单直接。

3.4 一份可直接抄的骨架

把上面三处合起来,用户级settings.xml的骨架大致是这样:

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"> <!-- 1. 本地仓库挪出系统盘,路径统一用正斜杠 --> <localRepository>D:/maven-repo</localRepository> <!-- 2. 镜像:只拦非本机的请求,保留 file:// 本地仓库可用 --> <mirrors> <mirror> <id>internal-public</id> <mirrorOf>external:*</mirrorOf> <url>https://maven.internal.example.com/repository/maven-public/</url> </mirror> </mirrors> <!-- 私服要认证时,id 必须和上面 mirror 的 id 一模一样 --> <servers> <server> <id>internal-public</id> <username>build</username> <password>******</password> </server> </servers> <!-- 3. 编译默认值:JDK 8 + UTF-8,子模块没显式写就继承 --> <profiles> <profile> <id>jdk8-utf8</id> <activation> <jdk>1.8</jdk> </activation> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> </profile> </profiles> <activeProfiles> <activeProfile>jdk8-utf8</activeProfile> </activeProfiles> </settings>

改完之后一定要跑一次mvn help:effective-settings看合并结果,确认三件事:localRepository是你写的那个绝对路径、镜像的mirrorOf没被别的配置覆盖、profile 确实在激活列表里。这三条对上了,后面九成的基础问题都不会发生。命令行里也可以用mvn -s /path/to/settings.xml临时指定配置文件,做对比测试时比来回改文件快。

4. 从 pom.xml 到可交付产物:依赖解析、生命周期与多模块

配置通了,接下来是把一个真实工程跑起来。Maven 的核心模型其实就三样东西:坐标定身份、仓库定来源、生命周期定步骤。把这三样串起来,命令行敲什么、产物出在哪、为什么这样出,就都清楚了。

4.1 最小可用 pom 与坐标的三段含义

一个能编译的 pom 至少要声明modelVersion、groupId、artifactId、version四样。坐标的三段各有分工:groupId是组织或项目组维度,通常写反域名;artifactId是模块名;version是版本。之所以强调这个分工,是因为本地仓库的存储路径完全由坐标拼出来——groupId的点变斜杠,再加artifactId和version,所以你从别的机器拷一个 jar 进来对不对路,看它放在仓库的哪个目录就一目了然。

<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <!-- 坐标三段:决定构件在本地仓库里的目录位置 --> <groupId>com.example.demo</groupId> <artifactId>order-service</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>jar</packaging> <properties> <!-- 编码与编译级别:建议和 settings 里的 profile 保持一致,避免两处打架 --> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.example.lib</groupId> <artifactId>common-utils</artifactId> <version>2.3.1</version> <!-- 只在编译期需要、不打进运行时的依赖必须写 scope,否则产物会带上不该带的 jar --> <scope>provided</scope> </dependency> </dependencies> </project>

packaging决定最终产物类型,jar走默认的打包绑定,war会额外触发 web 相关插件,pom通常给父工程或聚合工程用。scope是最容易被滥用的一处:provided表示编译和测试可见、运行时不打包,容器已经提供的库应该用它;test表示只对测试可见,JUnit 这类必须写;runtime表示编译时不需要、运行时需要,JDBC 驱动是典型。写错 scope 的后果往往延迟到部署阶段才暴露:本地跑得好好的,一上容器就ClassNotFoundException。

4.2 生命周期与实际命令对照

Maven 的三套生命周期(clean、default、site)里,日常只用到 default 的前七步。每一步都不是「命令」,而是「阶段」,敲某一阶段时它前面的所有阶段都会依次执行。这个规则解释了很多人的困惑:为什么mvn package会自动跑测试。

阶段实际做的事常见用途
validate校验 pom 结构与配置完整性改完 pom 先跑一次,比直接打包快
compile编译主源码到target/classes快速查语法错误
test跑测试用例提交前过一遍
package打成 jar/war 到target/出交付物
verify跑集成测试与校验插件流水线里的质量门禁
install装进本地仓库,供同机其他模块引用多模块本地联调
deploy上传到远端仓库发布 SNAPSHOT 或正式版

几个高频开关值得记住:-DskipTests跳过测试执行但仍编译测试代码,-Dmaven.test.skip=true连测试代码都不编译(速度更快,但会漏掉测试编译错误);-U强制刷新 SNAPSHOT 依赖,解决「别人更新了我这边还是旧的」;-o离线模式,只用本地仓库;-X输出调试日志,排错时用-X加管道过滤关键字比翻全量日志有效。

4.3 依赖冲突:先看树,再排

中大型工程里最耗时的不是写代码,是定位NoSuchMethodError。这个异常的意思是:编译时用的是 A 版本,运行时类加载器加载到的是 B 版本。Maven 的依赖调解规则是「路径最短优先,路径一样长时先声明优先」,这条规则本身简单,但传递依赖会让路径变得难以肉眼推算。

第一步永远是打依赖树,不要靠猜:

# 打印当前模块的依赖树,-Dverbose 会显示被忽略的版本和冲突原因 mvn dependency:tree -Dverbose -DoutputFile=tree.txt # 只看某个 groupId:artifactId 的来龙去脉,快速定位是谁把它引进来的 mvn dependency:tree -Dincludes=org.slf4j:slf4j-api -Dverbose # 编译期与运行期分开看,scope 不同结果可能不一致,这一点常被忽略 mvn dependency:tree -Dscope=compile

-Dverbose的价值在于它会打印omitted for conflict with ...这样的行,直接告诉你哪个版本被谁顶掉了。-Dincludes的格式是groupId:artifactId,支持通配符,只查可疑的那一条链比读几百行树高效得多。把输出重定向到文件再检索,也比在滚屏里找舒服。

确认了冲突来源之后就两种处理方式:在自己的 pom 里直接声明想要的版本(显式声明路径最短,一定获胜),或者在引入方上加exclusion把传递依赖切断。前者简单粗暴、适合统一版本,后者更精确、适合「这个库自带的旧版驱动必须去掉」的场景。

<dependency> <groupId>com.example.sdk</groupId> <artifactId>payment-sdk</artifactId> <version>1.6.0</version> <exclusions> <!-- 这个 SDK 自带的日志实现必须排除,否则会和主工程的日志门面打架 --> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> </exclusion> </exclusions> </dependency>

排除之后记得再跑一次依赖树验证,并且确认排除的是实现而不是门面。把slf4j-api排除掉、日志实现留着,编译能过,启动就报找不到LoggerFactory。这类错误的排查成本很高,因为异常栈不会告诉你是 pom 的锅。

4.4 多模块:聚合与继承别混为一谈

多模块工程的 pom 里常同时出现<modules>和<parent>,但它们解决的是两个完全不同的问题。<modules>是聚合,作用是「在根目录敲一条命令,所有列出的模块按顺序构建」;<parent>是继承,作用是「子模块继承父 pom 的依赖管理、插件配置、属性」。一个 pom 可以只有聚合没有继承,也可以只有继承没有聚合,混淆这两个概念会导致子模块莫名其妙地多出一堆依赖。

<project> <modelVersion>4.0.0</modelVersion> <groupId>com.example.demo</groupId> <artifactId>demo-parent</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>pom</packaging> <!-- 聚合:构建入口,列出所有子模块;顺序不等于构建顺序,Maven 会自己算依赖图 --> <modules> <module>common-utils</module> <module>order-service</module> <module>gateway</module> </modules> <properties> <spring.version>5.3.20</spring.version> </properties> <dependencyManagement> <dependencies> <!-- 只管版本不管引入:子模块写依赖时可以不写 version --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> </dependencies> </dependencyManagement> </project>

dependencyManagement与dependencies的区别是新人最容易踩空的地方:前者只声明版本,不会真的引入依赖;后者才真正引入。如果只在父 pom 的dependencyManagement里写了版本,子模块不写<dependency>是拿不到这个库的,表现是编译报「找不到符号」。

多模块场景下还有两个实用参数:-pl order-service -am表示只构建指定模块以及它依赖的模块,改一个子模块时能省掉大量时间;-rf :order-service表示从上次失败的模块继续。这两个参数在十几个模块的仓库里能省出可观的等待时间。

5. 排查清单:Maven 3.6.2 上翻车最多的五个场景

下面五条都是我在实际项目里反复见到的,按「现象 → 原因 → 解决」写,遇到问题时可以直接对号入座。

5.1 编译报 invalid target release,或控制台中文全是问号

现象:mvn compile报invalid target release: 1.8,或者编译能过但控制台输出的中文变成乱码、源文件里的中文注释被识别成非法字符。

原因:报 target release 说明当前跑 Maven 的 JVM 是低于 1.8 的版本,也就是JAVA_HOME指错了。乱码则是project.build.sourceEncoding没设或者设成了GBK,而源文件实际是 UTF-8,两条路径的编码不一致。

解决:先mvn -v确认 Java 版本,再检查JAVA_HOME是否指向正确的 JDK 而不是 JRE。编码问题在 pom 或 settings 的 profile 里统一声明project.build.sourceEncoding=UTF-8,同时把MAVEN_OPTS加上-Dfile.encoding=UTF-8。注意 pom 里如果同时存在maven.compiler.source属性和maven-compiler-plugin的<source>配置,后者优先级更高,两处不一致时以插件配置为准,这是「明明改了属性却没生效」的常见来源。

5.2 依赖明明下过,却一直重新下载或者报找不到

现象:某次构建中断之后,mvn package一直卡在下载,或者报Could not resolve dependencies,但去本地仓库一看那个 jar 文件就在那里。

原因:下载中断会在仓库目录里留下.lastUpdated后缀的标记文件,Maven 看到这个标记就认为上次失败过,默认策略下不会立即重试,某些情况下还会直接判定该构件不可用。另一种情况是 jar 文件下了一半,大小不对,解压校验时失败。

解决:定位到具体目录,把.lastUpdated文件清掉,必要时把该构件的整个版本目录删掉重下,然后加-U强制刷新。批量清理可以先找到再删:

# 找出所有下载失败留下的标记文件,先看一眼数量再决定怎么删 find ~/.m2/repository -name "*.lastUpdated" | wc -l find ~/.m2/repository -name "*.lastUpdated" -delete # 强制刷新 SNAPSHOT,配合 -U 使用 mvn -U clean package

如果是 SNAPSHOT 依赖始终不更新,重点看仓库配置里该仓库的snapshots是否被设为enabled=false,以及本地仓库里对应目录下是否有maven-metadata-*.xml的旧缓存。

5.3 命令行能构建,IDE 里死活不行

现象:终端里mvn clean package一切正常,切到 IDE 里点构建就报找不到依赖、JDK 版本不对、或者依赖列表和命令行不一致。

原因:IDE 内置了自己的 Maven 运行时和配置解析逻辑,很多 IDE 默认用 bundled 版本而不是你解压的那份,配置也可能读的是另一路径下的 settings.xml。两边用的压根不是同一套东西。

解决:在 IDE 的构建工具设置里显式指定 Maven 主目录为解压出来的那个路径、指定用户级 settings.xml 的绝对路径、指定本地仓库路径,三样对齐之后再刷新项目。改完之后对比两边mvn -v的输出是否一致。这一步做完还没好的话,去 IDE 的构建输出里找它实际执行的命令行参数,加-X复跑一遍,差异往往就在某个被 IDE 自作主张加上或去掉的参数上。

5.4 运行期 NoSuchMethodError / ClassNotFoundException

现象:编译零错误,单元测试也过,一启动就抛出NoSuchMethodError或ClassNotFoundException,栈里指向的类明明在源码里能搜到。

原因:依赖冲突或 scope 写错。前者是同一坐标的多个版本被同时引入,编译期用 A、运行期加载了 B;后者是某个必要的实现在打包时被provided或test排除了。

解决:先mvn dependency:tree -Dverbose找到被省略的版本,用显式声明或exclusion收敛到唯一版本;再检查最终产物里的内容,jar tf target/xxx.jar | grep 类名或者直接看target/*-classes与依赖目录,确认那个类到底有没有被打进去。provided用错的典型特征是「本地跑通、一上容器就崩」,因为容器里并不像你以为的那样提供了那个库。

5.5 打包越来越慢,甚至 OOM

现象:构建时间从两分钟涨到十分钟,日志里出现GC overhead limit exceeded或OutOfMemoryError: Java heap space,多模块工程尤其明显。

原因:默认堆偏小、模块数量多、插件在 fork 出来的 JVM 里跑测试或打包,内存不够就频繁 GC。另一个常见原因是每次构建都在重复解析大量 SNAPSHOT 元数据。

解决:调MAVEN_OPTS的堆上限,比如-Xmx2048m,同时确认 fork 出去的编译进程有独立内存参数(maven-compiler-plugin的<fork>true</fork>时要单独配<meminitial>/<maxmem>)。缩短构建时间优先用-pl ... -am做增量、用-o走离线、用-T 1C开并行构建(注意并行对插件线程安全有要求,老插件可能不兼容,先在小工程验证)。

6. 固化构建:wrapper 锁版本与离线仓库打包

上面所有配置都有一个前提:机器上得先有这个 Maven。换个人、换台机器、换条流水线,版本又可能不一样。真正把构建固定下来,靠的是两件事——把 Maven 版本写进代码库,把依赖提前搬进离线仓库。

6.1 用 wrapper 把 3.6.2 钉在项目里

Maven Wrapper 的思路和常见的「把工具版本写进仓库」一样:在项目根目录生成mvnw、mvnw.cmd和.mvn/wrapper/下的配置,之后所有人用./mvnw构建,wrapper 发现本地没有对应版本就去下载并缓存,版本号写在配置文件里,跟着代码走代码评审。生成只需一条命令:

# 在项目根目录执行,-N 表示不递归到子模块 # -Dmaven=3.6.2 把版本写死,避免生成时用了机器上当前的版本 mvn -N wrapper:wrapper -Dmaven=3.6.2 # 生成后验证:确认输出里的 Maven 版本就是 3.6.2,且主目录指向 .m2/wrapper 下的缓存 ./mvnw -v # 之后所有构建都走 wrapper,包括 CI 脚本 ./mvnw -U clean package -DskipTests

生成之后要检查.mvn/wrapper/maven-wrapper.properties里的分发地址和版本号,确认它指向的是你希望的那份 zip 分发包。内网环境里没有外网下载条件的话,把apache-maven-3.6.2.zip放到内网文件服务上,把distributionUrl改成对应的内网地址,wrapper 就会从那里取——这一步做完,构建链路才算真正自洽:代码库、工具版本、分发包来源三样全在内网闭环里。

有一个坑得提一句:wrapper 首次执行时的下载是串在构建流程里的,如果地址不通,报错信息往往只是一句「下载失败」,看不出是网络问题还是地址写错。我一般会先手动curl -I一下distributionUrl,确认能通再让 wrapper 去跑,能省掉一轮无谓的排查。

6.2 把依赖提前搬进离线仓库

隔离环境的第二件麻烦事是依赖。做法是先在一台能访问私服的机器上把依赖全拉下来,再把整个仓库目录打包搬进去:

# 1. 预拉依赖:go-offline 会把 pom 里声明的依赖、插件都尽量拉全 # 注意它不保证 100% 完整,动态版本和某些插件运行时依赖可能漏 mvn dependency:go-offline # 2. 用独立的仓库目录做一次全量构建,确保所有需要的构件都落在这个目录里 # 这样打出来的包是"自洽"的,可以直接整体拷进隔离环境 mvn -Dmaven.repo.local=./.m2-bundle clean package # 3. 隔离环境里用 -o 走离线模式,仓库指向拷进来的目录 mvn -o -Dmaven.repo.local=/data/repo-bundle clean package

dependency:go-offline的实际行为和名字给人的印象有差距:它能把主要依赖拉全,但对动态版本、profile 里条件激活的依赖、部分插件在运行时才解析的构件可能覆盖不到。所以第二步的全量构建是必要的补充——以一次成功构建为准,而不是以 go-offline 成功为准。判断离线包是否完整,最靠得住的验证就是把网络断掉(或加-o)跑一遍完整构建,能出产物才算过。

6.3 我固定走的那一遍流程

现在每次接手一个要在隔离环境里构建的工程,我都会按固定顺序走一遍:先mvn -v确认 JDK 和 Maven 对得上,再mvn help:effective-settings确认仓库路径和镜像命中,然后mvn dependency:tree -Dverbose落一份依赖树存档,接着mvn -N wrapper:wrapper -Dmaven=<version>把版本钉住,最后用独立仓库目录跑一次全量构建,把整个目录打进离线包。这五步做下来大概十几分钟,但能挡掉后面几小时的扯皮。

有一次赶工期跳过第二步,直接拿了一份别人的 settings.xml 用,结果镜像的mirrorOf写成了*,把项目里声明的本地文件仓库一起拦了,构建报「找不到构件」,翻了两小时日志才发现是一行配置的事。从那以后我每次改完 settings 都强制走一遍help:effective-settings,把合并结果和预期对着看一眼再开工。希望帮到你。

本文还有配套的精品资源,点击获取