引言
上一篇文章我们介绍了类加载器的基础知识和双亲委派模型,这一章我们开发自己的类加载器,然后根据源码解析深度解析系统类加载器。
自定义文件系统类加载器
下面代码展示了一个自定义的类加载器,该类加载器用来加载存储在文件系统上的Java字节代码。
public class FileSystemClassLoader extends ClassLoader {
private final String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if(classData == null){
throw new ClassNotFoundException();
}else {
return defineClass(name,classData,0,classData.length);
}
}
private byte[] getClassData(String className){
String path = classNameToPath(className);
try{
InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumberRead = 0;
while((bytesNumberRead = is.read(buffer)) != -1){
baos.write(buffer,0,bytesNumberRead);
}
return baos.toByteArray();
}catch (IOException e){
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className){
return rootDir + File.separatorChar + className.replace('.',File.separatorChar) + ".class";
}
public static void main(String[] args) throws ClassNotFoundException {
FileSystemClassLoader classLoader = new FileSystemClassLoader("/Users/cuihualong/develop/code/java/concurrency/target/classes");
System.out.println(classLoader.getParent());
Class<?> aClass = classLoader.loadClass("person.andy.concurrency.classload.SimpleObject");
System.out.println(aClass.getClassLoader());
System.out.println(aClass);
SimpleObject simpleObject = new SimpleObject();
System.out.println(aClass.isInstance(simpleObject));
}
}
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
class person.andy.concurrency.classload.SimpleObject
true
实现的逻辑很简单,就是重写了findClass方法,findClass方法主要是得到代表class文件的二进制字节数组,然后调用defineClass方法来得到Class实例。
前一篇文章中我们说过,在自定义类加载器时,推荐重写findClass方法而不是loadClass方法,因为类加载器的双亲委派模型是在loadClass方法中实现的,如果重写loadClass方法,可能导致该模型被破坏。
上面的FileSystemClassLoader没有破坏双亲委派模型,也正是这个原因,我们可以发现一些值得注意的问题:
在main方法中,我们使用自定义的类加载器加载了位于当前类ClassPath下的名为SimpleObject的class文件,得到了对应的Class的实例,然后输出了这个Class的类加载器,和FileSystemClassLoader的父类加载器,结果居然都是系统类加载器。自定义类加载器的父类加载器是系统类加载器这个不难理解,因为上一篇文章中我们有过介绍,但是为什么我们加载出来的类的getClassLoader方法返回的也是系统类加载器呢?这个问题涉及两个概念,一个就是双亲委派模型,一个就是系统类加载器的加载机制。
首先说双亲委派模型,由于我们在自定义的类加载器中只重写了findClass方法而不是loadClass方法,所以这个类加载器是遵循双亲委派模型的,那么当加载器一个类时,首先会请求父类加载器去完成这个请求,也就是系统类加载器,当然,系统类加载器同样遵循双亲委派模型,会将请求继续向上代理,但是ExtClassLoader和bootstrapClassLoader都只加载固定位置的类,只有系统类加载器才会加载ClassPath下的类,所以,最终是由系统类加载器来加载这个类的。
如果这个类是由系统类加载器进行加载的,那么aClass.isInstance(simpleObject)这段代码返回true也就很正常了。
现在,先看一个修改后的自定义类的实现:
package person.andy.concurrency.classload;
import java.io.*;
public class FileSystemClassLoader1 extends ClassLoader {
private String rootDir;
public FileSystemClassLoader1(String rootDir) {
this.rootDir = rootDir;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if(null == classData){
throw new ClassNotFoundException();
}
return defineClass(name,classData,0,classData.length);
}
private byte[] getClassData(String className){
String path = classNameToPath(className);
try{
InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumberRead = 0;
while((bytesNumberRead = is.read(buffer)) != -1){
baos.write(buffer,0,bytesNumberRead);
}
return baos.toByteArray();
}catch (IOException e){
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className){
return rootDir + File.separatorChar + className.replace('.',File.separatorChar) + ".class";
}
public static void main(String[] args) throws ClassNotFoundException {
FileSystemClassLoader1 classLoader1 = new FileSystemClassLoader1("/Users/cuihualong/develop/code/java/concurrency/target/classes");
System.out.println(classLoader1.getParent());
Class<?> aClass = classLoader1.loadClass("person.andy.concurrency.classload.SimpleObject");
System.out.println(aClass.getClassLoader());
SimpleObject simpleObject = new SimpleObject();
System.out.println(aClass.isInstance(simpleObject));
}
}
我们没有重写findClass方法,而是重写了loadClass方法,并且loadClass方法中没有调用父类去进行类的加载,而是直接获取了类的二进制字节数组然后通过defineClass去创建Class实例。如果你在想这个main方法会输出什么结果,那你就错了,因为会直接报错:
sun.misc.Launcher$AppClassLoader@18b4aac2
java.io.FileNotFoundException: /Users/cuihualong/develop/code/java/concurrency/target/classes/java/lang/Object.class (No such file or directory)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at person.andy.concurrency.classload.FileSystemClassLoader1.getClassData(FileSystemClassLoader1.java:24)
at person.andy.concurrency.classload.FileSystemClassLoader1.loadClass(FileSystemClassLoader1.java:14)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at person.andy.concurrency.classload.FileSystemClassLoader1.loadClass(FileSystemClassLoader1.java:18)
at person.andy.concurrency.classload.FileSystemClassLoader1.main(FileSystemClassLoader1.java:45)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at person.andy.concurrency.classload.FileSystemClassLoader1.loadClass(FileSystemClassLoader1.java:18)
at person.andy.concurrency.classload.FileSystemClassLoader1.main(FileSystemClassLoader1.java:45)
Caused by: java.lang.ClassNotFoundException
at person.andy.concurrency.classload.FileSystemClassLoader1.loadClass(FileSystemClassLoader1.java:16)
... 5 more
第一行确实输出了自定义类加载器的父类加载器,就是AppClassLoader,但是调用loadClass方法就报错了,而且从报错信息我们可以看出,是我们定义的classLoader去加载java.lang.object这个类了,但是在我们的代码逻辑中,肯定是不能在rootDir下面找到这个类的,所以报错。从这里我们可以知道,当加载一个类时,会去加载这个类中用到的类,并且加载的动作是由同一个类加载器完成的。
我们简单的修改一下loadClass的实现,当name是java开头的时候,我们就让父类加载器去加载它。
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if(name.startsWith("java")){
return getParent().loadClass(name);
}
byte[] classData = getClassData(name);
if(null == classData){
throw new ClassNotFoundException();
}
return defineClass(name,classData,0,classData.length);
}
再次运行,就可以正常输出结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
person.andy.concurrency.classload.FileSystemClassLoader1@511d50c0
false
我们看到,这次aClass.getClassLoader()输出的就是我们自定义的FileSystemClassLoader1而不是系统类加载器了,并且isInstance输出了false。
通过在子类中重写loadClass方法,我们能够破坏双亲委派模型。