背景

昨天帮一位同事排查了一个依赖冲突的问题。问题的现象就是在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. jQuery Mobile 所有class选项,开发全解+完美注释

    全栈工程师开发手册 (作者:栾鹏) jQuery Mobile事件全解 jQuery Mobile 所有class选项 jQuery Mobile 所有data-*选项 jQuery Mobile 所 ...

  2. 【转载】CSS direction属性简介与实际应用

    文章转载自 张鑫旭-鑫空间-鑫生活 http://www.zhangxinxu.com/wordpress/ 原文链接:http://www.zhangxinxu.com/wordpress/?p=5 ...

  3. typescript 的 polyfill 学习

    我们知道typescript 是ES 超集.这意味着,不仅仅ES 的各种语法特性都会包括,还能保证通过typescript的编译服务可以很方便的转成ES向下兼容的版本,这得意于typescript强大 ...

  4. 常用的HTTP状态码

    成功的状态码: 200 – 服务器成功返回网页 304 – 未修改 失败的状态码: 404 – 请求的网页不存在 503 – 服务器暂时不可用 500 – 服务器内部错误 下面的不是很常用,记住上面那 ...

  5. Python练习----多级菜单

    多级菜单要求:      1.三级菜单          2.可依次选择进入各子菜单      3.可以返回上一层      4.输入'q'可以退出   脚本: zone = { '北京' : { ' ...

  6. Ubuntu系统下的实用软件推荐

    想要在ubuntu下工作? 又担心影响效率? 这些软件可以帮助你解决问题 ! 基本在windows上可以做到的功能, 在linux中也同样能够实现,而且自己寻找解决方案的过程才是最有趣的! 1.gua ...

  7. JAVA 编码解码

    涉及编码的地方一般都在从字符到字节或是从字节到字符间的转换上 1.在IO中存在的编码,主要是 FileOutputStream 和 FileInputStream,在使用时需要指定字符集,而不是使用系 ...

  8. oracle建表权限问题和JSP连接oracle数据库基本操作

    JSP连接oracle数据库相关操作 1.创建表 打开Enterprise Manager Console,为用户添加权限CREATE ANY TABLE和分配一定的表空间USERS限额1024k. ...

  9. 关于 SVN 项目检出

    前几天呢,同事遇到这么一个问题:他新建了一个工作空间,当他通过 svn 检出公司项目的时候,准备过来测试运行,但是呢出现了下面的报错 [ERROR] Failed to execute goal or ...

  10. Windows下swoole扩展的编译安装部署

    1. 到cygwin官网下载cygwin. 官网地址:https://www.cygwin.com/ 2. 打开下载好的cygwin安装包,开始安装cygwin. 选择cygwin的安装目录(这个同时 ...