Java类隔离加载实现

类隔离技术是为了解决依赖冲突而诞生的,它通过自定义类加载器破坏双亲委派机制,然后利用类加载传导规则实现了不同模块的类隔离。

自定义的类加载器,首先继承 java.lang.ClassLoader,然后重写类加载方法:

  • 重写 findClass(String name)
  • 重写 loadClass(String name),破坏双亲委派模式。

类加载传导规则:JVM 会选择当前类的类加载器来加载所有该类的引用的类。

例如:我们定义了 TestA 和 TestB 两个类,TestA 会引用 TestB,只要我们使用自定义的类加载器加载 TestA,那么在运行时,当 TestA 调用到 TestB 的时候,TestB 也会被 JVM 使用 TestA 的类加载器加载。依此类推,只要是 TestA 及其引用类关联的所有 jar 包的类都会被自定义类加载器加载。

通过这种方式,只要让模块的 main 方法类使用不同的类加载器加载,那么每个模块的都会使用 main 方法类的类加载器加载,这样就能让多个模块分别使用不同类加载器。

重写 findClass

首先定义两个类,TestA 会打印自己的类加载器,然后调用 TestB 打印它的类加载器。(我们预期是实现重写了 findClass 方法的类加载器 MyClassLoaderParentFirst 能够在加载了 TestA 之后,让 TestB 也自动由 MyClassLoaderParentFirst 来进行加载。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.justxzm.classLoad;

public class TestA {
public static void main(String[] args) {
TestA testA = new TestA();
testA.hello();
}

public void hello() {
System.out.println("TestA: " + this.getClass().getClassLoader());
TestB testB = new TestB();
testB.hello();
}
}
1
2
3
4
5
6
7
package com.justxzm.classLoad;

public class TestB {
public void hello() {
System.out.println("TestB: " + this.getClass().getClassLoader());
}
}

然后重写一下 findClass 方法,这个方法先根据文件路径加载 class 文件,然后调用 defineClass 获取 Class 对象。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.justxzm.classLoad;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

public class MyClassLoaderParentFirst extends ClassLoader{

private Map<String, String> classPathMap = new HashMap<>();

public MyClassLoaderParentFirst() {
String classpath = "E:/MyLearning/LearningWorkSpace/MyLearning/bin/com/justxzm/classLoad";
classPathMap.put("com.justxzm.classLoad.TestA", classpath+"/TestA.class");
classPathMap.put("com.justxzm.classLoad.TestB", classpath+"/TestB.class");
}

// 重写了 findClass 方法
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
String classPath = classPathMap.get(name);
File file = new File(classPath);
if (!file.exists()) {
throw new ClassNotFoundException();
}
byte[] classBytes = getClassData(file);
if (classBytes == null || classBytes.length == 0) {
throw new ClassNotFoundException();
}
return defineClass(classBytes, 0, classBytes.length);
}

private byte[] getClassData(File file) {
try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new
ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[] {};
}
}

最后写一个 main 方法调用自定义的类加载器加载 TestA,然后通过反射调用 TestA 的 main 方法打印类加载器的信息。

1
2
3
4
5
6
7
8
9
10
11
12
package com.justxzm.classLoad;

import java.lang.reflect.Method;

public class MyTest {
public static void main(String[] args) throws Exception {
MyClassLoaderParentFirst myClassLoaderParentFirst = new MyClassLoaderParentFirst();
Class testAClass = myClassLoaderParentFirst.findClass("com.justxzm.classLoad.TestA");
Method mainMethod = testAClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { args });
}
}

执行结果如下:

1
2
TestA: com.justxzm.classLoad.MyClassLoaderParentFirst@72ffb35e
TestB: sun.misc.Launcher$AppClassLoader@62b92956

执行的结果并没有如我们期待,TestA 确实是 MyClassLoaderParentFirst 加载的,但是 TestB 还是 AppClassLoader 加载的。这是为什么呢?要回答这个问题,首先是要了解一个类加载的规则:

JVM 在触发类加载时调用的是 ClassLoader.loadClass 方法,该方法实现了双亲委派:

  • 委托给父加载器查询

  • 如果父加载器查询不到,就调用 findClass 方法进行加载

明白了这个规则之后,执行的结果的原因就找到了:

  • JVM 确实使用了MyClassLoaderParentFirst 来加载 TestB,但是因为双亲委派的机制,TestB 被委托给了 MyClassLoaderParentFirst 的父加载器 AppClassLoader 进行加载。
  • 为什么 MyClassLoaderParentFirst 的父加载器是 AppClassLoader?因为我们定义的 main 方法类默认情况下都是由 JDK 自带的 AppClassLoader 加载的,根据类加载传导规则,main 类引用的 MyClassLoaderParentFirst 也是由加载了 main 类的AppClassLoader 来加载。由于 MyClassLoaderParentFirst 的父类是 ClassLoader,ClassLoader 的默认构造方法会自动设置父加载器的值为 AppClassLoader。
1
2
3
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}

重写 loadClass

由于重写 findClass 方法会受到双亲委派机制的影响导致 TestB 被 AppClassLoader 加载,不符合类隔离的目标,所以我们只能重写 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.justxzm.classLoad;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

public class MyClassLoaderCustom extends ClassLoader{
private ClassLoader jdkClassLoader;

private Map<String, String> classPathMap = new HashMap<>();

public MyClassLoaderCustom(ClassLoader jdkClassLoader) {
this.jdkClassLoader = jdkClassLoader;
String classpath = "E:/MyLearning/LearningWorkSpace/MyLearning/bin/com/justxzm/classLoad";
classPathMap.put("com.justxzm.classLoad.TestA", classpath+"/TestA.class");
classPathMap.put("com.justxzm.classLoad.TestB", classpath+"/TestB.class");
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class result = null;
try {
//这里要使用 JDK 的类加载器加载 java.lang 包里面的类
result = jdkClassLoader.loadClass(name);
} catch (Exception e) {
//忽略
}
if (result != null) {
return result;
}
String classPath = classPathMap.get(name);
File file = new File(classPath);
if (!file.exists()) {
throw new ClassNotFoundException();
}

byte[] classBytes = getClassData(file);
if (classBytes == null || classBytes.length == 0) {
throw new ClassNotFoundException();
}
return defineClass(classBytes, 0, classBytes.length);
}

private byte[] getClassData(File file) {
try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new
ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[] {};
}
}

重写了 loadClass 方法意味着所有类包括 java.lang 包里面的类都会通过 MyClassLoaderCustom 进行加载,但类隔离的目标不包括这部分 JDK 自带的类,所以我们用 ExtClassLoader 来加载 JDK 的类,相关的代码就是:result = jdkClassLoader.loadClass(name);

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.justxzm.classLoad;

import java.lang.reflect.Method;

public class MyTest {
public static void main(String[] args) throws Exception {
//这里取AppClassLoader的父加载器也就是ExtClassLoader作为MyClassLoaderCustom的jdkClassLoader
MyClassLoaderCustom myClassLoaderCustom = new MyClassLoaderCustom(Thread.currentThread().getContextClassLoader().getParent());
Class testAClass = myClassLoaderCustom.loadClass("com.justxzm.classLoad.TestA");
Method mainMethod = testAClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[]{args});
}
}
1
2
TestA: com.justxzm.classLoad.MyClassLoaderCustom@110f965e
TestB: com.justxzm.classLoad.MyClassLoaderCustom@110f965e

重写了 loadClass 方法,TestB 也使用MyClassLoaderCustom 成功加载到了 JVM 中。

0%