要自定义自己的类加载器来加载类,需要先对类加载器和类加载机制有一些基本的了解。
1、类加载器
类加载器ClassLoader的作用有两个:
①是用于将class文件加载到JVM。
②是用于判断JVM运行时两个类是否相等。
2、类加载的时机
类的加载可分为隐式加载和显示加载。
隐式加载
隐式加载包括以下几种情况:
- 遇到new(new 一个实例对象的时候)、getstatic(获取一个类的静态字段的时候)、putstatic(设置一个类的静态字段的时候)、invokestatic(调用一个类的静态方法的时候)这4条字节码指令时。
- 对类进行反射调用时。
- 初始化一个类时,如果父类还没有初始化,则先加载其父类并初始化(但是初始化接口时,不要求先初始化父接口)
- 虚拟机启动时,需要指定一个包含main函数的主类,优先加载并初始化这个主类。
显式加载
显示加载包含以下几种情况:
- 通过Class.forName()加载
- 通过ClassLoader的loaderClass方法加载
- 通过ClassLoader的findClass方法
3、Class.forName()加载类
Class.forName()和ClassLoader都可以对类进行加载。ClassLoader就是遵循双亲委派模型最终调用启动类加载器的类加载器,实现的功能是“通过一个类的全限定名来获取描述此类的二进制字节流”,获取到二进制流后放到JVM中。Class.forName()方法实际上也是调用的CLassLoader来实现的。
先看看Class.forName()的源码:
/** * 参数解释: * 1、className:要加载的类名 * 2、true:class被加载后是否要被初始化。初始化即执行static的代码(静态代码) * 3、caller:指定类加载器 */ public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
可以看出来最后正在实现forName()方法的是forName0这一个native方法。而使用forName0需要传递的参数之一,就是ClassLoader类加载器。因此可以知道Class.forName()本质还是使用classloader来进行类的加载的。
4、使用ClassLoader加载类
ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。
loadClass() 方法是加载目标类的入口,它首先会查找当前ClassLoader以及它的父类classloader里面是否已经加载了目标类,如果没有找到就会让父类Classloader尝试加载,如果父类classloader都加载不了,就会调用findClass()让自定义加载器自己来加载目标类。这实际上就是双亲委派机制的原理。
看一下loadClass()的源码:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded //查看类是否已被加载,findLoadedClass最后调用native方法findLoadedClass0 Class<?> c = findLoadedClass(name); //还未加载 if (c == null) { long t0 = System.nanoTime(); try { //若有父类加载器,则调用其loadClass(),请求父类进行加载 if (parent != null) { c = parent.loadClass(name, false); } else { //若父类加载器为null,说明父类为BootstrapClassLoader(该类加载器无法被Java程序直接使用,用null代替即可),请求它来加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader //加载失败,抛出ClassNotFoundException异常 } //父类无法加载,调用findClass方法,尝试自己加载这个类 //注意:在这个findClass方法中,目前只是抛出一个异常,没有任何进行类加载的动作 //因此,想要自己进行类加载,就要重写findClass()方法。 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; } }
ClassLoader 的 findClass() 方法是需要子类来覆盖重写的,不同的加载器将使用不同的逻辑来获取目标类的字节码。得到字节码之后会调用 defineClass() 方法将字节码转换成 Class 对象。
findClass()的源码如下:
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); //仅抛出异常 }
可见,ClassLoader的findClass()方法是没有具体实现的,如果要自定义类加载器,就需要重写findClass()方法,并且配合defineClass() 方法一起使用,defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象。
以上就是CLassLoader进行类加载的简单流程。
虽然Class.forName()方法本质上还是使用Classloader来进行类的加载的,但它和使用Classloader来进行类加载依然有着区别:
①Class.forName()方法除了将类的字节码加载到jvm中之外,还会执行类中的static块,即会导致类的初始化。Class.forName(name, initialize, loader)带参方法也可以指定是否进行初始化,执行静态块。
②ClassLoader只是将类的字节码加载到jvm中,不会执行static中的内容,即不会进行类加载,只有在newInstance才会去执行static块。
5、自定义类加载器
我们知道,除了BootstrapClassLoader是由C/C++实现的,其他的类加载器都是ClassLoader的子类。所以如果我们想实现自定义的类加载器,首先要继承ClassLoader。
根据我们前面的分析,ClassLoader进行类加载的核心实现就在loadClass()方法中。再根据loadClass()方法的源码,我们可以知道有两种方式来实现自定义的类加载,分别如下:
①如果不想打破双亲委派机制,那么只需要重写findClass方法。
②如果想要打破双亲委派机制,那么就需要重写整个loadClass方法。
如果没有特殊要求,Java官方推荐重写findClass方法,而不是重写整个loadClass方法。这样既让我们能够按照自己的意愿加载类,也能保证自定义的类加载器符合双亲委派机制。
明确了如何实现,我们只需要两步就可以实现自定义的类加载器:第一步是继承classloader,第二步是重写findClass方法。
不过由于在findClass()内需要调用defineClass()方法将字节数组转换成Class类对象,因此要先对输入的class文件做一些处理,使其变为字节数组。
实现自定义的类加载器:
public class MyClassLoader extends ClassLoader{ //默认ApplicationClassLoader为父类加载器 public MyClassLoader(){ super(); } //加载类的路径 private String path = ""; //重写findClass,调用defineClass,将代表类的字节码数组转换为Class对象 @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] dataByte = new byte[0]; try { dataByte = ClassDataByByte(name); } catch (IOException e) { e.printStackTrace(); } return this.defineClass(name, dataByte, 0, dataByte.length); } //读取Class文件作为二进制流放入byte数组, findClass内部需要加载字节码文件的byte数组 private byte[] ClassDataByByte(String name) throws IOException { InputStream is = null; byte[] data = null; ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream(); name = name.replace(".", "/"); // 为了定位class文件的位置,将包名的.替换为/ is = new FileInputStream(new File(path + name + ".class")); int c = 0; while (-1 != (c = is.read())) { //读取class文件,并写入byte数组输出流 arrayOutputStream.write(c); } data = arrayOutputStream.toByteArray(); //将输出流中的字节码转换为byte数组 is.close(); arrayOutputStream.close(); return data; } }
使用自定义的类加载器:
public static void main(String[] args) { MyClassLoader myClassLoader = new MyClassLoader(); Class<?> clazz = myClassLoader.loadClass("com.fengjian.www.MyClassLoader"); clazz.newInstance(); }
由于能力有限,可能存在错误,感谢指出。以上内容为本人在学习过程中所做的笔记。参考的书籍、文章或博客如下:
[1]Mr羽墨青衫.深入分析Java ClassLoader原理.知乎.https://zhuanlan.zhihu.com/p/81759029
[2]愚公要移山.学了这么久的java反射机制,你知道class.forName和classloader的区别吗?.知乎.https://zhuanlan.zhihu.com/p/101114197