37. 请你详细说说类加载流程,类加载机制及自定义类加载器 下
五、创建并使用自定义类加载器
1、自定义类加载分析
除了根类加载器,所有类加载器都是ClassLoader的子类。所以我们可以通过继承ClassLoader来实现自己的类加载器。
ClassLoader类有两个关键的方法:
protected Class loadClass(String name, boolean resolve):name为类名,resove如果为true,在加载时解析该类。
protected Class findClass(String name) :根据指定类名来查找类。
所以,如果要实现自定义类,可以重写这两个方法来实现。但推荐重写findClass方法,而不是重写loadClass方法,因为loadClass方法内部会调用findClass方法。
我们来看一下loadClass的源码
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)) { //第一步,先从缓存里查看是否已经加载 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //第二步,判断父加载器是否为null 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) { //第三步,如果前面都没有找到,就会调用findClass方法 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加载方法流程:
判断此类是否已经加载;
如果父加载器不为null,则使用父加载器进行加载;反之,使用根加载器进行加载;
如果前面都没加载成功,则使用findClass方法进行加载。
所以,为了不影响类的加载过程,我们重写findClass方法即可简单方便的实现自定义类加载。
2、实现自定义类加载器
基于以上分析,我们简单重写findClass方法进行自定义类加载。
public class Hello { public void test(String str){ System.out.println(str); } } public class MyClassloader extends ClassLoader { /** * 读取文件内容 * * @param fileName 文件名 * @return */ private byte[] getBytes(String fileName) throws IOException { File file = new File(fileName); long len = file.length(); byte[] raw = new byte[(int) len]; try (FileInputStream fin = new FileInputStream(file)) { //一次性读取Class文件的全部二进制数据 int read = fin.read(raw); if (read != len) { throw new IOException("无法读取全部文件"); } return raw; } } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class clazz = null; //将包路径的(.)替换为斜线(/) String fileStub = name.replace(".", "/"); String classFileName = fileStub + ".class"; File classFile = new File(classFileName); //如果Class文件存在,系统负责将该文件转换为Class对象 if (classFile.exists()) { try { //将Class文件的二进制数据读入数组 byte[] raw = getBytes(classFileName); //调用ClassLoader的defineClass方法将二进制数据转换为Class对象 clazz = defineClass(name, raw, 0, raw.length); } catch (IOException e) { e.printStackTrace(); } } //如果clazz为null,表明加载失败,抛出异常 if (null == clazz) { throw new ClassNotFoundException(name); } return clazz; } public static void main(String[] args) throws Exception { String classPath = "loader.Hello"; MyClassloader myClassloader = new MyClassloader(); Class<?> aClass = myClassloader.loadClass(classPath); Method main = aClass.getMethod("test", String.class); System.out.println(main); main.invoke(aClass.newInstance(), "Hello World"); } } //输出结果 //Hello World
ClassLoader还有一个重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的作用是将class的二进制数组转换为Calss对象。
此例子很简单,我写了一个Hello测试类,并且编译过后放在了当前路径下(大家可以在findClass中加入判断,如果没有此文件,可以尝试查找.java文件,并进行编译得到.class文件;或者判断.java文件的最后更新时间大于.class文件最后更新时间,再进行重新编译等逻辑)。
六、总结
本篇从类加载的三大阶段:加载、链接、初始化开始细说每个阶段的过程;详细讲解了JVM常用的类加载器的区别与联系,以及类加载机制流程,最后通过自定义的类加载器例子结束本篇。