å. 前言

现在的大部分 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 中定义的版本号:

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

我们项目结构如下:

 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 !!!

一个线上 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. Java容器 | 基于源码分析List集合体系

    一.容器之List集合 List集合体系应该是日常开发中最常用的API,而且通常是作为面试压轴问题(JVM.集合.并发),集合这块代码的整体设计也是融合很多编程思想,对于程序员来说具有很高的参考和借鉴 ...

  2. WTM Blazor,Blazor开发利器

    Blazor从诞生到现在也有一段时间了,之前一直在观望,从dotnet5中Blazor的进步以及即将到来的dotnet6中的规划来看,Blazor的前途还是光明的,所以WtmBlazor来了! Bla ...

  3. computed和watch的区别

    严格上来说,计算属性能够实现的效果,watch都可以实现.只是有时候watch写起来比较麻烦. 但是watch能够实现的效果computed不一定能够实现. 1:watch内部可以包含异步操作,com ...

  4. .Net core Worker Service 扩展库

    .Net core Worker Service 扩展库,目的为更易控制每一个worker 的运行. 提供根据配置文件对每一个Worker的停止.启动和自动解析注册Worker. 获取配置的方式不限于 ...

  5. 强哥jQuery学习笔记

    js对象: 1.js内置对象 2.js元素对象 3.jquery对象 js特效: 1.js元素对象 2.jQuery对象 jQuery学习: 1.核心函数 2.选择器 3.筛选 4.文档处理 5.属性 ...

  6. Linux_进程管理的基本概述

    一.进程的基本概述 1️⃣:进程是已启动的可执行程序的运行中实例 2️⃣:/proc目录下以数字为名的目录,每一个目录代表一个进程,保存着进程的属性信息 3️⃣:每一个进程的PID是唯一的,就算进程退 ...

  7. Docker的镜像及容器常用操作(2)

    一.docker镜像 镜像(docker image) --- Docker 运行容器之前需要本地存在镜像,若本能地不存在,那么 Docker 会找默认镜像仓库( Docker Hub 公共注册服务器 ...

  8. Java安全之Cas反序列化漏洞分析

    Java安全之Cas反序列化漏洞分析 0x00 前言 某次项目中遇到Cas,以前没接触过,借此机会学习一波. 0x01 Cas 简介 CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用 ...

  9. leetcode中Java关于Json处理的依赖

    leetcode的java代码提供的main函数中,往往有关于json的依赖...我找了许久才找到他们用的是这个json实现 <dependency> <groupId>com ...

  10. GO学习-(1) why go?

    为什么你应该学习Go语言? 终于等到你!Go语言--让你用写Python代码的开发效率编写C语言代码. 为什么互联网世界需要Go语言 世界上已经有太多太多的编程语言了,为什么又出来一个Go语言? 硬件 ...