Maven依赖分析

Java基础

浏览数:316

2019-3-26

背景

昨天帮一位同事排查了一个依赖冲突的问题。问题的现象就是在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核心知识