欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Maven JAR包问题排查及解决方案

程序员文章站 2022-05-28 12:57:36
...

前言

写这篇文章的初衷是因为今天在使用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缓存造成的,清除缓存后重试即可