JVM基础(二) 实现自己的ClassLoader
阅读原文时间:2021年04月20日阅读:1

为何要花时间实现自己的ClassLoader

虽然人生的乐趣很大一部分来自于将时间花在有意思但是无意义的事情上,但是这件事绝对是有意思并且有意义的,有以下几个情景是值得我们花费时间实现自己的classLoader的:

  • 我们需要的类不一定存放在已经设置好的classPath下(有系统类加载器AppClassLoader加载的路径),对于自定义路径中的class类文件的加载,我们需要自己的ClassLoader
  • 有时我们不一定是从类文件中读取类,可能是从网络的输入流中读取类,这就需要做一些加密和解密操作,这就需要自己实现加载类的逻辑,当然其他的特殊处理也同样适用。
  • 可以定义类的实现机制,实现类的热部署,如OSGi中的bundle模块就是通过实现自己的ClassLoader实现的。

理解ClassLoader类的结构

加载class文件

ClassLoader的loadClass采用双亲委托型实现,因为我们实现的ClassLoader都继承于java.lang.ClassLoader类,父加载器都是AppClassLoader,所以在上层逻辑中依旧要保证该模型,所以一般不覆盖loadClass函数

protected synchronized Class<?> loadClass ( String name , boolean resolve ) throws ClassNotFoundException{
        //检查指定类是否被当前类加载器加载过
        Class c = findLoadedClass(name);
        if( c == null ){//如果没被加载过,委派给父加载器加载
            try{
                if( parent != null )
                    c = parent.loadClass(name,resolve);
                else 
                    c = findBootstrapClassOrNull(name);
            }catch ( ClassNotFoundException e ){
                //如果父加载器无法加载
            }
            if( c == null ){//父类不能加载,由当前的类加载器加载
                c = findClass(name);
            }
        }
        if( resolve ){//如果要求立即链接,那么加载完类直接链接
            resolveClass();
        }
        //将加载过这个类对象直接返回
        return c;
    }

从上面的代码中,我们可以看到在父加载器不能完成加载任务时,会调用findClass(name)函数,这个就是我们自己实现的ClassLoader的查找类文件的规则,所以在继承后,我们只需要覆盖findClass()这个函数,实现我们在本加载器中的查找逻辑,而且还不会破坏双亲委托模型

加载资源文件(URL)

我们有时会用Class.getResource():URL来获取相应的资源文件。如果仅仅使用上面的ClassLoader是找不到这个资源的,相应的返回值为null。
下面我们来看Class.getResource()的源码:

public java.net.URL getResource(String name) {
        name = resolveName(name);//解析资源
        ClassLoader cl = getClassLoader();//获取到当前类的classLoader
        if (cl==null) {//如果为空,那么利用系统类加载器加载
            // A system class.
            return ClassLoader.getSystemResource(name);
        }
        //如果获取到classLoader,利用指定的classLoader加载资源
        return cl.getResource(name);
    }

我们发现Class.getResource()是通过委托给ClassLoader的getResource()实现的,所以我们来看classLoader对于资源文件的获取的具体实现如下:

    public URL getResource(String name) {
        URL url;
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            url = getBootstrapResource(name);
        }
        if (url == null) {
            url = findResource(name);//这里
        }
        return url;
    }

通过代码我们容易发现,也是双亲委派模型的实现,在不破坏模型的前提下,我们发现我们需要覆写的只是findResource(name)函数

综上

我们在创建自己的ClassLoader时只需要覆写findClass(name)和findResource()即可

例讲ClassLoader的实现

以下的实现均基于对于ClassLoader抽象类的继承(只给出对于findClass的覆写,因为常理上处理逻辑基本一致)

加载自定义路径下的class文件

package com.company;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.ByteBuffer;

/**
 * Created by liulin on 16-4-20.
 */
public class MyClassLoader extends ClassLoader {
    private String classpath;

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = getClassFile( name );
        byte[] classByte=null;
        try {
            classByte = getClassBytes(fileName);
        }catch( IOException e ){
            e.printStackTrace();
        }
        //利用自身的加载器加载类
        Class retClass = defineClass( null,classByte , 0 , classByte.length);
        if( retClass != null ) {
            System.out.println("由我加载");
            return retClass;
        }
        //System.out.println("非我加载");
        //在classPath中找不到类文件,委托给父加载器加载,父类会返回null,因为可加载的话在
        //委派的过程中就已经被加载了
        return super.findClass(name);
    }

    /***
     * 获取指定类文件的字节数组
     * @param name
     * @return 类文件的字节数组
     * @throws IOException
     */
    private  byte [] getClassBytes ( String name ) throws IOException{
        FileInputStream fileInput = new FileInputStream(name);
        FileChannel channel = fileInput.getChannel();
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        WritableByteChannel byteChannel = Channels.newChannel(output);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        try {
            int flag;
            while ((flag = channel.read(buffer)) != -1) {
                if (flag == 0) break;
                //将buffer写入byteChannel
                buffer.flip();
                byteChannel.write(buffer);
                buffer.clear();
            }
        }catch ( IOException e ){
            System.out.println("can't read!");
            throw e;
        }
        fileInput.close();
        channel.close();
        byteChannel.close();
        return output.toByteArray();
    }

    /***
     * 获取当前操作系统下的类文件合法路径
     * @param name
     * @return 合法的路径文件名
     */
    private String getClassFile ( String name ){
        //利用StringBuilder将包形式的类名转化为Unix形式的路径
        StringBuilder sb = new StringBuilder(classpath);
        sb.append("/")
                .append ( name.replace('.','/'))
                .append(".class");
        return sb.toString();
    }

    public static void main ( String [] args ) throws ClassNotFoundException {
        MyClassLoader myClassLoader = new MyClassLoader("/home/liulin/byj");
        try {
            myClassLoader.loadClass("java.io.InputStream");
            myClassLoader.loadClass("TestServer");
            myClassLoader.loadClass("noClass");
        }catch ( ClassNotFoundException e ){
            e.printStackTrace();
        }
    }
}

结果如下:

从结果我们看,因为我们加载的类的父加载器是系统加载器,所以调用双亲委托的loadClass,会直接加载掉java.io.InputStream类,只有在加载双亲中没有的TestServer类,才会用到我们自己的findClass加载逻辑加载指定路径下的类文件,满足双亲委派模型具体前面已经讲述过,不再赘述
热部署和加密解密的ClassLoader实现,大同小异。只是findClass的逻辑发生改变而已