前言

前面 认识类加载器及双亲委派模型/ 中我们认识了类加载器,本文我们来自定义一个类加载器。

为什么需要自定义类加载器?

既然JDK已经有类加载器了,为什么还需要自定义类加载器呢?大概有以下几个原因:

  • 隔离加载类

    模块隔离,将类加载到不同的应用程序中。比如Tomcat这类web应用服务器,内部定义了好几种类加载器,用于隔离web应用服务器上不同的应用程序。

  • 扩展加载源

​ 还可以从数据库、网络或其他终端上加载类

  • 防止源码泄露

​ Java代码容易被编译和篡改,可以进行编译加密,类加载需要自定义还原加密字节码。

类加载器的调用过程

我们需要从ClassLoader的源码入手来看类加载的过程。

ClassLoader中的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
    protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

双亲委派的核心代码逻辑如下:

而findClass()默认的实现是抛出ClassNotFoundException异常:

还有一个方法我们需要注意的是defineClass(),它的作用是将字节数组转换成Class对象,源码如下:

整个类加载的流程如下图所示:

总结下这几个核心的方法:

loadClass:双亲委派的核心实现逻辑;

findClass:将class文件加载到内存中,是二进制的形式,最后得由defineClass来讲二进制文件转换成class文件。

自定义类加载器

所有用户自定义类加载器都应该继承ClassLoader类。 在自定义ClassLoader的子类是,我们通常有两种做法:

  • 重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
  • 重写findClass方法 (推荐)

代码实现

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
package com.jvm;

import java.io.*;

//自定义类加载器
public class MyClassLoader extends ClassLoader{


//磁盘上类的路径
private String codePath;

public MyClassLoader(ClassLoader parent, String codePath) {
super(parent);
this.codePath = codePath;
}

public MyClassLoader(String codePath) {
this.codePath = codePath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

BufferedInputStream bis=null;
ByteArrayOutputStream baos=null;

//完整的类名
String file = codePath+name+".class";
try {

//初始化输入流
bis = new BufferedInputStream(new FileInputStream(file));
//获取输出流
baos=new ByteArrayOutputStream();

int len;
byte[] data=new byte[1024];
while ((len=bis.read(data))!=-1){
baos.write(data,0,len);
}

//获取内存中的字节数组
byte[] bytes = baos.toByteArray();

//调用defineClass将字节数组转换成class实例
Class<?> clazz = defineClass(null, bytes, 0, bytes.length);
return clazz;

} catch (Exception e) {
e.printStackTrace();
}finally {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}

return null;
}
}

测试

在本地D盘的目录下放置一个class文件:

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyClassLoaderTest {

public static void main(String[] args) {
MyClassLoader myClassLoader = new MyClassLoader("D:/");
try {
Class<?> clazz = myClassLoader.loadClass("AddressTest");
//打印具体的类加载器,验证是否是由我们自己定义的类加载器加载的
System.out.println("测试字节码是由"+clazz.getClassLoader().getClass().getName()+"加载的。。");
Object o = clazz.newInstance();
System.out.println(o.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行结果:

可以看出我们使用自定义的类加载器加载了我们本地的一个class文件。

总结

双亲委派模型的实现核心方法是loadClass()、findClass()和defineClass(),其中loadClass()是核心逻辑,findClass()将文件加载到内存成为,defineClass()是将二进制文件转换为活的class文件,自定义类加载器也是从这三个核心方法入手的,我们通过重写findClass()方法来自定义了一个类加载器,成功加载了本地的一个class文件。