一个项目的构建在开发机上很顺,到了内网的 CI 机器上必然失败在"下载依赖"——因为 CI 机器"能访问内网私服,但访问不了公网",而某个新加的依赖"私服里还没有代理过"。
离线构建的问题不是"要不要联网",而是"依赖从哪里来、什么时候进来"。这个三层的"渐进"很实用:多数团队只需要第一层(代理),少数需要第三层(完全隔离)——而"一开始就上离线包"成本高且维护重。
而它有三个层次:镜像/代理(缓存)、锁定(可复现)、离线包(完全隔离)。
1. 三种"离线"程度
| 程度 | 机制 |
|---|---|
| 代理缓存(私服) | 首次联网取,之后缓存 |
| 锁定 + 预取(vendor/镜像) | 依赖随代码走 |
| 完全离线(离线包) | 依赖由"离线包"提供 |
第一项是"最常见的内网形态":私服代理公网仓库,于是"用过的依赖"能在内网取到——而"新依赖"第一次仍需要"私服能访问公网"。
第三项是"军工/金融隔离网"的形态:依赖被打成"离线包",随版本发布第三项的"维护要点"是"离线包与代码版本一一对应":代码加了新依赖,离线包必须同步重建——否则"用旧包构建新代码"必然失败。
——于是"构建完全不需要网络"。
2. "私服没代理过"的三种表现
| 表现 | 原因 |
|---|---|
| 404(依赖不存在) | 私服没代理过,且不能连公网 |
| 401/403 | 私服需要认证,配置缺失 |
| 卡在"下载中" | 私服尝试回源,超时 |
第三项的"误导性"在"日志":报错写的是"连接超时"或"read timed out",于是排查方向变成"网络问题",而不是"依赖不存在"。
第三项最费时间:它在"尝试回源"上耗掉几分钟,然后失败——而"失败信息"往往是"超时",不是"依赖缺失"。
```tex第三项(离线构建缺插件)的"预防"是"把"构建命令"本身也放进预取流程":先 go-offline,再 -o package 跑一次——两次都过才说明"预取完整"。
t
// 三种配置(按"私服能力"选)
私服可回源:只配 mirror(首次自动代理)
私服不可回源:必须"手工导入"或"用离线包"
混合:核心依赖固定版本 + 私服回源兜底
// 判据:CI 机器能否访问公网(哪怕只能访问私服)
## 3. "vendor / 离线包"的三种做法
| 做法 | 特点 |
|---|---|
| Go:`vendor/` 目录 | 依赖随仓库(体积大) |
| Maven:`mvn dependency:go-offline` | 预取到本地仓库 |
| Node:`node_modules` 打包 或 `yarn offline mirror` | 体积大但完全离线 |
第二项很实用:**它把"构建所需"全部下到本地 `.m2`**第二项的"边界"是"**它不覆盖"运行时才下载"的东西**"(如某些插件的"动态解析")——所以"预取完还要在离线环境"试跑一次全量构建"。
,于是"之后构建不需要网络"——而它需要在"能联网的机器"上跑一次。
```bash
// Maven:一次预取,之后离线构建
mvn -q dependency:go-offline # 在能联网的机器上跑
// 打包 ~/.m2 到离线环境
tar -czf m2-offline.tar.gz -C ~ .m2
// 离线构建(强制离线,缺依赖立刻失败而不是"卡超时")
mvn -o -B clean package
// 关键:-o 让"缺依赖"变成"立刻报错",而不是"卡在下载"
4. "锁定"为什么是离线的前提
| 有锁文件 | 无锁文件 |
|---|---|
| 依赖版本确定 | 每次解析结果可能不同 |
| 离线包可对应 | 无法知道"该打哪些包" |
这个"同一件事"的解释是"离线包只能按"确定的依赖集"打":没有锁文件,"依赖集"本身是"每次解析的产物"——于是"包"与"构建"对不上。
所以"离线构建"与"可复现构建"是同一件事的两面:没有锁文件,"离线包"就不知道"该包含什么"。
"镜像/代理"的配置要写清"回源策略",否则行为不可预期:
<!-- settings.xml:镜像 + 不可回源的私服 -->
<mirrors>
<mirror>
<id>internal</id>
<mirrorOf>*</mirrorOf> <!-- 所有请求都走私服 -->
<url>http://nexus.internal/repository/maven-public/</url>
<blocked>false</blocked>
</mirror>
</mirrors>
<!-- 判据:私服"是否允许回源"决定"新依赖能不能自动取到" -->
5. 三种"离线构建"的失败模式
| 失败 | 成因 |
|---|---|
| 缺一个传递依赖 | 预取不完整 |
| 版本与在线不同 | 锁文件未更新 |
| 插件缺失 | 插件也需预取 |
第三项最容易被忽略:构建插件(如 maven-shade-plugin)本身也是依赖第三项的"典型表现"是"编译过了、打包失败":依赖都在,而"打包插件"没被预取——于是"到最后一个阶段才失败"。
,而"预取主依赖"不一定包括它。
// 预取要覆盖的四类
1 编译依赖(compile)
2 运行依赖(runtime)
3 测试依赖(test)
4 插件与其依赖(plugin + pluginDependencies)
// 缺任一类 → 离线构建在"某个阶段"失败,而阶段不同、报错不同
"离线构建"应当有自己的验收命令,跑在"干净环境"里:
// 三个命令(每个都能独立暴露一类缺失)
mvn -o -B validate # 依赖解析(缺依赖立刻失败)
mvn -o -B test-compile # 测试依赖
mvn -o -B package # 插件(最容易缺)
// 关键:三个都过,才算"离线包完整"
6. 排错:五种"离线构建问题"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 404 某个依赖 | 私服未代理 | 手工导入/加离线包 |
| 卡几分钟后超时 | 私服尝试回源 | 配 -o 或禁回源 |
| 离线构建缺插件 | 未预取插件 | 加插件预取 |
| 本地能编 CI 不能 | 本地仓库残留 | 用干净环境验证 |
| 换版本后必失败 | 离线包未更新 | 版本与包同步发布 |
第四项的"验证方式"是"用干净容器跑一次离线构建":docker run -v $PWD:/src -w /src maven:3.9 mvn -o package——本地残留不会带进去。
第四项很常见:本地 ~/.m2 里"历史上取过的"依赖让"离线构建碰巧成功"——而在"干净机器"上就失败。
最后一个"体积"视角:离线包会随依赖数线性增长,于是"要不要打全量"是个成本问题。
// 三种"离线包"范围的取舍
全量(含测试/插件):最稳,体积最大
仅 runtime:体积小,但"测试跑不了"
仅核心依赖:最省,但"新依赖要手工导入"
// 判据:离线环境里的"构建任务"包含哪些(编译/测试/打包)
7. 边界:什么情况不必操心离线
| 场景 | 为什么 | 做法 |
|---|---|---|
| CI 能访问公网 | 无离线需求 | 不操心 |
| 私服可回源且稳定 | 首次失败可接受 | 只配 mirror |
| 依赖极少(< 20) | 失败易定位 | 不操心 |
| 用"构建镜像"(依赖预装) | 镜像即离线包 | 跟随镜像 |
最后一条与"团队"有关的边界:"离线包"是"团队共享资产",应当有"谁维护、多久重建"的约定——否则它会"半年没更新",而"新依赖"永远进不来。
判断口径一句话:"构建机器能否在"需要新依赖时"访问到来源"。能(公网或可回源私服),只需"镜像/代理";不能(隔离网),就必须"锁文件 + 离线包 + 插件预取"三件齐。
8. 自测
- 三种"离线"程度分别是什么机制?为什么"私服代理"在"新依赖"上仍会失败?
- "卡在下载中"为什么比"404"更费时间?怎么让它"立刻报错"?
- 为什么"锁定"是"离线"的前提?没有锁文件"离线包"会怎样?
- 预取要覆盖哪四类?为什么"插件"最容易被忽略?
- "本地能编、CI 不能"为什么常见?它说明什么验证缺失?
- 判断"是否需要操心离线"的那个问题是什么?