最近一段时间发现好多小伙伴对类加载器的认实还停留在很浅的一个层面所以我想特意出一系列关于java类加载的一些讲解
今天我们从最简单的将起,如何自定义一个类加载器
首先我们先来说说java中有哪几种类加载器
加上我们自定义类加载器一共四种, 其中启动类加载器是由 c++实现的 在java中用"null"来表示,除此之外,扩展类加载器,以及系统类加载器都是由java实现的,并且都直接或间接的继承了ClassLoader类,从上图 中可以看出现,我们自定义的类加载器也需要继承ClassLoader, 这里需要注意的是,在类加载器中所谓的父类加载器指的不是这个类加载器继承了哪个类,换句话说,在类加载器中的父子关系不是通过继承来体现的,而是通过ClassLoader类中的一个成员变量来体现的
另外java中的类加载器基于双亲委托模型,即 子类加载器去加载某个类的时候会先委托给父类加载器去加载,如果所有父类加载器加载不了才会自己加载,如果自己也加载不了那就报错了
注:这种模型可以破坏掉,比如Tomcat中就破坏了这种模型 有兴趣的可以了解一下,再比如java中的 线程上下文类加载器也在一定程度上破坏了这种模型(以后专门会讲)
了解一些基本概念后我们来自己实现一个自定义的类加载器
通过上面的分析我们知道 如果要自定义一个类加载器就需要继承ClassLoader类
我们先来看看ClassLoader类中的 loadClass方法
通过这个方法名我们也知道 这个方法是用来加载一个类的,接下来 我将这个方法的代码拷贝下来,并写上注释,能够让你们更好的理解
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);
// 等于 null 则代码没有被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
// 这时候会判断这个类加载的父类等不等于null 其实也就是判断当前类加载的父类加载器 是不是跟类加载器
if (parent != null) {
// 如果不是跟类加载器的话 会调用父类加载器的 loadClass方法,其实也就是这个方法,
c = parent.loadClass(name, false);
} else {
// 父类加载器等于 null 也就代表当前类加载器的父类加载器是根类加载器,然后会尝试用根类加载器去加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果上面所有的类加载器都尝试过加载了,但仍然该类还没有被加载,则调用 findClass方法
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(name) 方法
很明显,这个方法是交给子类去实现的, 那么我们现在自定义类加载器的思路就有了
首先继承 ClassLoader类
然后重写它的 findClass方法就行了
好我们先写代码
继承完后 我们现在应该想办法在这个方法中返回一个 Class类对象,那么又该怎么呢?
接着看ClassLoader类的代码,因为靠我们自己是生成不了Class对象的,
往下面的代码看 你会找到一个 defineClass(String name, byte[] b, int off, int len) 的方法
这个方法上面也介绍了,将一个字节数组转换成一个Class类实例
那么我们到这里思路就非常的清晰了,我们只要在findClass方法里将一个class文件的数据保存到一个byte数组里就可以了
那么下面就直接开始写代码了
public class MyClassLoader extends ClassLoader {
/**
* 当前自定义类加载器是加载哪个文件下的class
*/
private String classPath;
/**
* 当前操作系统的文件分隔符
*/
private char delimiter = File.separatorChar;
/**
* class类的扩展名
*/
private final String ext = ".class";
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String name) {
// 获取该类在文件系统中保存的格式 (即路径加文件名)
String fileName = classPath + delimiter + name.replace('.', delimiter) + ext;
// 创建这个文件的File对象
File file = new File(fileName);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (InputStream in = new FileInputStream(file)) {
int len;
byte[] b = new byte[1024 * 8];
while ((len = in.read(b)) != -1) {
bos.write(b, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
return bos.toByteArray();
}
public String getClassPath() {
return classPath;
}
public void setClassPath(String classPath) {
this.classPath = classPath;
}
}
这样我们的自定义类加载器就写好了
接下来来测试一下
创建一个类 我这里是放在 G:\com\itzijin 这个路径
注意:这里G:\后面的路径取决于你们的包名是什么。例如这个类的包名是 com.itzijin 所以我这类就要放在这个路径下
写完之后使用 javac 类名.java 给编译成class文件就行了
然后就是在程序中测试就可以了
到这里我们的自定义类加载器就讲完了
比较遗憾的是,在这篇博客里没有带你们直观的去看下类加载器的双亲委托模型,所以在这里想给告诉你们的是,如果不了解双亲委托模型的可以先去了解一些,并通过自己去写一个自定义类加载器 去直观的看下类加载器的双亲委托模型