1. 程式人生 > >Maven JAR包衝突問題排查及解決方案

Maven JAR包衝突問題排查及解決方案

前言

寫這篇文章的初衷是因為今天在使用mvn dependency:tree命令時,突然想起一年前面試阿里的一道面試題。面試題是說假設線上發生JAR包衝突,應該怎麼排查?我那時候的回答是IDEA有個Maven Helper的外掛,可以幫忙分析依賴衝突,然後還有一種辦法是如果一個類import的時候提示兩個地方可匯入,那就說明有衝突。現在回頭想想確實太不專業了,以下是一次JAR包衝突的一個比較正規的流程,是通過整理幾篇部落格後總結的希望對大家也有幫助,如果有錯誤的地方也歡迎指出

  • JAR衝突產生的原因

        Pom.xml
        /    \
      B        C
    /  \      /  \
    
    X Y X M

    在以上依賴關係中專案除了會引入B、C還會引入X、Y、M的依賴包,但是如果B依賴的X版本會1.0而C依賴的X版本為2.0時,那最後專案使用的到底是X的1.0版本還是2.0版本就無法確定了。這是就要看ClassLoader的載入順序,假設ClassLoader先載入1.0版本那就不會載入2.0版本,反之同理

  • 使用mvn -Dverbose dependency:tree排查衝突

    [INFO] +- org.apache.tomcat:tomcat-servlet-api:jar:7.0.70:compile
    [INFO] +- org.apache.tomcat:tomcat-jsp-api:
    jar:7.0.70:compile [INFO] | +- org.apache.tomcat:tomcat-el-api:jar:7.0.70:compile [INFO] | \- (org.apache.tomcat:tomcat-servlet-api:jar:7.0.70:compile - omitted for duplicate) [INFO] +- net.sf.jasperreports:jasperreports:jar:5.6.0:compile [INFO] | +- (commons-beanutils:commons-beanutils:jar:1.8.0:compile
    - omitted for conflict with 1.8.3) [INFO] | +- commons-collections:commons-collections:jar:3.2.1:compile [INFO] | +- commons-digester:commons-digester:jar:2.1:compile [INFO] | | +- (commons-beanutils:commons-beanutils:jar:1.8.3:compile - omitted for duplicate) [INFO] | | \- (commons-logging:commons-logging:jar:1.1.1:compile - omitted for duplicate)

    遞迴依賴的關係列的算是比較清楚了,每行都是一個jar包,根據縮排可以看到依賴的關係

    • 最後寫著compile的就是編譯成功的
    • 最後寫著omitted for duplicate的就是有JAR包被重複依賴了,但是JAR包的版本是一樣的
    • 最後寫著omitted for conflict with xx的,說明和別的JAR包版本衝突了,該行的JAR包不會被引入
    該命令可配合-Dincludes-Dexcludes進行使用,只輸出自己感興趣/不感興趣的JAR
    引數格式為:[groupId]:[artifactId]:[type]:[version]
    每個部分(冒號分割的部分)是支援*萬用字元的,如果要指定多個格式則可以用,分割,如:
    mvn dependency:tree -Dincludes=javax.servlet,org.apache.*
  • 解決衝突,使用exclusion標籤將衝突的JAR排除

    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>dubbo</artifactId>
      <version>2.8.3.2</version>
      <exclusions>
          <exclusion>
              <artifactId>guava</artifactId>
              <groupId>com.google.guava</groupId>
          </exclusion>
          <exclusion>
              <artifactId>spring</artifactId>
              <groupId>org.springframework</groupId>
          </exclusion>    
      </exclusions>
    </dependency>

    解決了衝突後的樹(解決衝突的策略是:就近原則,即離根近的依賴被採納)

  • 檢視執行期類來源的JAR包

    有時你以為解決了但是偏偏還是報類包衝突,典型症狀是java.lang.ClassNotFoundExceptionMethod不相容等異常,這時你可以設定一個斷點,在斷點處通過下面這個工具類來檢視Class所來源的JAR

    public class ClassLocationUtils {
      public static String where(final Class clazz) {
          if (clazz == null) {
              throw new IllegalArgumentException("null input: cls");
          }
          URL result = null;
          final String clazzAsResource = clazz.getName().replace('.', '/').concat(".class");
          final ProtectionDomain protectionDomain = clazz.getProtectionDomain();
          if (protectionDomain != null) {
              final CodeSource codeSource = protectionDomain.getCodeSource();
              if (codeSource != null) result = codeSource.getLocation();
              if (result != null) {
                  if ("file".equals(result.getProtocol())) {
                      try {
                          if (result.toExternalForm().endsWith(".jar") || result.toExternalForm().endsWith(".zip")) {
                              result = new URL("jar:".concat(result.toExternalForm()).concat("!/").concat(clazzAsResource));
                          } else if (new File(result.getFile()).isDirectory()) {
                              result = new URL(result, clazzAsResource);
                          }
                      } catch (MalformedURLException ignore) {
    
                      }
                  }
              }
          }
          if (result == null) {
              final ClassLoader clsLoader = clazz.getClassLoader();
              result = clsLoader != null ? clsLoader.getResource(clazzAsResource) : ClassLoader.getSystemResource(clazzAsResource);
          }
          return result.toString();
      }
    }

    然後隨便寫一個測試設定好斷點,在執行到斷點出使用ALT+F8動態執行程式碼,如

    ClassLocationUtils.where(Logger.class)

    即可馬上找到對應的JAR,如果這個JAR不是你期望的就說明是IDE快取造成的,清除快取後重試即可