前段时间,因为经历了项目重构,引入很多包,加上管理不善,出现了很多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已经给我们提供了很多解决的方法。
找不到方法:java.lang.NoSuchMethodError
找不到类:Exception in thread "main" java.lang.NoClassDefFoundError
找不到变量:Exception in thread "main" java.lang.NoSuchFieldError
项目a依赖项目b和项目c,项目b依赖项目d的1.0-snapshot版本,项目c依赖项目d的2.0-snapshot版本
public class Banana { public String getName() { System.out.println(“this is Banana, delicious!”); return this.getClass().getSimpleName(); } }
public class Cherry { public String getName() { System.out.println(“this is Cherry, delicious!”); return this.getClass().getSimpleName(); } }
public class Fruit { public void eatFruit() { Banana banana = new Banana(); String name = banana.getName(); System.out.println(“b” + name);
} }
public class Fruit { public void eatFruit() { Cherry cherry = new Cherry(); String name = cherry.getName(); System.out.println("now i am eat " + name); } }
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(); } }
项目a依赖项目b和项目c,项目b依赖项目d,项目c依赖项目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”; } }
public class Cherry { public String getName() { System.out.println(“this is Cherry, delicious!”); return this.getClass().getSimpleName(); } }
public class Fruit { public void eatFruit() { Cherry cherry = new Cherry(); String name = cherry.getName(); System.out.println("now i am eat " + name); } }
public class Fruit { public void eatFruit() { Banana banana = new Banana(); String name = banana.getName(); System.out.println(“b” + name);
} }
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(); } }
感觉maven的依赖原则使用的是广度搜索算法,主要有两个原则,路径不同,取路径最短,路径相同,按照申明顺序,在上面的项目举例中,可以看到,对于项目d的两个版本,路径长度是一样,都为2,这时候就是比较项目c,b在项目a中声名的顺序,在a项目中b项目依赖写在前面,c项目写在后面,所以出现了找不到Cherry.class类。
这里只是简单讲一下双亲委派模式和tomcat的加载过程,不细说。
java的类大概可以分为核心类,可扩展类,应用程序类三种类别。其中核心类有启动类加载器加载,扩展类由扩展类加载器加载,应用程序类有应用程序类加载器加载。在一个类的加载过程中,类加载器首先会将这个加载过程交给父亲类加载器完成,比如应用程序的类,定义是有自己定义的类加载器加载,会传给应用程序类加载器,一直往上,知道给启动类加载器加载。而如果父类加载器加载不了,才会往下传,直到该类被加载到jvm中。
这个问题现在想想也是有意思,涉及到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任务中发现了双重锁机制的单例出现了问题。