jar包冲突原因分析

jar包冲突原因

前段时间,因为经历了项目重构,引入很多包,加上管理不善,出现了很多jar包冲突问题。当时项目想用spring管理hbase实例,引入了org.springframework.data,spring-data-hadoop,2.5.0.RELEASE jar包,出现了guava包的冲突,tomcat对servlet-api加载冲突问题。在最近的开发中也遇到了curator-client包冲突问题。借着这次机会,顺便学习写博客。
其实在java开发中,jar hell是一个很常见的问题,主要是因为jvm在加载的过程中,项目的加载的顺序问题。同一个类(全限定名相同)在jvm中只会加载一次,这里面涉及到类加载和maven依赖管理的知识点。当然,maven已经给我们提供了很多解决的方法。

冲突原因
造成jar包冲突的原因主要有两种
1. 第一种是一个项目,依赖了同一个项目的两个版本。
2. 第二种冲突原因是不同项目中,出现了相同的类。何为相同的类,即类的全限定名相同。
一般的错误有:

找不到方法:java.lang.NoSuchMethodError

找不到类:Exception in thread "main" java.lang.NoClassDefFoundError

找不到变量:Exception in thread "main" java.lang.NoSuchFieldError


项目工程

  • 引入5个项目工程做说明:分别为leaf-verb-a(简称a),leaf-verb-b(简称b),leaf-verb-c(简称c),leaf-verb-d(简称d),leaf-verb-e(简称e)

同一个项目中,版本不同造成

项目关系
leaf-verb-a
leaf-verb-b
leaf-verb-c
leaf-verb-d

项目a依赖项目b和项目c,项目b依赖项目d的1.0-snapshot版本,项目c依赖项目d的2.0-snapshot版本

1.0-snapshot版本d项目

public class Banana {
public String getName() {
System.out.println(“this is Banana, delicious!”);
return this.getClass().getSimpleName();
}
}

2.0-snapshot版本d项目

public class Cherry {
public String getName() {
System.out.println(“this is Cherry, delicious!”);
return this.getClass().getSimpleName();
}
}

项目b

public class Fruit {
public void eatFruit() {
Banana banana = new Banana();
String name = banana.getName();
System.out.println(“b” + name);

}
}

项目c

public class Fruit {
public void eatFruit() {
Cherry cherry = new Cherry();
String name = cherry.getName();
System.out.println("now i am eat " + name);
}
}

项目a

public class Dinner {
public void eat() {
com.leaf.b.Fruit fruit = new com.leaf.b.Fruit();
fruit.eatFruit();
com.leaf.c.Fruit eatFruit = new com.leaf.c.Fruit();
eatFruit.eatFruit();

}

public static void main(String[] args) {
Dinner dinner = new Dinner();
dinner.eat();
}
}

出现如下报错:
Exception in thread "main" java.lang.NoClassDefFoundError: com/leaf/d/Cherry
	at com.leaf.c.Fruit.eatFruit(Fruit.java:11)
	at com.leaf.a.Dinner.eat(Dinner.java:12)
	at com.leaf.a.Dinner.main(Dinner.java:18)
Caused by: java.lang.ClassNotFoundException: com.leaf.d.Cherry
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 3 more

不同项目中,出现相同的类

项目关系
leaf-verb-a
leaf-verb-b
leaf-verb-c
leaf-verb-d
leaf-verb-e

项目a依赖项目b和项目c,项目b依赖项目d,项目c依赖项目e

项目e

package com.leaf.d;
public class Cherry {
public String shade() {
System.out.println(“circle”);
return “circle”;
}
}

package com.leaf.d;
public class Banana {
public String shade() {
System.out.println(“long”);
return “long”;
}
}

项目d

public class Cherry {
public String getName() {
System.out.println(“this is Cherry, delicious!”);
return this.getClass().getSimpleName();
}
}

项目c

public class Fruit {
public void eatFruit() {
Cherry cherry = new Cherry();
String name = cherry.getName();
System.out.println("now i am eat " + name);
}
}

项目b

public class Fruit {
public void eatFruit() {
Banana banana = new Banana();
String name = banana.getName();
System.out.println(“b” + name);

}
}

项目a

public class Dinner {
public void eat() {
com.leaf.b.Fruit fruit = new com.leaf.b.Fruit();
fruit.eatFruit();
com.leaf.c.Fruit eatFruit = new com.leaf.c.Fruit();
eatFruit.eatFruit();

}

public static void main(String[] args) {
Dinner dinner = new Dinner();
dinner.eat();
}
}

出现如下错误:
Exception in thread "main" java.lang.NoSuchMethodError: com.leaf.d.Banana.getName()Ljava/lang/String;
	at com.leaf.b.Fruit.eatFruit(Fruit.java:12)
	at com.leaf.a.Dinner.eat(Dinner.java:10)
	at com.leaf.a.Dinner.main(Dinner.java:18)

Process finished with exit code 1
maven的依赖原则

感觉maven的依赖原则使用的是广度搜索算法,主要有两个原则,路径不同,取路径最短,路径相同,按照申明顺序,在上面的项目举例中,可以看到,对于项目d的两个版本,路径长度是一样,都为2,这时候就是比较项目c,b在项目a中声名的顺序,在a项目中b项目依赖写在前面,c项目写在后面,所以出现了找不到Cherry.class类。

类加载机制

这里只是简单讲一下双亲委派模式和tomcat的加载过程,不细说。

双亲委派模式
启动类加载器
扩展类加载器
应用程序类加载器
自定义类加载器
自定义类加载器

java的类大概可以分为核心类,可扩展类,应用程序类三种类别。其中核心类有启动类加载器加载,扩展类由扩展类加载器加载,应用程序类有应用程序类加载器加载。在一个类的加载过程中,类加载器首先会将这个加载过程交给父亲类加载器完成,比如应用程序的类,定义是有自己定义的类加载器加载,会传给应用程序类加载器,一直往上,知道给启动类加载器加载。而如果父类加载器加载不了,才会往下传,直到该类被加载到jvm中。

tomcat的类加载机制

jar包冲突原因分析_第1张图片

遇见问题和解决过程

guava包冲突问题
  • guava冲突这个相信有过java开发的程序员基本都能遇到,这个包很实用,但是不同保本变动很大,很容易就会出现冲突问题。当时遇到guava包冲突问题主要分为两方面,
  • 第一方面是spring-data-hadoop中引入的guava版本和本来项目中冲突,当然这个问题很好解决。idea有个插件(maven helper)可以很方便的查到这个guava包有哪些版本冲突。当时我们看报错日志(具体报错在本文有介绍),通过报错打印的类快速定位到是那个包出现了问题,之后可以通过maven的exclude排除掉即可。
  • 第二方面出现的问题是在顶层web项目中遇到的,也是guava包问题,用我们上文定义的项目举例。项目a为顶层web项目。项目d为guava包。项目b,项目c分别调用了guava的两个版本。而guava这两个版本都是我项目需要的,这时候问题就来了,我不能将guava的两个版本中的任何一个干掉,同时调用又会出现冲突问题。怎么办,maven提供了我们一个插件maven-shade-plugin,可以在打包过程中指定将类的全限定名转换。比如:上面项目中的,我在b项目中,d项目的类Banana的全限定名为com.leaf.d.Banana,通过shade插件,可以转换为com.leaf.b.Banana(具体插件如何使用这里不做细说),同理,将Cherry的全限定名转换为com.leaf.c.Cherry
    这样,对于项目a来说,是可以将这两个类都加载的。也可以解决冲突的问题。有些尴尬的是,由于对插件的不熟悉,我发现我们项目对guava=转换后的路径也是一样的,汗。
    现在回过头来看,其实解决过程也挺简单的。
  1. 首先冲突问题表现,如在项目a中看日志,发现类,字段,函数找不到这些错误,基本可以确认是冲突问题。
  2. 接着看看具体这个类是属于那个jar包(或者哪些jar包,因为有可能出现一个类出现在多个包中,双击shift,用全限定名去查找),基本可以确认是哪个出现了冲突问题。
  3. 然后通过idea插件(maven helper或者其他)查看项目中具体引入了jar哪些版本。
  4. 最后尝试exclude掉一些版本,做测试,或者是通过shade插件基本就可以将问题解决了。
servlet-api问题

这个问题现在想想也是有意思,涉及到tomcat的类加载机制,还涉及到idea的项目打包问题(这个具体没有去深究)。
也是spring-data-hadoop这个包引入了一个包servlet-api版本为2.5,主要还是javax/servlet/Servlet.class这个类。在tomcat的lib目录下有一个servlet-api版本应该为3.X,出现了冲突,因为项目中使用shard插件,使用这个插件打包,对外来说是一个jar包。还是用上面的列子,即本来是d项目出现了冲突,c项目使用了shade插件,对外c项目就是一个jar包,a项目引入了c项目,在启动tomcat是,因为冲突,造成了全部c项目的类都没有加载到。
这个问题主要有两点需要注意,第一就是servlet.class这个类的冲突,第二就是shade插件的打包后的jar包对外是一个jar。
还有个有趣的是,对于使用idea的程序员,我们平时都喜欢将多个项目以模块方式放在一起开发,也方便调试。修改底层项目也不需要每次都手动打包,可以快速开发。但是这种方式和我们手动打的包是有些差别的。但是我本地调试tomcat没问题,放到服务器上就报错。最后还是对比了两个方式c项目(举例)的jar(一个几百k,一个几十m)才发现问题。

最近开发中还发下很多有趣的问题,比如spring的ClassPathXmlApplicationContext了两次就报错。
在spark任务中发现了双重锁机制的单例出现了问题。

你可能感兴趣的:(java基础)