自定义类加载器

先上个图:

classLoader.jpg

上图中的粗箭头表示了不同类加载器之间的关系(注意这里是组合关系并非继承关系):

名义上,引导类加载器是爷爷,是个 native class,用 C++ 写的,并无父亲(正如大圣是从石头里蹦出来的一样);其他加载器是用 Java 写的,扩展类加载器是爸爸;然后应用程序类加载器与咋们同辈,最后自定义的类加载器是咋程序员自己造的儿子。这里千万别搞混了,他们之间的关系并不是继承,只是为了实现双亲委派机制,而定义的逻辑名义上的族谱关系,真不是血亲哈!

刚才提到双亲委派机制,是用来保证 Java 核心类库加载时的类型安全性的。比如程序员自己定义了某个与 Java核心类库中 java.lang.String 同包同名的类,那么当我们不使用自定义的,且已摆脱双亲委脱机制的类加载器加载这个同包同名类时,首先会让爷爷,即引导类加载器去加载此类,如果引导类加载器不能加载,然后找爸爸,即扩展类加载器去加载,若爸爸再不能加载,则交给同辈的应用程序类加载器去加载,最后再不行,才轮到自定义加载器去加载。自定义的,与核心类库同包同名的类不可以被自定义的类加载器加载。那么到底如何才能加载与核心类库同包同名的自定义类?这个问题虽然无脑,但值得无脑人士探究!

再次回到上图,图中已给出了不同的类加载器加载的特定类库。但图中所示并不全面,到底这些类加载器加载了哪些目录下的类库?我们还可以用下面的代码去查看:

package com.binroad.test;

//import java.util.Arrays;
//import java.util.List;

public class GetClassLoaderLoadingPath {

    public static void main(String[] args) {
        System.out.println("引导类加载器加载路径:");
        BCLLoadingPath();
        System.out.println("扩展类加载器加载路径:");
        ECLLoadingPath();
        System.out.println("应用程序类加载器加载路径:");
        ACLLoadingPath();
    }
    
    /**
     * bootstrap class loader loading path
     */
    public static void BCLLoadingPath() {
        String loadingPaths = System.getProperty("sun.boot.class.path");
//      List<String> pathsList = Arrays.asList(loadingPaths.split(";"));
//      for (String path : pathsList) {
//          System.out.println(path);
//      }
        String[] pathsArr = loadingPaths.split(";");
        for (String path : pathsArr) {
            System.out.println(path);
        }
    }
    
    /**
     * extensions class loader loading path
     */
    public static void ECLLoadingPath() {
        String loadingPaths = System.getProperty("java.ext.dirs");
        String[] pathsArr = loadingPaths.split(";");
        for (String path : pathsArr) {
            System.out.println(path);
        }
    }
    
    /**
     * application class loader loading path
     */
    public static void ACLLoadingPath() {
        String loadingPaths = System.getProperty("java.class.path");
        String[] pathsArr = loadingPaths.split(";");
        for (String path : pathsArr) {
            System.out.println(path);
        }
    }
}

运行结果:

引导类加载器加载路径:
C:\Program Files\Java\jdk1.8.0_221\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_221\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_221\jre\lib\sunrsasign.jar
C:\Program Files\Java\jdk1.8.0_221\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_221\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_221\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_221\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_221\jre\classes
扩展类加载器加载路径:
C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
应用程序类加载器加载路径:
D:\IdeaProjects\ClassLoading\bin

下面代码可以简单查看不同的类加载器:

package com.binroad.test;

import sun.net.spi.nameservice.dns.DNSNameService;

public class Demo1 {
    public static void main(String[] args) {
        System.out.println(ClassLoader.getSystemClassLoader());
        System.out.println(ClassLoader.getSystemClassLoader().getParent());
        System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent()); //null,实际是bootstrap class loader
        
//      System.out.println(System.getProperty("java.class.path"));
        
        System.out.println("--------我是分割线--------");
        
        String a = "Alei";
        System.out.println("String类的类加载器是:" + a.getClass().getClassLoader()); //这里打印null,实际上使用bootstrap class loader加载了java.lang.String
        System.out.println("DNSNameService类的类加载器是:" + DNSNameService.class.getClassLoader());
        System.out.println("Demo1s类的类加载器是:" + Demo1.class.getClassLoader());
    }
}

运行结果:

sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
null
--------我是分割线--------
String类的类加载器是:null
DNSNameService类的类加载器是:sun.misc.Launcher$ExtClassLoader@15db9742
Demo1类的类加载器是:sun.misc.Launcher$AppClassLoader@73d16e93

那么如何自定义一个遵循双亲委派机制的类加载器呢?首先,继承 ClassLoader,然后重写 findClass() 方法,需要实现(通过IO流等方法)将 .class 文件转化 byte[] 的功能,最后将此 byte[] 作为参数传递至继承于 ClassLoader 的 defineClass() 方法里,从而获得加载类的反射对象(即 Class 对象),将此对象作为返回值返回到调用自定义类加载器的 loadClass() 代码块里。

对 findClass() 的重写,在 ClassLoader 的源码里有简单示例:

class NetworkClassLoader extends ClassLoader {
    String host;
    int port;
    
    public Class findClass(String name) {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }
    
    private byte[] loadClassData(String name) {
    // load the class data from the connection
        
    }
}

然后我们再看看 ClassLoader 里的 findClass() 源码:

/**
     * Finds the class with the specified <a href="#name">binary name</a>.
     * This method should be overridden by class loader implementations that
     * follow the delegation model for loading classes, and will be invoked by
     * the {@link #loadClass <tt>loadClass</tt>} method after checking the
     * parent class loader for the requested class.  The default implementation
     * throws a <tt>ClassNotFoundException</tt>.
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
     *
     * @since  1.2
     */
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

ClassLoader 里的 findClass() 是个空方法,只抛出了一个 ClassNotFoundException 异常,并无具体的实现。这就要求我们在自定义的类加载器里重写 findClass(),完成类的加载。

下面代码中,我们自定义了一个文件系统类加载器 FileSystemClassLoader(遵循双亲委托机制):

package com.binroad.test;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
 * 自定义文件系统类加载器
 */
public class FileSystemClassLoader extends ClassLoader {
    private String rootDir;
    
    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        /**
         * 下面注释掉的代码是错误示范,真正委派给父辈加载的地方在loadClass()方法体里,
         * findClass()只需要获得.class文件的字节数组byte[],而后使用defineClass()返回一个Class对象便可
         * 这里即使在findClass()里没有委派给父辈去加载,但若不重写loadClass()方法,
         * 在loadClass()里,也首先会委派给父辈加载,最后父辈加载不了,
         * 才调用自身重写的findClass()方法,返回一个Class对象。
         * 双亲委派机制的更深层原理可自行研究源码。
         * loadClass()方法的调用是递归调用,这里先提以下两点:
         * 1、自定义类加载器若要遵行双亲委派机制,只用重写findClass()即可;
         * 2、自定义类加载器若要摆脱双亲委派机制,则要重写loadClass()和findClass()。
         */
//      Class<?> clz = findLoadedClass(name);
        //先查询有没有加载过该类,如果已经加载则直接返回已经加载好的类,否则加载新的类
//      if (clz != null) {
//          return clz;
//      } else {
//          ClassLoader parent = this.getParent(); //application class loader
//          try {
//              clz = parent.loadClass(name); //委派给父辈加载
//          } catch(Exception e) {
//              System.out.println(clz);
//              System.out.println("应用程序类加载器未加载到该类~");
//              System.out.println("下面将使用自定义类加载器加载该类……");
//          }
            
//          if (clz != null) {
//              return clz;
//          } else {
                Class<?> clz = null;
                byte[] classData = loadClassData(name);
                if (classData == null) {
                    throw new ClassNotFoundException();
                } else {
                    clz = defineClass(name, classData, 0, classData.length);
                }
//          }
//      }
        
        return clz;
    }
    
    private byte[] loadClassData(String className) {
        String path = rootDir + "/" + className.replace(".", "/") + ".class";
        byte[] datas = null;
        InputStream is = null;
        ByteArrayOutputStream baos = null;
        try {
            is = new FileInputStream(path);
            baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int readLen = -1;
            while ((readLen = is.read(buffer)) != -1) {
                baos.write(buffer, 0, readLen);
            }
            datas = baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return datas;
    }
}

我之前重写 findClass() 时,对双亲委派机制的理解产生了错误。在上面代码中,我将之前写的错误代码保留了下来并注释掉,以此在上面的多行注释里说明了问题。

在 Demo2 里测试自定义的文件系统类加载器:

package com.binroad.test;
/**
 * 测试自定义的FileSystemClassLoader
 */
public class Demo2 {
    public static void main(String[] args) throws Exception {
        FileSystemClassLoader classLoader = new FileSystemClassLoader("D:/TestClassLoader");
        FileSystemClassLoader cl = new FileSystemClassLoader("D:/TestClassLoader");
//      Class<?> clz = classLoader.loadClass("com.binroad.test.HelloWorld");
//      System.out.println(clz);
//      System.out.println(clz.hashCode());
        
        Class<?> c0 = classLoader.loadClass("com.binroad.test.HelloWorld");
        Class<?> c1 = classLoader.loadClass("com.binroad.test.HelloWorld");
        Class<?> c2 = cl.loadClass("com.binroad.test.HelloWorld");
        Class<?> c3 = cl.loadClass("java.lang.String"); //此时,遵循双亲委派机制,交给父辈类加载器来处理(首先交予bootstrap class loader)
        Class<?> c4 = cl.loadClass("com.binroad.test.Demo1");
        
        System.out.println(c0.hashCode());
        System.out.println(c1.hashCode());
        System.out.println(c2.hashCode()); //同一个类被不同的类加载器加载,JVM将认作不同的类(不同的类加载器对象,其命名空间不同,即使是同类的类加载器)
        System.out.println(c3);
        System.out.println(c2.getClassLoader()); //自定义类加载器
        System.out.println(c3.getClassLoader()); //引导类加载器
        System.out.println(c4.getClassLoader()); //应用程序(系统)类加载器
    }
}

运行结果:

1550089733
1550089733
865113938
class java.lang.String
com.binroad.test.FileSystemClassLoader@4e25154f
null
sun.misc.Launcher$AppClassLoader@73d16e93

当然,我们还可以在文件系统类加载器的基础上再写一个网络类加载器:

package com.binroad.test;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

/**
 * 自定义网络类加载器
 */
public class NetClassLoader extends ClassLoader {
    //例如:www.binroad.com/TestClassLoader/ com.binroad.test.User --> com/binroad/test/User.class
    private String rootUrl;
    
    public NetClassLoader(String rootUrl) {
        this.rootUrl = rootUrl;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> clz = null;
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            clz = defineClass(name, classData, 0, classData.length);
        }
        return clz;
    }
    
    private byte[] loadClassData(String className) {
        String path = rootUrl + "/" + className.replace(".", "/") + ".class";
        byte[] datas = null;
        InputStream is = null;
        ByteArrayOutputStream baos = null;
        try {
            URL url = new URL(path);
            is = url.openStream();
            baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int readLen = -1;
            while ((readLen = is.read(buffer)) != -1) {
                baos.write(buffer, 0, readLen);
            }
            datas = baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return datas;
    }
}

我们还可以对编译好的某个类的 .class 文件进行加密处理,然后自定义一个可以解密此 .class 文件的类加载器来加载该类:

简单的加密处理(这里只是做了简单的异或处理,当然还可以使用更加复杂的加密算法和更加可靠的工具类来实现加密):

package com.binroad.test;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class EncryptUtil {
    public static void main(String[] args) {
        encrypt("D:/TestClassLoader/com/binroad/test/HelloWorld.class", "D:/TestClassLoader/encrypt/com/binroad/test/HelloWorld.class");
    }
    
    public static void encrypt(String src, String dest) {
        InputStream is = null;
        OutputStream os = null;
        try {
            is = new FileInputStream(src);
            os = new FileOutputStream(dest);
            
            int readByte = -1;
            while ((readByte = is.read()) != -1) {
                os.write(readByte ^ 0xFF); //取反加密
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        
    }
}

可以解密的类加载器源码:

package com.binroad.test;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
 * 解密并加载加密后的.class字节码文件的类加载器
 */
public class DecryptClassLoader extends ClassLoader {
private String rootDir;
    
    public DecryptClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> clz = null;
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            clz = defineClass(name, classData, 0, classData.length);
        }
        return clz;
    }
    
    private byte[] loadClassData(String className) {
        String path = rootDir + "/" + className.replace(".", "/") + ".class";
        byte[] datas = null;
        InputStream is = null;
        ByteArrayOutputStream baos = null;
        try {
            is = new FileInputStream(path);
            baos = new ByteArrayOutputStream();
            /**
             * 在写到内存里时进行解密处理
             */
            int readByte = -1;
            while ((readByte = is.read()) != -1) {
                baos.write(readByte ^ 0xFF); //取反解密
            }
            datas = baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return datas;
    }
}

在 Demo3 里测试写好的自带解密功能的类加载器:

package com.binroad.test;

/**
 * 测试简单加密解密(取反)操作
 */
public class Demo3 {
    public static void main(String[] args) {
//      int a = 3; //00000011
//      System.out.println(Integer.toBinaryString(a ^ 0xFF)); //异或取反
        
        //加密后的.class文件,正常的类加载器无法加载,会报java.lang.ClassFormatError
//      FileSystemClassLoader classLoader = new FileSystemClassLoader("D:/TestClassLoader/encrypt");
//      try {
//          Class<?> clz = classLoader.loadClass("HelloWorld");
//      } catch (ClassNotFoundException e) {
//          e.printStackTrace();
//      }
        
        DecryptClassLoader classLoader = new DecryptClassLoader("D:/TestClassLoader/encrypt");
        try {
            Class<?> clz = classLoader.loadClass("com.binroad.test.HelloWorld");
            System.out.println(clz);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

class com.binroad.test.HelloWorld //加载成功

对于怎样自定义一个摆脱双亲委派机制的类加载器,以及指定线程上下文类加载器,我将在后续的文章中分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容