背景

昨天帮一位同事排查了一个依赖冲突的问题。问题的现象就是在IntelliJ IDEA运行项目正常,但是打包(Maven assembly jar)之后传到服务器运行失败,报错:Caused by: java.lang.NoSuchFieldError: INSTANCE

后来定位到某个类存在多个版本,其中一个版本是没有INSTANCE的。进一步发现项目所依赖的其他module,都是以assembly jar的形式install到本地仓库的,最终通过修改pom文件,对所依赖的module重新install,使其安装到本地仓库的是原始的、不包含依赖的jar。至此,问题解决。

在排查的过程中,发现了一些有趣的现象,后来又自己研究了下,现把结果记录下来,以供分享。

两种分析依赖的方式

这里先介绍两种依赖分析的方式。

Maven支持打印当前项目的依赖树,命令是mvn dependency:tree。下面是一个demo:

[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ m-c ---
[INFO] com.heyikan.demo:m-c:jar:1.0-SNAPSHOT
[INFO] \- com.heyikan.demo:m-b:jar:1.0-SNAPSHOT:compile
[INFO] \- com.heyikan.demo:m-a:jar:1.0-SNAPSHOT:compile

从这里可以看到,m-c依赖于m-b,m-b依赖于m-a。

需要注意这个命令是基于仓库进行依赖解析的,也就是说如果要解析m-c项目的依赖,那么必须确保它所依赖的所有项目都可以在仓库中找到。即使m-c和m-b是同一个项目的不同module,如果m-b没有安装到本地仓库,这个命令也会失败。

另外,这个命令打印出来的是最终的依赖结果:对于同一个项目的多个版本只会打印被选择的那一个。

另一种方式是IntelliJ IDEA的功能,支持以图形的方式展示项目的依赖图谱。打开项目的pom文件,使用快捷键Ctrl+Shift+Alt+U打开当前的图谱:

在图谱页面使用Ctrl+F快捷键,可以搜索指定的依赖。

这个命令不要求所有的依赖都已安装在本地仓库,对于同一个项目的不同module之间的依赖,可以直接解析

而且同一个依赖的不同版本依赖都会在图谱中展示出来,其中被选择的那个是黑线相连,其他的是红线。选中其中一个,会出现被弃用的依赖版本到最终选择的版本的一条连线。

注意这两者的区别,它表示使用Maven命令处理Maven项目的方式,和使用IDEA工具直接处理Maven项目的方式是有差别的。这种差别一般都会很微妙,并且是造成开发环境运行正常,但是服务器上运行失败的可能原因。

Maven依赖调解机制

下面的内容参考自许晓斌的《Maven实战》。

因为Maven的传递依赖,很可能导致依赖的冲突,这种冲突的具体形式表现在同一个项目的不同版本都出现在项目的依赖图谱中。

针对这种情况,Maven有依赖调解的规则。

首先是路径最近者优先,举例来说,如果项目A存在这样的依赖关系:A -> B -> C -> X(1.0) 和 A -> D -> X(2.0)。项目X有两个版本的依赖出现,此时因为X(1.0)的依赖长度为3,X(2.0)的依赖长度为2,最终被采用的依赖时2.0版本的X。

当第一个规则无法区别同一个依赖项目的不同版本时,使用第二个规则:位置靠前的优先。比如A -> B -> Y(1.0)和A -> C -> Y(2.0),最终会选择Y(1.0)。

有趣的是,如果直接在项目中声明一个项目的两个版本的依赖,如A -> Z(1.0)和A -> Z(2.0),则最后的会覆盖前面的。

shade插件对依赖的影响

shade插件用于制造一个包含依赖的assembly jar包。

默认的情况下,shade插件的打包包名会占用Maven原生的打包包名,如果将插件打包目标绑定到生命周期的package阶段,那么install阶段安装到本地仓库的实际上是shade插件打出来的assembly jar。

而且,这个assembly jar的pom文件已经改变,pom文件中不包含任何的依赖,因为所有的依赖都已经在它里面了。

比如说,我创建一个项目demo-dependency,它有三个模块m-am-bm-c,依赖关系为m-c -> m-b -> m-a。其中m-b的pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>demo-dependency</artifactId>
<groupId>com.heyikan.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion> <artifactId>m-b</artifactId> <dependencies>
<dependency>
<groupId>com.heyikan.demo</groupId>
<artifactId>m-a</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies> <build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build> </project>

注意,它使用了shade插件,并将打包目标shade绑定到了package阶段。

执行mvn clean install之后,去本地仓库看下这个项目的pom文件,它变成了这个样子:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>demo-dependency</artifactId>
<groupId>com.heyikan.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>m-b</artifactId>
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

注意,已经没有依赖的内容了。

此时,使用mvn dependency:tree分析m-c的命令,结果如下:

--- maven-dependency-plugin:2.8:tree (default-cli) @ m-c ---
[INFO] com.heyikan.demo:m-c:jar:1.0-SNAPSHOT
[INFO] \- com.heyikan.demo:m-b:jar:1.0-SNAPSHOT:compile

试想一下,m-c项目依赖于m-b,而m-b是一个assembly jar,那么所有m-b依赖的项目最终都会被视做m-b本身。如果你想把m-c也打成一个assembly jar,如何处理m-b的依赖和其他依赖链上的冲突?恐怕无法得到什么保证。

shade插件的这种行为,实际上干扰了Maven的依赖调解机制。

要规避这个问题,最简单的方式是为shade插件的打包结果自定义名称,避免和Maven标准包名冲突:

<?xml version="1.0" encoding="UTF-8"?>
<project ...>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>${project.build.finalName}-assembly</finalName>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

注意,configuration -> finalName配置了自定义的jar包名称。

扩展阅读

  1. Java项目打包方式分析
  2. Maven核心知识

Maven依赖分析的更多相关文章

  1. Maven依赖版本冲突的分析及解决小结

    1:前言 做软件开发这几年遇到了许多的问题,也总结了一些问题的解决之道,之后慢慢的再遇到的都是一些重复性的问题了,当然,还有一些自己没有完全弄明白的问题.如果做的事情是重复的,遇到重复性问题的概率也就 ...

  2. Maven依赖解析

    本文将记录Maven工程中依赖解析机制,内容包括: Maven依赖基本结构 从仓库解析依赖的机制 依赖传递性解析实例 1. Maven依赖基本结构 上篇文章记录了Maven依赖的聚合与继承,POM中依 ...

  3. Maven依赖机制

    案例分析 让我们看一个案例研究,以了解它是如何工作的.假设你想使用 Log4j 作为项目的日志.这里你要做什么? 1.在传统方式 访问 http://logging.apache.org/log4j/ ...

  4. Maven学习(八)-----Maven依赖机制

    Maven依赖机制 在 Maven 依赖机制的帮助下自动下载所有必需的依赖库,并保持版本升级. 案例分析 让我们看一个案例研究,以了解它是如何工作的.假设你想使用 Log4j 作为项目的日志.这里你要 ...

  5. maven 学习---Maven依赖机制

    在 Maven 依赖机制的帮助下自动下载所有必需的依赖库,并保持版本升级. 案例分析 让我们看一个案例研究,以了解它是如何工作的.假设你想使用 Log4j 作为项目的日志.这里你要做什么? 1.在传统 ...

  6. Maven 依赖树的解析规则

    对于 Java 开发工程师来说,Maven 是依赖管理和代码构建的标准.遵循「约定大于配置」理念.Maven 是 Java 开发工程师日常使用的工具,本篇文章简要介绍一下 Maven 的依赖树解析. ...

  7. SpringCloud升级之路2020.0.x版-4.maven依赖回顾以及项目框架结构

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们先来回顾下 m ...

  8. 解决maven依赖冲突,这篇就够了!

    一.前言 什么是依赖冲突 依赖冲突是指项目依赖的某一个jar包,有多个不同的版本,因而造成了包版本冲突. 依赖冲突的原因 我们在maven项目的pom中 一般会引用许许多多的dependency.例如 ...

  9. AMD and CMD are dead之KMDjs内核之依赖分析

    有人说js中有三座大三:this.原型链和scope tree,搞懂了他们就算是js成人礼.当然还有其他不同看法的js成人礼,如熟悉js的:OOP.AP.FP.DOP.AOP.当然还听说一种最牛B的j ...

随机推荐

  1. [mysql使用(3)] 使用mysql的时候遇到的一些错误

    1.Err1055,出现这个问题往往是在执行sql语句时候,在最后一行会出现这个问题. [Err] 1055 - Expression #1 of ORDER BY clause is not in ...

  2. 【UML 建模】活动图介绍

    1.活动图,即Activity Diagram,是UML中用于对系统的动态行为建模的一种常用工具,它描述活动的顺序,展现从一种活动到另一种活动的控制流.其本质上是一种流程图,着重表现从一个活动到另一个 ...

  3. Linux系列教程(四)——Linux文件和目录处理命令

    这个系列教程的前面我们讲解了如何安装Linux系统,以及学习Linux系统的一些方法.那么从这篇博客开始,我们就正式进入Linux命令的学习.学习命令,首先要跟大家纠正的一点就是,我们不需要记住每一条 ...

  4. LeetCode 62. Unique Paths(所有不同的路径)

    A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below). The ...

  5. C#第二篇——关于C#中的正则表达式

    在C#中,正则表达式是用来进行查询在给出的一串字符中的某些字符或者数字的工具.与在办公软件中的查找功能相似,可以用精确查找也可以用模糊查找. 元字符: 元字符 说明 . 匹配除换行符以外的任意字符 \ ...

  6. 无所不会的fiddler遇到的尴尬

    昨天测试项目时,遇到一个尴尬的事 预期功能:点击页面某个按钮会post2个请求 实际情况:点了按钮,fiddler抓包没有看到任何请求 后来经过他人提醒在PC端浏览器打开此页面,点击按钮后看到页面有j ...

  7. 使用Identity Server 4建立Authorization Server (1)

    预备知识: http://www.cnblogs.com/cgzl/p/7746496.html 本文内容基本完全来自于Identity Server 4官方文档: https://identitys ...

  8. Python Web框架篇:Django Form组件

    Form简介 在HTTP中,表单(form标签),是用来提交数据的,其action属性说明了其传输数据的方法:如何传.如何接收. 访问网站时,表单可以实现客户端与服务器之间的通信.例如查询,就用到了表 ...

  9. 剖析Linux系统调用的执行路径

    在什么是操作系统这篇文章中,介绍过操作系统像是一个代理一样,为我们去管理计算机的众多硬件,我们需要计算机的一些计算服务.数据管理的服务,都由操作系统提供接口来完成.这样做的好处是让一般的计算机使用者不 ...

  10. css伪类的说明以及使用(css事件)

    CSS伪类的使用(css事件) 转载请注明源地址:http://www.cnblogs.com/funnyzpc/p/7670959.html 之前有开发开发App的时候,有同事问我那个列表的条目按下 ...