1. 问题

项目中依赖一个Jar,同时有一个地方又依赖这个Jar的早期版本的某方法或者某类等,
如果不依赖上这个早期版本的话可能造成代码错误,调用不到指定的方法;
但是如果依赖了这个早期版本,两个版本都在项目中大概率会造成无法启动,冲突;
为了解决这个问题,我们便需要将局部地方依赖的早期jar作为局部使用的依赖传入项目供使用
这需要知道ClassLoader的工作原理并正确的加载

如我项目中调用一个早期版本的Test1类

1
2
3
<groupId>com.kewen.demo</groupId>
<artifactId>D0-Simple</artifactId>
<version>1.0-SNAPSHOT</version>
1
2
3
4
5
6
public class Test1 {
public void hello(String a1,String a2){
System.out.printf("a1+a2 = "+a1+a2);
//处理逻辑
}
}

但现在这个包升级了,变成了如下

1
2
3
<groupId>com.kewen.demo</groupId>
<artifactId>D0-Simple</artifactId>
<version>1.0.1-SNAPSHOT</version>
1
2
3
4
5
6
7

public class Test1 {
public void hello(String a1,String a2){
System.out.printf("变化之后的 a1+a2 = "+a1+a2);
//新的逻辑
}
}

现项目中引入的是新版本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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public abstract class ClassLoader {
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
//检查是否已经加载了,加载了则使用加载了的
Class<?> c = findLoadedClass(name);

if (c == null) {
long t0 = System.nanoTime();
try {

//判断是否有父级,有的话从父级加载
//**就是这里**会先获取parent的class
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

//没有获取到则获取当前的
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}

知道了这个原理,我们便可以通过classloader加载指定的jar了

3. 代码实现

3.1. 第一种情况:新的方法和原来的方法无关

新旧两个版本的方法没啥关系,即不是同一个方法(参数不同也算不同)

如:

  • 旧版本

    1
    2
    3
    4
    5
    6
    public class Test1 {
    public void hello(String a1,String a2){
    System.out.printf("a1+a2 = "+a1+a2);
    //处理逻辑
    }
    }
  • 新版本

    1
    2
    3
    4
    5
    6
    public class Test1 {
    public void hello(String a1,String a2,String a3){
    System.out.printf("a1+a2 +a3= "+a1+a2+a3);
    //处理逻辑
    }
    }

这时我们可以直接新建一个Classloader把原Classloader加载进去即可,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class D116app {

public static void main(String[] args) throws Exception {
//1. 获取到当前Classloader,这个classloader可以获取到所有的加载类
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

//2. 新建classlaoder加载指定的jar包,注意这里是把原来的classloader作为parent传进去的
//2. 新建classlaoder加载指定的jar包,注意这里是把原来的classloader作为parent传进去的
URLClassLoader loader = new URLClassLoader(new URL[]{new File("D:\\Projects\\demo-project\\D116-Outerclassloader\\lib\\D0-Simple-1.0-SNAPSHOT.jar").toURI().toURL()}, classLoader);

//3. 拿到Class
Class<?> aClass = loader.loadClass("com.kewen.d116.Test1");

//4. 实例化和拿到方法等,(旧方法的参数)
Object o = aClass.newInstance();
Method method = aClass.getMethod("hello", String.class,String.class);
}
}

上面代码中

  1. 通过Thread加载的ClassLoader包含了项目中启动的ClassLoader,加载所有的类
  2. 新建的通过路径加入新的jar的类信息等,相当于还是能拿到所有的以及新的jar
  3. 通过classloader拿到需要的类,实际上由于没有覆盖关系,新的旧的方法都能拿到

3.2. 第二种情况:新的方法和原来的相同

即方法相同,通过Method无法区分

如:

  • 旧版本

    1
    2
    3
    4
    5
    6
    public class Test1 {
    public void hello(String a1,String a2){
    System.out.printf("a1+a2 = "+a1+a2);
    //处理逻辑
    }
    }
  • 新版本

    1
    2
    3
    4
    5
    6
    public class Test1 {
    public void hello(String a1,String a2){
    System.out.printf("a1+a2= "+a1+a2);
    //处理逻辑
    }
    }

由于原来与现在包含了相同的类和相同的方法,我们如果用第一种方式获取则会出现问题,因为获取类的时候是先获取parent中的,当parent获取到之后则返回
相同的类和方法则肯定会在parent中获取到,因此就没有办法在我们新指定的classloader中获取到

解决的思路也简单,就是我们构造classloader的时候不带原本加载的classloader

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class D116app {

public static void main(String[] args) throws Exception {
//1. 获取到当前Classloader,这个classloader可以获取到所有的加载类
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

//2. 只获取根节点的classloader,不再要下层的,因为我们本身只需要通过构造classloadr拿到指定的类
while (classLoader.getParent() !=null){
classLoader = classLoader.getParent();
}

//3. 新建classlaoder加载指定的jar包,注意这里是把原来的classloader作为parent传进去的
URLClassLoader loader = new URLClassLoader(new URL[]{new File("D:\\Projects\\demo-project\\D116-Outerclassloader\\lib\\D0-Simple-1.0-SNAPSHOT.jar").toURI().toURL()}, classLoader);

//4. 拿到Class
Class<?> aClass = loader.loadClass("com.kewen.d116.Test1");

//5. 实例化和拿到方法等,(旧方法的参数)
Object o = aClass.newInstance();
Method method = aClass.getMethod("hello", String.class,String.class);
}
}

通过增加第二步,传入的classloader只包含了根节点,中间的包含了项目中其他类加载器的就不要了,这样就可以只加载当前classloader本身