所有自定义的类加载器都是ClassLoader的直接或间接子类,此类中并没有抽象方法,但是有findClass方法,这个一定要实现,不然会抛异常:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
自定义类加载器:
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class MyClassLoader extends ClassLoader {
//自定义默认的class存放路径
private final static Path DEFAULT_CLASS_DIR = Paths.get("E:","classloader1");
private final Path classDir;
//使用默认的class路径
public MyClassLoader(){
super();
this.classDir = DEFAULT_CLASS_DIR;
}
//允许传入指定路径的class路径
public MyClassLoader(String classDir){
super();
this.classDir = Paths.get(classDir);
}
//指定class路径的同时,指定父类加载器
public MyClassLoader(String classDir,ClassLoader parent){
super(parent);
this.classDir = Paths.get(classDir);
}
//重写父类方法,这是至关重要的步骤
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//读取class的二进制数据
byte[] classBytes = this.readClassBytes(name);
if(null == classBytes || classBytes.length == 0){
throw new ClassNotFoundException("can not load the class "+name);
}
//调用defineclass方法定义class
return this.defineClass(name,classBytes,0,classBytes.length);
}
//将class文件读入内存
private byte[] readClassBytes(String name) throws ClassNotFoundException {
//将包名分隔符转换为文件路径分隔符
String classPath = name.replace(".", "/");
Path classFullPath = classDir.resolve(Paths.get(classPath + ".class"));
if(!classFullPath.toFile().exists()){
throw new ClassNotFoundException("the class "+name+" not found");
}
try(ByteArrayOutputStream baos = new ByteArrayOutputStream()){
Files.copy(classFullPath,baos);
return baos.toByteArray();
}catch (IOException e){
throw new ClassNotFoundException("load the class "+name+" occur error",e);
}
}
@Override
public String toString() {
return "my classloader";
}
}
此函数包括三个构造函数,第一个使用默认的文件路径,第二个允许外部指定一个特定的磁盘目录,第三个还可以指定该类加载类的父加载器。
这里需要强调一个defineClass方法,该方法的完整描述是
defineClass(String name, byte[] b, int off, int len)
其中第一个是要定义类的名字,一般与findClass方法中的类名保持一致即可,第二个是class文件的二进制字节数组,第三个是字节数组的偏移量,第四个是从偏移量开始读取多长的byte数据。在类的加载过程中,第一个阶段的加载主要是获取class的字节流信息,而这里在将字节数组传递后还指定了偏移量和读取长度,这是为什么呢?原因就是因为class字节数组不一定是从一个class文件中获取的,有可能来自网络,也有可能是用编程的方式写入的,所有说,一个字节数组有可能存储多个class的字节信息。
下面写一个简单的程序,使用自定义的加载器进行加载
public class HelloWorld {
static {
System.out.println("HelloWorld class is initialized");
}
public String welcome() {
return "hello world";
}
}
Java文件编译后,将class文件复制到自定义的类加载器中设置的目录下,同时在程序运行的class patch中删除掉HelloWorld这个class,IDE测试的话,对应的Java类也要一并删除,否则测试类将会被系统类加载器加载。
编写测试类:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MyClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//声明自定义的class loader
MyClassLoader classLoader = new MyClassLoader();
Class<?> aClass = classLoader.loadClass("thread.classloader.HelloWorld");
System.out.println(aClass.getClassLoader());
System.out.println(aClass.getClassLoader().getParent());
//注释此代码后,静态代码块没有输出,因为类加载器loadclass并不会导致类的主动初始化
Object hello = aClass.newInstance();
System.out.println(hello);
Method welcome = aClass.getMethod("welcome");
String result = (String) welcome.invoke(hello);
System.out.println(result);
}
}
注释中的信息说明,使用类加载器loadClass并不会导致类的主动初始化,它只是执行了加载过程中的加载阶段而已。
思考:如何不删除HelloWorld.java文件又可以使用自定义类加载器加载呢?
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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;
}
}
这是jdk中的loadClass源码,分析可知:
- 从当前类加载器的已加载类缓存中根据类的全路径名查询是否存在,存在即返回。
- 如果当前类存在父类加载器,则调用父类loadClass(name,false)方法对其进行加载。
- 如果当前类加载器不存在父类加载器,直接调用根类加载器进行加载。
- 如果当前类的所有父类加载器都没有成功加载class,尝试调用当前类加载器的findClass方法对其进行加载,该方法就是我们自定义加载器需要重写的方法。
- 最后如果类被成功加载,做一些性能数据的统计。
- 由于loadClass指定了resolve为false,所以不会进行连接阶段的继续执行,不会导致类的初始化。
解答:
第一种方式是绕过系统类加载器,直接将扩展类加载器作用MyClassLoader的父类加载器:
ClassLoader extClassLoader = MyClassLoaderTest.class.getClassLoader().getParent();
MyClassLoader loader = new MyClassLoader("E:\\classloader1", extClassLoader);
loader.loadClass("thread.classloader.HelloWorld");
首先获取到系统类加载器,然后再获取系统类加载器的父类加载器中扩展类加载器,使其成为MyClassLoader的父类加载器,这样,根加载器和扩展类加载器都无法对E:\\classloader1中的类文件进行加载,自然就交给MyClassLoader对HelloWorld进行加载了。
第二种方式是在构造MyClassLoader的时候指定其父类加载器为null:
MyClassLoader loader = new MyClassLoader("E:\\classloader1", null);
loader.loadClass("thread.classloader.HelloWorld");
当前类在没有父类加载器的情况下,会直接使用根加载器对该类进行加载,但是HelloWorld在根加载器的加载路径下是无法找到的,那么就只能交给当前类加载器进行加载了。