除了在面试中遇到类的加载器的概率会高外,在实际的工作中很少接触。但是一个程序员想要成长为大牛就必须对一些 JVM 的底层设计有些了解。在此基础上我们阅读一些源码和框架会显得更轻松。
好了废话不多说,我们接着前面的文章,乘热打铁。来实现一个 Java 自定义类加载器吧。
要实现 Java 自定义的类加载器,我们需要继承 ClassLoader 。并且需要了解Java的双亲委派模型。
loadClass
loadClass默认实现如下:
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
再看看loadClass(String name, boolean resolve)函数:
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 } // :www.xttblog.com 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; } }
从上面代码可以明显看出,loadClass(String, boolean)
函数即实现了双亲委派模型!整个大致过程如下:
- 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
- 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
- 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
话句话说,如果自定义类加载器,就必须重写findClass方法!
findClass
findClass的默认实现如下:
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
可以看出,抽象类ClassLoader的findClass函数默认是抛出异常的。而前面我们知道,loadClass在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的findeClass函数,因此我们必须要在loadClass这个函数里面实现将一个指定类名称转换为Class对象.
如果是是读取一个指定的名称的类为字节数组的话,这很好办。但是如何将字节数组转为Class对象呢?很简单,Java提供了defineClass方法,通过这个方法,就可以把一个字节数组转为Class对象。
defineClass
defineClass主要的功能是:将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。如,假设class文件是加密过的,则需要解密后作为形参传入defineClass函数。
defineClass默认实现如下:
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError { return defineClass(name, b, off, len, null); }
函数调用过程
函数调用过程,如下图所示:
通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自ClassLoader类,从上面对loadClass方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:
package com.xttblog.classloader; import java.io.*; public class MyClassLoader extends ClassLoader { private String root; // :www.xttblog.com protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = loadClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] loadClassData(String className) { String fileName = root + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try { InputStream ins = new FileInputStream(fileName); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; int length = 0; while ((length = ins.read(buffer)) != -1) { baos.write(buffer, 0, length); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } public String getRoot() { return root; } public void setRoot(String root) { this.root = root; } public static void main(String[] args) { MyClassLoader classLoader = new MyClassLoader(); classLoader.setRoot("E://temp"); Class<?> testClass = null; try { testClass = classLoader.loadClass("com.xttblog.classloader.Test2"); Object object = testClass.newInstance(); System.out.println(object.getClass().getClassLoader()); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:
-
1、这里传递的文件名需要是类的全限定性名称,即
com.paddx.test.classloading.Test
格式的,因为 defineClass 方法是按这种格式进行处理的。 - 2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
-
3、这类Test 类本身可以被
AppClassLoader
类加载,因此我们不能把com/paddx/test/classloading/Test.class
放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader
加载,而不会通过我们自定义类加载器来加载。
参考资料
- 【深入Java虚拟机】之四:类加载机制
- JAVA类加载机制全解析
- Java虚拟机类加载机制
- 深入研究Java类加载机制
- Java 类加载机制详解
: » Java 自定义类加载器教程
原创文章,作者:kirin,如若转载,请注明出处:https://blog.ytso.com/251768.html