背景
昨天幫一位同事排查了一個依賴沖突的問題。問題的現象就是在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-a、m-b、m-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包名稱。
