0%

类加载实战

前面一直都是在介绍概念,正所谓实践才是检验真理的唯一标准,这里我们就来动手实现一个简单的类加载器,这里实现的类加载器很简单,但足够理解前面的所说的内容。

findLoadedClass方法简介

ClassLoader的findLoadedClass方法,传入一个全类名,如果当前ClassLoader的引用和全类名之间的关系被JVM所记录,那么将返回这个Class的实例。

前文介绍过, SystemDictionary,系统字典是保存Java加载类型的数据结构。它通过Class的全类名以及ClassLoader实例来定位一个Class实例引用。

在ClassLoader的实现中,我们没有看到如何将对应关系添加到SystemDictionary中,原因在于添加到SystemDictionary的逻辑在JVM的实现中,并且在某些情况下才会触发,而findLoadedClass方法仅仅简单的去SystemDictionary中查询一下而已。

因此从findLoadedClass中查询出什么已经变得不在重要,而在哪些情况下被添加到SystemDictionary才是关键。在SystemDictionary中生成ClassLoader和全类名的记录有如下情况:

  1. 确由该ClassLoader加载了类,记入SystemDictionary;
  2. 通过Class.forName(String name)或者Class.forName(String name, boolean initialize, ClassLoader loader)来加载全类名为name的类,如果传递的ClassLoader与真实加载类的ClassLoader不同,则记入SystemDictionary;
  3. 代码中出现类型,结果同2。

测试代码:
基础的测试类如下,后面会用到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
JavaLoaded.java、ClassForName.java、LoadClass.java

public class JavaLoaded {
public void jm() {
System.out.println(this.getClass().getClassLoader() + " loaded JavaLoaded");
}
}

public class ClassForName {
public void cm() {
System.out.println(this.getClass().getClassLoader() + " loaded ClassForName");
}
}

public class LoadClass {
public void lm() {
System.out.println(this.getClass().getClassLoader() + " loaded 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
66
67
/*********************************************
*
* User : fengxiu.zk
* Date : 19-7-5 下午3:14
* Intro : 模拟系统字典构建一个带缓存的类加载器
*
***********************************************/
public class CachedClassLoader extends URLClassLoader {

private final Map<String, Class<?>> CACHE =
new ConcurrentHashMap<String, Class<?>>();

private String name;

public CachedClassLoader(URL[] urls) {
super(urls, null);
}

/**
* 根据类名加载制定的类,如果已经在缓存中存在,则直接返回
*
* @param name
* 类的全限定名
* @return
* @throws ClassNotFoundException
*/
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clazz = CACHE.get(name);
if (clazz != null) {
return clazz;
}
return super.loadClass(name);
}

/**
* 将class类名和实例放到cache中
*
* @param name
* @param clazz
*/
public final void injectClass(String name, Class<?> clazz) {
if (name != null && clazz != null) {
CACHE.put(name, clazz);
}
}

/**
* 获取加载类的class
*
* @param name
* 类的全限定名
* @return
*/
public Class<?> findLoadedClass1(final String name) {
return this.findLoadedClass(name);
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return name;
}
}

这个类只是简单的重写了下loadClass方法,先去缓存中查找是否已有类加载器加载过这个类,如果加载了,则不重新加载类。注意这里只是简单的demo,所以不要去纠结这个类加载器加载类会出现的一些问题。

测试类:

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
66
67
68
69
public class Main {

public static void main(String[] args) throws Exception {

// Method method = Main.class.getMethod("m");
// method.invoke(new Main());
Main main = new Main();
main.loadedClass();
}

public void loadedClass() throws Exception {
String s = "studydemo-1.0-SNAPSHOT.jar";
File file = new File(s);

CachedClassLoader j1 = new CachedClassLoader(new URL[]{file.toURI().toURL()});
j1.setName("1#");

CachedClassLoader j2 = new CachedClassLoader(new URL[]{file.toURI().toURL()});
j2.setName("2#");


j1.injectClass("club.zhangke.classLoaderStudy.JavaLoaded",
j2.loadClass("club.zhangke.classLoaderStudy.JavaLoaded"));

j1.injectClass("club.zhangke.classLoaderStudy.ClassForName",
j2.loadClass("club.zhangke.classLoaderStudy.ClassForName"));

j1.injectClass("club.zhangke.classLoaderStudy.LoadClass",
j2.loadClass("club.zhangke.classLoaderStudy.LoadClass"));

Class<?> a1 = j1.loadClass("club.zhangke.classLoaderStudy.Main");
Method aM = a1.getMethod("m", new Class<?>[]{});
aM.invoke(a1.newInstance(), new Object[]{});


System.out.println("1# findLoaded.");
System.out.println(j1.findLoadedClass1("club.zhangke.classLoaderStudy.JavaLoaded")); // √
System.out.println(j1.findLoadedClass1("club.zhangke.classLoaderStudy.ClassForName")); // √
System.out.println(j1.findLoadedClass1("club.zhangke.classLoaderStudy.LoadClass")); // x


System.out.println("2# findLoaded.");
System.out.println(j2.findLoadedClass1("club.zhangke.classLoaderStudy.JavaLoaded")); // √
System.out.println(j2.findLoadedClass1("club.zhangke.classLoaderStudy.ClassForName")); // √
System.out.println(j2.findLoadedClass1("club.zhangke.classLoaderStudy.LoadClass")); // √

}

/**

m方法中,分别使用JavaLoaded、ClassForName和LoadClass指代Java代码出现类型的默认加载方式、通过Class.forName进行加载的方式以及使用ClassLoader.loadClass进行加载类型的方式。
*/
public void m() throws Exception {
System.out.println(this.getClass().getClassLoader() + " loaded Main");

// Java loaded
JavaLoaded javaLoaded = new JavaLoaded();
javaLoaded.jm();

// Class.forName
Class<?> classForName = Class.forName("club.zhangke.classLoaderStudy.ClassForName");
classForName.getMethod("cm", new Class[]{}).invoke(classForName.newInstance(), new Object[]{});

// ClassLoader.loadClass
Class<?> loadClass = Main.class.getClassLoader().loadClass("club.zhangke.classLoaderStudy.LoadClass");
loadClass.getMethod("lm", new Class[]{}).invoke(loadClass.newInstance(), new Object[]{});
}
}

该测试用例构建了两个CachedClassLoader,也就是1#和2#,用2#加载了JavaLoaded、ClassForName和LoadClass,并将其注入给1#,这样1#在进行loadClass时,将会优先命中缓存中2#加载的以上3个类。

调用Main之后,在依次查询1#和2#的findLoadedClass方法。 输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1# loaded Main
2# loaded JavaLoaded
2# loaded ClassForName
2# loaded LoadClass

1# findLoaded.
class com.murdock.book.jarviewer.trap.JavaLoaded
class com.murdock.book.jarviewer.trap.ClassForName
null

2# findLoaded.
class com.murdock.book.jarviewer.trap.JavaLoaded
class com.murdock.book.jarviewer.trap.ClassForNameruxia
class com.murdock.book.jarviewer.trap.LoadClass

首先从上面的m函数的运行结果可以看出,m函数使用的class都是由2#加载的,至于为什么JarLoad这个类为什么还是2#加载的,这个需要看CacheClassLoader里面的具体代码就可以明白,这里就不具体解释。
可以看到JavaLoaded、ClassForName和LoadClass都是2#所加载,因此调用2#的findLoadedClass均能够找到这些Class,这对应于情况1。

下面为什么JavaLoaded和ClassForName为什么还是能够被发现,具体原因如下。
1#没有加载3个类,但是由于JavaLoaded类型出现在Main中,而Main是由1#加载,因此1#找寻不到JavaLoaded,则由1#.loadClass进行加载,结果委派给了2#成功加载。但是由于1#不等于2#,所以在SystemDictionary中会增加一条记录,这使得调用1#.findLoadedClass也能找到JavaLoaded,这对应与情况3。

ClassForName类似于JavaLoaded,对应于情况2。

而直接调用1#.loadClass(“LoadClass”),只是相当于一个简单java方法调用,返回了LoadClass的类型实例,但是没有在System新增记录,因此调用1#.findLoadedClass(“LoadClass”)是无法找到LoadClass的。