拓冰建站拓冰建站
首页 / 资讯中心 / 正文

mvn install 原理详解:从构建生命周期到本地仓库的完整链路

我在团队里带新人时经常看到这样的场景项目连不上别人的模块大家第一反应就是跑到某个子模块里敲一句mvn install敲完发现还是不行就又加clean再不行就删~/.m2/repository里的目录重来一遍。至于install到底触发了哪些动作、本地仓库里的文件为什么长那样、为什么明明install了别的项目却还在用旧代码很少有人能一口气讲清楚。这篇文章就用我这些年实际踩坑的经验把maven install这条命令从执行到入库整个流程拆开给你看。适合刚把 Maven 用熟但还没系统理解构建链路的开发者也适合被“多模块依赖”反复折磨、想搞明白底层逻辑的团队新人。搞懂了它你会发现自己对日常构建、CI 发布、本地私有依赖哪一环出了问题心里会完全有数。1. mvn install 在构建生命周期中的真实位置1.1 三条生命周期最常走的是 defaultMaven 不是只有一套流程它内部定义了 clean、default、site 三套生命周期lifecycle。我们平时用的mvn install走的是 default 这条主线。这三套各自独立但通过命令组合可以串联执行。比如mvn clean install是先走完 clean 生命周期的clean阶段把 target 目录清掉再走 default 生命周期到install阶段结束。site 生命周期主要用于生成项目站点报告日常开发用得少但也别忘了他存在。关键是理解 default 生命周期它不是一个命令而是一串按顺序排列的阶段phase。Maven 执行某个阶段时它不会只跑那一小步而是从生命周期最开头一路跑到你指定的那个阶段为止。所以你执行mvn install不是“只执行 install 这一个动作”而是“把 default 生命周期里所有排在 install 之前的阶段全部执行完然后再执行 install 阶段”。这就像一个流水线你按下“完成”按钮不代表只做最后一个盖章动作而是从原材料入场开始加工、质检、包装、入库全流程一起跑完。1.2 从 validate 到 install整条流水线的执行顺序default 生命周期里的阶段有很多完整列表长得能吓到新人我这里挑主干讲一遍validate、compile、test、package、verify、install、deploy 这几个是我们肉眼最常见的。但在它们之间还藏着十几二十个自动执行的阶段例如 process-resources、process-classes、test-compile、prepare-package 等等。如果展开说mvn install实际执行的顺序大致是validate校验项目信息是否完整POM 配置是否合法。compile编译主代码把src/main/java的 .class 文件生成到 target/classes。test这里又分好几步会先编译src/test/java下的测试代码再由 maven-surefire-plugin 去跑所有单元测试。package根据 POM 里的 packaging 类型把项目打成 jar、war、pom 或其他格式输出到 target 目录。verify执行集成测试等额外检查比如用 maven-failsafe-plugin 跑集成测试。install到这一步才轮到真正的主角把构建产物复制进本地仓库。deploy你如果执行的不是 install 而是 deploy它会继续往远程仓库推。这里有个很反常识的点maven 执行mvn test时也会执行前面的 compile 等阶段执行mvn package时也会先跑完所有测试。也就是说阶段不是“你可以挑选执行的单个任务”而是“整个阶梯上你选在哪一层停”。新人很容易踩的第一个坑就在这里我明明只想打个包为什么测试跑了半天因为它默认就会跑完 test 之前的全部阶段。理解了生命周期你就知道这不是 Maven 在无理取闹而是它的机制设计就是如此。1.3 看一次 mvn install 的真实日志与其背理论不如直接看一次真实的执行尾部输出。当一次mvn clean install跑完后你会看到类似下面这样的内容[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) demo --- [INFO] --- maven-compiler-plugin:3.10.1:compile (default-compile) demo --- [INFO] --- maven-surefire-plugin:2.22.2:test (default-test) demo --- [INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] --- maven-jar-plugin:3.3.0:jar (default-jar) demo --- [INFO] Building jar: /Users/xxx/code/demo/target/demo-1.0.0.jar [INFO] [INFO] --- maven-install-plugin:3.0.0-M1:install (default-cli) demo --- [INFO] Installing /Users/xxx/code/demo/target/demo-1.0.0.jar to /Users/xxx/.m2/repository/com/example/demo/1.0.0/demo-1.0.0.jar [INFO] Installing /Users/xxx/code/demo/pom.xml to /Users/xxx/.m2/repository/com/example/demo/1.0.0/demo-1.0.0.pom注意最后这两行这才是install阶段真正的成果一个是 jar 文件一个是 pom 文件都被复制到了~/.m2/repository下对应的目录里。看到这两行Installing ... to ...基本可以确定这次 install 成功了。有人被“无头构建”折腾过遇到 Maven 卡住没输出大概率是下载依赖网络慢而如果最后没有出现Installing ... to ...那就要先看看前面是不是有测试失败或编译报错中断了流水线。日志里这两行是你判断 install 是否生效的第一依据。2. install 阶段的幕后流程与本地仓库之间的那点事2.1 坐标决定归宿本地仓库目录是怎么算出来的Maven 里每个依赖都有唯一坐标groupId、artifactId、version再加上 packaging 类型。这个坐标不光用来找依赖还直接决定文件在本地仓库的物理路径。举个例子一个项目的坐标是groupIdcom.example.demoartifactIdhello-worldversion0.0.1-SNAPSHOTpackagingjar那么它 install 过后本地仓库里的路径就是~/.m2/repository/com/example/demo/hello-world/0.0.1-SNAPSHOT/这个路径的生成规则其实很简单把 groupId 的每个.换成一层目录后面依次拼上 artifactId 和 version变成groupId路径/artifactId/version/。然后把 artifactId 和 version 拼成文件名形成hello-world-0.0.1-SNAPSHOT.jar和hello-world-0.0.1-SNAPSHOT.pom。很多人第一次打开~/.m2/repository会觉得目录嵌套深得离谱其实就是这个规则导致的。你看到一个 jar 的完整路径时基本能倒推出它的坐标反过来也成立。记住这个映射关系特别实用。比如你怀疑某个依赖本地没装好可以直接跑到对应目录看一眼文件到底在不在、文件大小是不是 0 字节、pom 有没有损坏。2.2 maven-install-plugin 在 POM 里看不到却一直在干活实际执行 install 阶段的是 maven-install-plugin 的 install 目标。你大概率不会在自己的 pom 里显式声明这个插件因为 Maven 在超级 POM每个项目都隐式继承的父 POM中已经帮你把插件和目标绑定到了相应阶段。那么 install 目标到底做了哪些事我把它拆开看第一它会把项目的主构建产物复制到本地仓库。对普通 jar 项目来说就是把你package阶段打出来的 jar 复制过去。如果你的 packaging 是 war就把 war 复制过去如果 packaging 是 pom比如父工程模块那它只复制 pom 文件不会硬找一个 jar 去复制。第二复制的同时会把 POM 文件一起放进去。这一点常常被忽略其实其他项目依赖你的模块时不仅需要 jar 里的类还需要读取 POM 里声明的传递依赖。所以本地仓库里那一个.pom文件同样非常重要缺了它下游项目解析你的依赖时会直接报错。第三它会对快照版本额外做处理。如果 version 是0.0.1-SNAPSHOTinstall 插件除了复制 jar 和 pom还会维护该版本目录下的maven-metadata-local.xml。这个文件记录的是本地这个快照的时间戳信息虽然没有远程仓库那么复杂但在依赖解析判断“这个快照是不是最新的”时它会起作用。第四如果你的项目配置了生成 sources 包或 javadoc 包并且这些附属构件已经在构建过程中生成install 插件会把它们作为附加构件一并复制到本地仓库。不带 classifier 的主 jar 会被下游默认依赖带sources或javadocclassifier 的附属 jar 会在 IDE 里自动关联方便看源码。最后install 还会维护一个叫_remote.repositories的文件用来标记每个构件是从哪个远程仓库下载的还是本地安装的。这个文件平时没人注意但偶尔会成为疑难杂症的源头。后文排障部分我会专门讲。2.3 package、install、deploy三者就像打包、入库、上架很多初学者搞不清这三个词的区别我用仓库的类比来解释mvn package把货物打包好放在“工位旁边的暂存区”target 目录。这一步不进入本地仓库也不影响任何其他项目。mvn install把打包好的货物搬到“自己家里的仓库”本地仓库 ~/.m2/repository以后你自己家其他的项目要用可以随时取。mvn deploy把货物上架到“对外营业的总仓库”远程私有仓库/中央仓库其他同事或 CI 服务器才能从这里拉取。所以三者的核心差别就在“产物最终到了哪里”。如果你的项目只是自己本地跑mvn install就够了如果你希望团队里其他人也能用这个公共模块就必须mvn deploy到远程仓库。这里有个容易忽略的细节执行mvn deploy时因为生命周期是顺序执行的它会先走到 install 阶段把构件安装到本地仓库然后才会执行 deploy 阶段。所以很多人以为deploy只会上传远程仓库实际上它也会先改一遍本地仓库。如果你希望跳过本地的安装动作早期的做法是给 maven-install-plugin 配一个 skip 参数不过日常我们很少这么干。还有一点要注意本地仓库优先于远程仓库。哪怕你已经成功 deploy 到了远程服务器只要本机~/.m2/repository里还留着旧版本的 jar你本地构建时默认还是会用旧 jar。这解释了为什么很多人抱怨“部署了新版本本地跑起来还是旧行为”——检查自己的本地仓库往往比检查远端更快找到问题根源。3. 多模块项目里 install 的实操套路3.1 为什么子模块之间“连不上”时敲 install 最管用多模块项目是 Maven 使用频率最高的场景。一个典型的微服务或分层工程通常有一个父 POM 和一些子模块比如my-project/ ├── pom.xml (packagingpom) ├── common/ ├── service-api/ └── web/假设service-api依赖commonweb又依赖service-api。你在根目录直接跑mvn clean install时Maven 会构建一个 reactor把所有模块都纳入同一个构建组然后根据模块间的依赖关系自动排序保证被依赖的模块先构建。这里有一个关键知识在同一个 reactor 中如果web依赖service-apiMaven 访问的其实未必是本地仓库里的 jar而可能是service-api模块的target/classes目录。也就是说只要它们是同一个 reactor 一起构建的子模块之间可以暂时不依赖本地仓库。既然如此那为什么很多团队还是建议“多模块项目先 install 公共模块”因为一旦你脱离 reactor单独构建其中某个模块比如你在web目录里直接跑mvn spring-boot:run或mvn packageMaven 就不再认识common和service-api的源码只能从本地仓库找它们的 jar。如果这些模块之前没有 install 到本地仓库或者安装的还是旧版本你立刻就会遇到“类找不到”或“方法不存在”的报错。所以多模块场景下的最优习惯是公共模块有改动后先在根目录或公共模块目录执行一次mvn clean install把最新产物同步到本地仓库再去跑依赖它的业务模块。3.2 官方组合拳-pl 与 -am有时候全量构建太重尤其子模块很多、测试又慢的时候你只改了底层的一个公共模块结果要等整个工程全量打包一遍。这时候 Maven 提供了一组特别好用的参数。先看一个小型父工程结构parent ├── pom.xml ├── base-common └── user-service如果你只改了base-common只想重新构建并装好它运行mvn clean install -pl base-common-pl是--projects的缩写后面可以跟模块名也可以跟相对路径。这样 Maven 只会去构建列出的模块而不是把所有模块都跑一遍。但实际工作中有个更常见的需求项目里存在多级依赖你想构建user-service但又怕它依赖的base-common还没更新于是希望把依赖它的模块一起带上。这时候要用-am也就是--also-makemvn clean install -pl user-service -am上面这行的意思是构建user-service同时把它依赖到的其他模块比如base-common也一起构建并 install。组合起来最常用的命令长这样mvn clean install -pl user-service -am -DskipTests这个命令在我的日常开发里使用频率极高。在本地开发一个相对独立的业务服务时根本不需要每次全量 build 几十个模块找到你本次依赖的最上游模块用-pl加上-am控制范围既能保证产物最新又能节约大量时间。3.3 跳过测试的各种姿势与代价install默认是会跑测试的因为在生命周期里test阶段排在install前面。如果一个模块的单元测试写得比较慢或者某些测试依赖外部环境构建很容易卡在这里。两条主流跳过测试的参数经常被混用实际上差别不小mvn install -DskipTests表示“测试代码照常编译只是不执行”。好处是编译环节仍然会暴露测试代码里的语法问题不会让坏代码一路混过去。这对保证代码质量更友好。mvn install -Dmaven.test.skiptrue表示“测试代码直接不编译也不执行”。这个参数会让 maven-compiler-plugin 连testCompile都跳过速度最快但如果测试代码里有编译错误你完全发现不了等到 CI 或别人跑测试时才会爆出问题。我个人的习惯是本地做快速验证时用-DskipTests因为它保留了编译检查只有在某些测试依赖外部中间件、本地根本没有环境时才用-Dmaven.test.skiptrue。如果两边参数被混用弄乱了可以在命令行加-X查看实际执行了哪些插件目标排查起来看得一清二楚。4. 那些 install 之后依然找不到依赖的破事4.1 老毛病改了代码却忘了重新 install这是多模块项目里出现频率最高的问题没有之一。你改了一个公共模块比如base-common里的一个工具类方法然后在user-service里调用新方法编译时却报错找不到方法。第一反应往往是“代码是不是没保存”“IDE 是不是缓存坏了”但真正的答案通常是本地仓库里那个base-common-1.0.0-SNAPSHOT.jar还是旧的源代码没重新 install 过。为什么会这样前面说过单独构建下游项目时Maven 依赖解析是先找本地仓库找不到才去远程仓库。它会优先使用本地仓库里的同名同版本 jar而这个 jar 不会因为你改了源码就自动更新必须重新执行mvn install才会被新的 jar 替换。解决办法看起来简单但实际工程里有个很容易踩到的小分支当你改了公共模块代码后很多人在 IDEA 里只点模块目录的 install没跑到根目录全量构建。如果这个模块本身还依赖了其他兄弟模块的未发布改动只 install 单模块依然会失败或装了残缺版本。我的经验是在父工程目录统一执行mvn clean install -pl base-common -am把该模块以及它依赖的兄弟模块一起装进本地仓库然后再去下游模块构建。4.2 依赖为什么不走中央仓库偏偏先看本地另一个困扰大家的问题是本地仓库里的 SNAPSHOT 依赖没更新但明明远程比如公司私服已经有新版本了为什么本地项目拉不到这背后是 Maven 的依赖解析顺序本地仓库优先。Maven 在解析坐标时会首先看本地仓库有没有这个 groupId、artifactId、version 对应的目录和文件如果存在通常就直接用了只有当本地不存在或者本地遇到了构建失败、文件损坏需要重试时才会去远程仓库下载。对 SNAPSHOT 版本来说Maven 可以配置更新策略去检查远程仓库的快照有没有更新但默认更新频率很低不是每次都去找远程比对。所以当你要强制拉取远程仓库中一个比较新的 SNAPSHOT 版本时最常见的命令是加-U参数强制更新mvn clean install -U-U会强制 Maven 检查所有 SNAPSHOT 依赖在远程仓库是否有更新把本地可以更新的快照强制覆盖一遍。如果你怀疑某个快照依赖还停留在老版本用这个参数基本能解决。如果你不想每次输入这个参数也可以在 pom 或 settings 里给仓库配updatePolicy例如always但那会让每次构建都去远程仓库检查一遍快照速度会明显变慢一般不建议。做开发时遇到快照问题时临时用-U才是高效的方案。4.3 手动把三方 jar 塞进本地仓库install-file除了自己项目的模块开发中还经常遇到一种情况需要用到某个第三方 jar但这个 jar 不在 Maven 中央仓库和公司私服里只有供应商发来的一个文件。你不能把它直接扔进项目 lib 目录就完事更规范的做法是把 jar 手工安装到本地仓库让 Maven 像管理普通依赖一样管理它。它的标准命令是mvn install:install-file \ -Dfile/path/to/your-custom.jar \ -DgroupIdcom.example \ -DartifactIdyour-custom \ -Dversion1.0.0 \ -Dpackagingjar执行成功后Maven 会把 jar 复制到~/.m2/repository/com/example/your-custom/1.0.0/your-custom-1.0.0.jar同时生成对应的 pom 文件。这样你在其他项目的 pom 里就能像普通依赖一样写坐标引用了。这里有几个实操细节要特别注意如果该 jar 还依赖了别的第三方库手工 install-file 不会自动把依赖关系写进 pom除非你额外用-DpomFilexxx.pom指定一个 pom 文件否则下游项目引用后可能会因为缺传递依赖而运行时报ClassNotFoundException。最好在供应商资料里找一找有没有对应的 pom没有的话只能自己写一个最小的 pom 传进去。另外install-file 默认会覆盖本地仓库里同坐标的文件。如果你需要插入多个不同版本要确保-Dversion也对应修改否则会把旧版本覆盖掉导致其他项目引用旧版本时也被悄悄换了包。4.4 常见问题速查表上面讲了很多场景我把这些年遇到的和 install 相关的典型问题整理成一个速查表方便你定位崩溃现场现象最可能的根因处理方式多模块项目里改了下游模块代码上游模块还是报旧方法下游模块没有重新 install 到本地仓库到对应模块执行mvn clean install或根目录用-pl 模块名 -am构建install 一直卡在下载依赖进度几乎不动远程仓库访问慢或镜像仓库配置问题settings.xml 中配置一些开源镜像仓库优先使用国内镜像mvn install执行到 test 阶段失败根本没生成 jar有单元测试失败生命周期中断先修测试或临时用-DskipTests/-Dmaven.test.skiptrue日志出现Installing ... to ...但另一个项目仍说找不到依赖依赖的 groupId、artifactId、version 写错或不是同一个本地仓库检查坐标是否和安装日志里的路径一致检查本地仓库位置本地仓库里一个依赖目录下有损坏的.lastUpdated文件下载中断导致 Maven 认为“最新检查过失败结果”删除对应目录里的.lastUpdated文件重新构建deploy成功但本地项目还是旧版本本地仓库优先同名同版本旧 jar 还在本地重新执行mvn clean install -U或删除本地同名目录里的旧 jar代码改了但 IDEA 里还是提示找不到新方法IDE 的依赖索引还是旧 jar重新 install 后在 IDEA 里右键项目 Maven - Reload Projectinstall 后 target 下有自定义名称的 jar但本地仓库文件名却是标准名finalName 只影响 target 输出仓库安装仍按坐标规范命名以本地仓库路径里的文件名为准别用 target 文件名去判断最后再分享一个和_remote.repositories相关的冷门坑。如果你把某个项目从一个旧机器拷贝到新机器或者切换了仓库镜像源构建时偶尔会出现“本地明明有 jar依赖却下载失败”的奇怪现象这经常是本地仓库里_remote.repositories记录的仓库 id 和当前配置不匹配导致的。遇到这种情况先把对应依赖目录下的_remote.repositories删掉再重新执行mvn install或mvn dependency:resolve大多数时候就能恢复。这个文件看着不起眼但它记录着依赖来源有时候却会成为本地仓库里最隐蔽的一根刺。我自己的习惯是对核心公共模块的改动永远先跑一次干净的mvn clean install并盯着日志确认出现那两行Installing ... to ...再切换到下游模块继续开发。Maven 的本地仓库就像是所有本地工程共同仰仗的“后厨”你不把最新菜品送进后厨前厅的顾客能点的永远只有旧菜单。把这条思路想通很多 install 相关的疑难杂症就都不难定位了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门