Classloader加载外部jar
1. 问题
项目中依赖一个Jar,同时有一个地方又依赖这个Jar的早期版本的某方法或者某类等,
如果不依赖上这个早期版本的话可能造成代码错误,调用不到指定的方法;
但是如果依赖了这个早期版本,两个版本都在项目中大概率会造成无法启动,冲突;
为了解决这个问题,我们便需要将局部地方依赖的早期jar作为局部使用的依赖传入项目供使用
这需要知道ClassLoader的工作原理并正确的加载
如我项目中调用一个早期版本的Test1类
1 | <groupId>com.kewen.demo</groupId> |
1 | public class Test1 { |
但现在这个包升级了,变成了如下
1 | <groupId>com.kewen.demo</groupId> |
1 |
|
现项目中引入的是新版本1.0.1-SNAPSHOT
,但是我们某一个指定位置的调用需要调用旧版本的代码,此位置便需要单独加载;
2. 原理
2.1. Java类加载实现原理
Java虚拟机加载的时候,需要通过classloader类加载器加载类的二进制流,类加载器一共包含
- 启动类加载器(Bootstrap ClassLoader):前面已经大致介绍过了,这个类加载器负责将存放在
\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 - 扩展类加载器(Extension ClassLoader):这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将
/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 - 应用程序类加载器(Application ClassLoader):这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
2.2. 双亲委派模型
上图是上面所介绍的这几种类加载器的层次关系,称为类加载器的双亲委派模型。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。
一言概之,双亲委派模型,其实就是一种类加载器的层次关系。
我们启动程序的时候就会创建一系列Classloader,Classloader会嵌套parent。这就是双亲委派的运用。
当我们调用classLoader.loadClass()
的时候会先调用上层parent的classLoader.loadClass()
,当存在一个值的时候便会返回得到的结果,此时不再继续调用
源码主要看loadClass这里
1 | public abstract class ClassLoader { |
知道了这个原理,我们便可以通过classloader加载指定的jar了
3. 代码实现
3.1. 第一种情况:新的方法和原来的方法无关
新旧两个版本的方法没啥关系,即不是同一个方法(参数不同也算不同)
如:
旧版本
1
2
3
4
5
6public class Test1 {
public void hello(String a1,String a2){
System.out.printf("a1+a2 = "+a1+a2);
//处理逻辑
}
}新版本
1
2
3
4
5
6public class Test1 {
public void hello(String a1,String a2,String a3){
System.out.printf("a1+a2 +a3= "+a1+a2+a3);
//处理逻辑
}
}
这时我们可以直接新建一个Classloader把原Classloader加载进去即可,如
1 | public class D116app { |
上面代码中
- 通过Thread加载的ClassLoader包含了项目中启动的ClassLoader,加载所有的类
- 新建的通过路径加入新的jar的类信息等,相当于还是能拿到所有的以及新的jar
- 通过classloader拿到需要的类,实际上由于没有覆盖关系,新的旧的方法都能拿到
3.2. 第二种情况:新的方法和原来的相同
即方法相同,通过Method无法区分
如:
旧版本
1
2
3
4
5
6public class Test1 {
public void hello(String a1,String a2){
System.out.printf("a1+a2 = "+a1+a2);
//处理逻辑
}
}新版本
1
2
3
4
5
6public class Test1 {
public void hello(String a1,String a2){
System.out.printf("a1+a2= "+a1+a2);
//处理逻辑
}
}
由于原来与现在包含了相同的类和相同的方法,我们如果用第一种方式获取则会出现问题,因为获取类的时候是先获取parent中的,当parent获取到之后则返回
相同的类和方法则肯定会在parent中获取到,因此就没有办法在我们新指定的classloader中获取到
解决的思路也简单,就是我们构造classloader的时候不带原本加载的classloader
如下
1 | public class D116app { |
通过增加第二步,传入的classloader只包含了根节点,中间的包含了项目中其他类加载器的就不要了,这样就可以只加载当前classloader本身