å. 前言

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

举个例子,说明下为什么会出现这种情况:

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

简单分析会有如下的问题:

    1. 多应用发布dump 中需要在 domain 中添加一些类和方法势必会导致 portal 应用跟着发布一次,将代码合并到基线
    2. 版本错乱:多分支开发时大家使用的 snapshot 版本号不一致,不是在处理冲突就是在处理冲突的路上
    3. 上线换包:应用发布前需要将所有代码切成同一个正式版,在将代码中所有引用版本的地方一一替换

ß. Maven 依赖机制(Dependency Mechanism)

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

在此之前我们先看看 Maven 项目到底是如何对模块和包进行组织的。

首先创建一个 Maven 项目,然后在通过上图的三步你就能完成一个新模块的创建。

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

2.1 父子 POM

父 POM 核心内容如下:

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

子 POM 相对要更简单:

声明自己的父模块是谁,以及自己的 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 的一张表来确定:

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

ç. 大坑

在回到我们一开始提出的问题,如果团队里三个人开发同一个应用,大家都需要修改二方包的版本号,分支合并一定会冲突。同时引用这个二方包的应用也一定会冲突,因为大家使用的版本号一般都不同,那么以谁的为准?谁来解决这个冲突?往往因为版本号的问题导致冲突合并半小时应用都不一定可以构建的起来。

同时在发布上线的时候要改包为正式包,需要替换很多个地方,大家的版本还需要一致,往往需要解决多个地方的版本冲突。

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

    1. 大家在同一个环境开发的时候版本号永远都保持统一,比如在预发你的包版本只能是 pre0-snapshot  否则分支提交不上去
    2. 所有的包版本都收束到主 POM 中,禁止单独在每个 POM 中单独声明要发布或依赖的二方包

改造前后,主 POM样子如下:

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

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

我们项目结构如下:

  1. ProjA
  2. | -- Apache Commons 3.0
  3. |________
  4. | Proj B's Client
  5. | | -- mq-client
  6. | | -- redis-client
  7. | | -- etc.
  8. |
  9. |________
  10. Server
  11. | -- Server Libraries
  12. | -- etc.

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

∂. 溯源

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

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

  1. mvn clean && mvn install -fn

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

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

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

  1. [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
  2. [ERROR] 'dependencies.dependency.version' for xx:jar is missing.
  3. [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 找,如果没有找到就会根据

  1. <parent>
  2. <artifactId>module-test</artifactId>
  3. <groupId>org.example</groupId>
  4. <version>1.0-SNAPSHOT</version>
  5. </parent>

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

所以如果要发布新的二方包而且想要使用传递依赖的特性的话一定要重新发布父 POM !!!

一个线上 Maven 诡异问题排查过程的更多相关文章

  1. 01 . Go之Gin+Vue开发一个线上外卖应用

    项目介绍 我们将开始使用Gin框架开发一个api项目,我们起名为:云餐厅.如同饿了么,美团外卖等生活服务类应用一样,云餐厅是一个线上的外卖应用,应用的用户可以在线浏览商家,商品并下单. 该项目分为客户 ...

  2. 一个线上JVM的CPU资源占用过高问题的排查

    原文:https://www.iteye.com/blog/tyrion-2293369 上午线上某应用的一台JVM的CPU占比突然飙高到192%,并且一直下不来,导致监控一直告警,好久没处理这种问题 ...

  3. 关于GC(上):Apache的POI组件导致线上频繁FullGC问题排查及处理全过程

    某线上应用在进行查询结果导出Excel时,大概率出现持续的FullGC.解决这个问题时,记录了一下整个的流程,也可以作为一般性的FullGC问题排查指导. 1. 生成dump文件 为了定位FullGC ...

  4. 数据库char varchar nchar nvarchar,编码Unicode,UTF8,GBK等,Sql语句中文前为什么加N(一次线上数据存储乱码排查)

    背景 公司有一个数据处理线,上面的数据经过不同环境处理,然后上线到正式库.其中一个环节需要将数据进行处理然后导入到另外一个库(Sql Server).这个处理的程序是老大用python写的,处理完后进 ...

  5. 记一次线上gc调优的过程

           近期公司运营同学经常表示线上我们一个后台管理系统运行特别慢,而且经常出现504超时的情况.对于这种情况我们本能的认为可能是代码有性能问题,可能有死循环或者是数据库调用次数过多导致接口运行 ...

  6. 一个线上问题的思考:Eureka注册中心集群如何实现客户端请求负载及故障转移?

    前言 先抛一个问题给我聪明的读者,如果你们使用微服务SpringCloud-Netflix进行业务开发,那么线上注册中心肯定也是用了集群部署,问题来了: 你了解Eureka注册中心集群如何实现客户端请 ...

  7. BitArray虽好,但请不要滥用,又一次线上内存暴增排查

    一:背景 1. 讲故事 前天写了一篇大内存排查在园子里挺火,这是做自媒体最开心的事拉,干脆再来一篇满足大家胃口,上个月我写了一篇博客提到过使用bitmap对原来的List<CustomerID& ...

  8. JVM线上故障初步简易排查

    线上故障主要包括cpu 磁盘 内存 网络等问题 依次排查 1.cpu 1) 先用ps找到进程pid 2) top -H -p pid 找到cpu占用高的线程 3)printf '%x\n' pid 获 ...

  9. maven(二):创建一个可用的maven项目,完整过程

    环境:eclipse4.5 (内置maven插件) 创建maven项目 文件菜单--新建--其他-- maven project 下一步 选择web 结构 group id:  指项目在maven本地 ...

随机推荐

  1. win10 下安卓源码同步小技巧

    win10下,通过 清华镜像源 AOSP 可以快速拿到 100G 的 .repo  备份 然后 用 repo sync 就可以得到 安卓源码,爽不爽! 下载到win10 e盘下,用powershell ...

  2. Flink去重统计-基于自定义布隆过滤器

    一.背景说明 在Flink中对流数据进行去重计算是常有操作,如流量域对独立访客之类的统计,去重思路一般有三个: 基于Hashset来实现去重 数据存在内存,容量小,服务重启会丢失. 使用状态编程Val ...

  3. 【开源技术分享】无需流媒体服务,让浏览器直接播放rtsp/rtmp的神器:EasyMedia

    不同于市面上其他需要各种转发到流媒体服务的中间件来说,EasyMedia不需要依赖任何nginx-rtmp,srs,zlmediakit等等第三方流媒体服务,只需要你有rtsp或者rtmp等等协议的视 ...

  4. [时间模块、random模块]

    [时间模块.random模块] time模块 在Python中,通常有这几种方式来表示时间: 时间戳(timestamp):通常来说,时间戳表示的是从1970年1月1日00:00:00开始按秒计算的偏 ...

  5. mysql 使用 source/mysqldump 命令导入/导出文件信息

    要导入/导出数据库信息,使用 mysql 的source命令可以方便快速的处理 以MAC为例: 一.mysqldump命令导出SQL文件 /usr/local/mysql/bin/mysqldump ...

  6. SecureCRT自动保存日志设置

    SecureCRT自动保存日志设置原创杭州_燕十三 最后发布于2017-03-26 22:00:08 阅读数 24731 收藏展开 嵌入式开发经常由于无法debug而只能使用串口打印日志的方式调试代码 ...

  7. 串口配合DMA接收不定长数据(空闲中断+DMA接收)-(转载)

    1.空闲中断和别的接收完成(一个字节)中断,发送完成(发送寄存器控)中断的一样是串口中断: 2.空闲中断是接收到一个数据以后,接收停顿超过一字节时间  认为桢收完,总线空闲中断是在检测到在接收数据后, ...

  8. 关于typedef的用法总结-(转自Bigcoder)

    不管实在C还是C++代码中,typedef这个词都不少见,当然出现频率较高的还是在C代码中.typedef与#define有些相似,但更多的是不同,特别是在一些复杂的用法上,就完全不同了,看了网上一些 ...

  9. 10.5 arp:管理系统的arp缓存

    arp命令 用于操作本机的arp缓存区,它可以显示arp缓存区中的所有条目.删除指定的条目或者添加静态的IP地址与MAC地址的对应关系.     什么是arp?即地址解析协议(ARP,Address ...

  10. 10.16-17 mailq&mail:显示邮件传输队列&发送邮件

    mailq命令 是mail queue(邮件队列)的缩写,它会显示待发送的邮件队列,显示的条目包括邮件队列ID.邮件大小.加入队列时间.邮件发送者和接受者.如果邮件进行最后一次尝试后还没有将邮件投递出 ...