一个线上 Maven 诡异问题排查过程
å. 前言
现在的大部分 Java 应用基本都是通过 Maven 进行组织的,不论是分布式应用还是单体集群应用往往都会通过一个 父 POM 加若干子 POM 完成项目的组织。然而这种多应用多模块的拆分就带来了一个巨大的体力成本 --- 发包
举个例子,说明下为什么会出现这种情况:
上面这个图中有两个应用 portal 和 dump,其中 portal 的四个包是需要对外引用的也就是说 client 、domain、common、log 这几个包是两个应用共享的二方包。而共享不可避免的会带来竞争!
简单分析会有如下的问题:
- 多应用发布:dump 中需要在 domain 中添加一些类和方法势必会导致 portal 应用跟着发布一次,将代码合并到基线
- 版本错乱:多分支开发时大家使用的 snapshot 版本号不一致,不是在处理冲突就是在处理冲突的路上
- 上线换包:应用发布前需要将所有代码切成同一个正式版,在将代码中所有引用版本的地方一一替换
ß. 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
ç. 大坑
在回到我们一开始提出的问题,如果团队里三个人开发同一个应用,大家都需要修改二方包的版本号,分支合并一定会冲突。同时引用这个二方包的应用也一定会冲突,因为大家使用的版本号一般都不同,那么以谁的为准?谁来解决这个冲突?往往因为版本号的问题导致冲突合并半小时应用都不一定可以构建的起来。
同时在发布上线的时候要改包为正式包,需要替换很多个地方,大家的版本还需要一致,往往需要解决多个地方的版本冲突。
为了解决这个问题,我采用了如下的方案:
- 大家在同一个环境开发的时候版本号永远都保持统一,比如在预发你的包版本只能是
pre0-snapshot
否则分支提交不上去 - 所有的包版本都收束到主 POM 中,禁止单独在每个 POM 中单独声明要发布或依赖的二方包
改造前后,主 POM样子如下:
子 POM 中就不在单独声明版本号了 而是直接继承父 POM 中定义的版本号:
这样确实很好的解决了上面的两个问题,但是在某次部署过程中遇到了一个非常诡异的问题。
我们项目结构如下:
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 usingmvn 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 !!!
一个线上 Maven 诡异问题排查过程的更多相关文章
- 01 . Go之Gin+Vue开发一个线上外卖应用
项目介绍 我们将开始使用Gin框架开发一个api项目,我们起名为:云餐厅.如同饿了么,美团外卖等生活服务类应用一样,云餐厅是一个线上的外卖应用,应用的用户可以在线浏览商家,商品并下单. 该项目分为客户 ...
- 一个线上JVM的CPU资源占用过高问题的排查
原文:https://www.iteye.com/blog/tyrion-2293369 上午线上某应用的一台JVM的CPU占比突然飙高到192%,并且一直下不来,导致监控一直告警,好久没处理这种问题 ...
- 关于GC(上):Apache的POI组件导致线上频繁FullGC问题排查及处理全过程
某线上应用在进行查询结果导出Excel时,大概率出现持续的FullGC.解决这个问题时,记录了一下整个的流程,也可以作为一般性的FullGC问题排查指导. 1. 生成dump文件 为了定位FullGC ...
- 数据库char varchar nchar nvarchar,编码Unicode,UTF8,GBK等,Sql语句中文前为什么加N(一次线上数据存储乱码排查)
背景 公司有一个数据处理线,上面的数据经过不同环境处理,然后上线到正式库.其中一个环节需要将数据进行处理然后导入到另外一个库(Sql Server).这个处理的程序是老大用python写的,处理完后进 ...
- 记一次线上gc调优的过程
近期公司运营同学经常表示线上我们一个后台管理系统运行特别慢,而且经常出现504超时的情况.对于这种情况我们本能的认为可能是代码有性能问题,可能有死循环或者是数据库调用次数过多导致接口运行 ...
- 一个线上问题的思考:Eureka注册中心集群如何实现客户端请求负载及故障转移?
前言 先抛一个问题给我聪明的读者,如果你们使用微服务SpringCloud-Netflix进行业务开发,那么线上注册中心肯定也是用了集群部署,问题来了: 你了解Eureka注册中心集群如何实现客户端请 ...
- BitArray虽好,但请不要滥用,又一次线上内存暴增排查
一:背景 1. 讲故事 前天写了一篇大内存排查在园子里挺火,这是做自媒体最开心的事拉,干脆再来一篇满足大家胃口,上个月我写了一篇博客提到过使用bitmap对原来的List<CustomerID& ...
- JVM线上故障初步简易排查
线上故障主要包括cpu 磁盘 内存 网络等问题 依次排查 1.cpu 1) 先用ps找到进程pid 2) top -H -p pid 找到cpu占用高的线程 3)printf '%x\n' pid 获 ...
- maven(二):创建一个可用的maven项目,完整过程
环境:eclipse4.5 (内置maven插件) 创建maven项目 文件菜单--新建--其他-- maven project 下一步 选择web 结构 group id: 指项目在maven本地 ...
随机推荐
- 如何通过Zoho Books门户管理供应商
作为一个企业,不管规模大小,都有自己的供应商来为业务提供相关的服务和配件.随着采购的频率和供应商数量的增加,采购的管理和付款的跟踪难度就会增加,进而影响到企业和供应商之间的关系. 为了解决这个问题,Z ...
- 使用alpine为基础镜像Q&A
作为go应用存在二进制文件却不能执行 明明镜像中有对应的二进制文件,但是执行时却提示 not found 或 no such file 或 standard_init_linux.go:211: ex ...
- 3.1.5 LTP(Linux Test Project)学习(五)-LTP代码学习
3.1.5 LTP(Linux Test Project)学习(五)-LTP代码学习 Hello小崔 华为技术有限公司 Linux内核开发 2 人赞同了该文章 LTP代码学习方法主要介绍两个步骤, ...
- ltp
1.查找文件 find / -name 'filename' 1 2.查找目录 find / -name 'path' -type d 1 3.查找内容 # find .| xargs grep ...
- MyBaits 全局配置文件(mybatis-config.xml)
什么是 MyBatis 全局配置文件 MyBatis 全局配置文件包含影响 MyBatis 框架正常使用的功能设置和属性信息. 它的作用好比手机里的设置图标,点击这个图标就可以帮助我们查看手机的属性信 ...
- gpgj-12 ROIC估值法总结
1.公司甲乙丙 ·公司的分类: 1.ROIC < 10%的公司 不赚钱的公司,价值摧毁者,避免投资这种 2.ROIC 约等于 10%的公司 勉强能够糊口,EV ...
- .NET平台系列12 .NET未来之开源.NET Core
系列目录 [已更新最新开发文章,点击查看详细] 微软于2014年11月推出了.NET Core 1.0..NET Core的目标是从我们在过去12年中对.NET Framework的构建.交付 ...
- Linux C 文件IO
文件IO 2021-05-31 12:46:14 星期一 目录 文件IO 基础IO open 错误 creat read 一个例子 write close lseek 文件空洞 unlink删除 io ...
- C# DeepClone 深拷贝
常规利用反射进行克隆 public static T CloneModel<T>(T oModel) { var oRes = default(T); var oType = typeof ...
- 【SpringBoot基础系列】手把手实现国际化支持实例开发
[SpringBoot基础系列]手把手实现国际化支持实例开发 国际化的支持,对于app开发的小伙伴来说应该比价常见了:作为java后端的小伙伴,一般来讲接触国际化的机会不太多,毕竟业务开展到海外的企业 ...