一个线上 Maven 诡异问题排查过程

å. 前言

如今的大部分 Java 应用基本都是经过 Maven 进行组织的,不管是分布式应用仍是单体集群应用每每都会经过一个 父 POM 加若干子 POM 完成项目的组织。然而这种多应用多模块的拆分就带来了一个巨大的体力成本 --- 发包git

举个例子,说明下为何会出现这种状况:程序员

image.png

上面这个图中有两个应用 portal 和 dump,其中 portal 的四个包是须要对外引用的也就是说 client 、domain、common、log 这几个包是两个应用共享的二方包。而共享不可避免的会带来竞争!redis

简单分析会有以下的问题:数据库

  1. 多应用发布**:**dump 中须要在 domain 中添加一些类和方法势必会致使 portal 应用跟着发布一次,将代码合并到基线markdown

  2. **版本错乱:**多分支开发时你们使用的 snapshot 版本号不一致,不是在处理冲突就是在处理冲突的路上app

  3. **上线换包:**应用发布前须要将全部代码切成同一个正式版,在将代码中全部引用版本的地方一一替换dom

ß. Maven 依赖机制(Dependency Mechanism)

为了解决上面遇到的种种问题,怎么作才能让这种频繁的 发包替换版本解决冲突 的流程更加简便自动化呢?简单来说个人思路是 集中式版本控制!是否是听着很耳熟,和大名鼎鼎的 git 的思路恰好相反,接下来就一块儿来看如何让流程优雅起来以及踩到 Maven 的一些大坑后又是如何一步步爬起来的。maven

在此以前咱们先看看 Maven 项目究竟是如何对模块和包进行组织的。分布式

image.png

首先建立一个 Maven 项目,而后在经过上图的三步你就能完成一个新模块的建立。ide

image.png

结果你会获得如上图所示的一个父 POM 和两个子 POM。

2.1 父子 POM

父 POM 核心内容以下:
image.png

分为两个部分,一个部分是父 POM 的声明,包含 GAV 坐标,打包方式必须为 POM,由于须要使用聚合模型,另一部分就是父工程管理的子模块 modules 标签。

子 POM 相对要更简单:

image.png

声明本身的父模块是谁,以及本身的 GAV 坐标,可能细心的你发现了这里他并无写 GroupId 和 Version 这是由于父工程已经声明了,若是没有特别的版本号和 groupId 的要求直接继承父工程的内容。

2.2 依赖传递

Maven 支持经过父 POM 中的依赖继承的方式避免开咱们手动指定依赖库的版本。可是传递依赖会致使依赖图迅速增加的特别大,因此 Maven 对于传递依赖有必定的限制:

  • 当依赖了多个版本的组件时 Maven 只会选择其中一个版本做为依赖,而选择的策略称为:nearest definition 最短路径

  • 依赖自动引入: 当 A 依赖了 B 而 C 依赖了 A 那么 C 组件会自动引入 B 组件

  • 依赖排除:这个理解起来就很简单 ,若是不想引入自动引入的一些依赖能够经过 ,排除依赖的手段将其去掉

2.3 依赖范围

依赖项的范围决定了何时这些依赖会被加载进去,在 Jar 瘦身等操做的时候特别有用,同时解决依赖冲突也是一把好手

  • compile 这个是默认值,也就是没有写做用域的依赖项在编译和运行阶段都会被加载到类路径

  • provided 这个和 compile 很是相似只是他仅在编译和测试阶段被加载,运行时不会。例如咱们经常使用的 Servlet API 这个 jar 仅仅是在编译测试须要,运行时 Tomcat 早已为咱们准备好了这个 Jar ,若是加了反而会可能致使类冲突

  • runtime 此范围表示编译时不须要依赖项,可是执行时须要依赖项,例如数据库的驱动

  • test 这个基本都是一些跑单测会依赖的 Jar

  • system 从参与度来讲,和 provided 相同,不过被依赖项不会从maven仓库抓,而是从本地文件系统拿,必定须要配合systemPath属性使用。

当前项目为 A,A依赖于B,B依赖于C。知道B在A项目中的scope,那么怎么知道C在A中的scope呢?这个就须要根据 nexus 的一张表来肯定:

image.png

好比 A 依赖 B 的范围为 provided ,B 依赖 C 的范围为 runtime 的 最终 A 依赖 C 的范围为 provided

ç. 大坑

在回到咱们一开始提出的问题,若是团队里三我的开发同一个应用,你们都须要修改二方包的版本号,分支合并必定会冲突。同时引用这个二方包的应用也必定会冲突,由于你们使用的版本号通常都不一样,那么以谁的为准?谁来解决这个冲突?每每由于版本号的问题致使冲突合并半小时应用都不必定能够构建的起来。

同时在发布上线的时候要改包为正式包,须要替换不少个地方,你们的版本还须要一致,每每须要解决多个地方的版本冲突。

为了解决这个问题,我采用了以下的方案:

  1. 你们在同一个环境开发的时候版本号永远都保持统一,好比在预发你的包版本只能是 pre0-snapshot 不然分支提交不上去

  2. 全部的包版本都收束到主 POM 中,禁止单独在每一个 POM 中单独声明要发布或依赖的二方包

改造先后,主 POM样子以下:

image.pngimage.png

子 POM 中就不在单独声明版本号了 而是直接继承父 POM 中定义的版本号:

image.png

这样确实很好的解决了上面的两个问题,可是在某次部署过程当中遇到了一个很是诡异的问题。

咱们项目结构以下:

ProjA
|  -- Apache Commons 3.0
|________
|        Proj B's Client
|        | -- mq-client
|        | -- redis-client
|        | -- etc.
|
|________
         Server
         | -- Server Libraries
         | -- etc.
复制代码

A 工程引用了 B 工程的 client 包,而其 client 包中引入了 mq 和 redis 的客户端,所以 A 工程在不用引入这两个包的状况下能够直接使用这两个包中的类。可是在某次部署的过程当中,A 工程怎么都找不到 mq 和 redis 的类文件,这就让人摸不着头脑了,线上都是能够的,为什么预发就有这个问题了???

∂. 溯源

又到了紧张而又刺激的问题排查阶段了。从 mvn 仓库上下载了最新的编译后的包放到 jad 中发现代码都是和个人分支保持一致的,没有啥问题,并且看到 snapshot 包后面的时间戳也是我发布包的时间戳。

那也就是发包的过程和结果都没啥问题,确定是拉包的时候出问题了呗,看看拉包的过程是否有异常。

mvn clean && mvn install -fn 
复制代码

一套命令跑下来,好像也没有 error,可是包就是拉不下来。看看日志里面有什么猫腻吧!一顿日志的搜查发现了一行 waring 日志:应用引入的依赖包无效,依赖包中传递依赖项不可用,能够经过开启debug获取更多信息。

[WARNING] the POM for A is invalid, transitive dependencies (if any) will not be available, enable debug logging for more details...
复制代码

开启maven debug功能后,警告后紧跟了一条错误信息,以下。

[WARNING] The POM forxx:jar:1.0-SNAPSHOT is invalid, transitive dependencies (if any) will not be available: 2 problems were encountered while building the effective model for xx:1.0-SNAPSHOT
[ERROR] 'dependencies.dependency.version' for xx:jar is missing.
[ERROR] 'dependencies.dependency.version' for xx:jar is missing.
复制代码

transitive dependencies 这玩意不就是依赖传递么,我已开始还不知道遇到的这个问题如何用文字向搜索引擎描述,如今显然就是传递依赖的一些包没有被引入啊,这不就找到问题所在了, 由于下面有两个包没有声明 jar 的包版本。

可是为什么会出现这个问题呢?根据上述报错的关键字我在 stackoverflow 中找到了答案:

One reason for this is when you rely on a project for which the parent pom is outdated. This often happens if you are updating the parent pom without installing/deploying it.

To see if this is the case, just run with mvn dependency:tree -X and search for the exact error. It will mention it misses things you know are in the parent pom, not in the artifact you depend on (e.g. a jar version).The fix is pretty simple: install the parent pom using mvn install -N and re-try

上面短短几句话即说明了缘由也给出了解决方案,美利坚的程序员果真牛皮!描述的大体意思就是由于这个二方包的父 POM 用的是老版本里面没有包含一些传递依赖的 jar 包的版本致使不少包拉不下来。解决方案也很简单直接把父 POM 中的依赖版本号加上并从新打包发布下就行了。

回顾上面说的组件的传递依赖,这里的二方包中依赖的 redis 和 mq 的 client 包没有拉下来是由于二方包 POM 中的某个 jar 的版本号即没有在父 POM 中定义也没有在二方 POM 中定义。二方包在找组件的依赖的时候首先会在本 POM 找,若是没有找到就会根据

<parent>
        <artifactId>module-test</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
复制代码

声明的父 POM 的版本号去父 POM 中找,由于父 POM 用的老版本里面根本没有那个包的版本号因此就报了刚才那个错误。

因此若是要发布新的二方包并且想要使用传递依赖的特性的话必定要从新发布父 POM !

相关文章
相关标签/搜索