所有自定义的类加载器都是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源码,分析可知:

  1. 从当前类加载器的已加载类缓存中根据类的全路径名查询是否存在,存在即返回。
  2. 如果当前类存在父类加载器,则调用父类loadClass(name,false)方法对其进行加载。
  3. 如果当前类加载器不存在父类加载器,直接调用根类加载器进行加载。
  4. 如果当前类的所有父类加载器都没有成功加载class,尝试调用当前类加载器的findClass方法对其进行加载,该方法就是我们自定义加载器需要重写的方法。
  5. 最后如果类被成功加载,做一些性能数据的统计。
  6. 由于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在根加载器的加载路径下是无法找到的,那么就只能交给当前类加载器进行加载了。