Java自定义类加载器实现-原理分析
阅读原文时间:2021年04月20日阅读:1

Java自定义类加载器实现-原理分析

  这篇文章主要聊一下如何自定义Java的类加载器,关于Java的类加载机制,可以参考Java的类加载机制双亲委派模型的文章:https://blog.csdn.net/strive_or_die/article/details/98664519

为什么要自定义

  1. 需要将我们的class文件放到自定义的classpath下,这时我们可以通过自己定义的类加载器实现加载指定目录下的class;其实这种情况能用到的情况并不多,因为我们可以通过java提供的指定加载目录实现我们需求。
  2. 某些类需要特殊的操作,例如该类的字节码数据加密过,这就需要做一些解密操作,这就需要自己实现加载类的逻辑,当然其他的特殊处理也同样适用。
  3. 热部署,例如Tomcat的jsp编译后的class。

怎么自定义

  如果我们只是想实现自己的类加载器,但是没有打破双亲委派机制的,那么我们只需要继承自ClassLoader即可,然后实现它的com.learn.classloader.custom.MyClassLoader#findClass方法。为什么实现该方法即可呢?先来看一下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
                }
                //如果父加载器都没有加载到指定的类,则当前类执行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;
        }
    }

  这个方法的基本步骤,就是先交给父加载器去加载类,如果父加载器都没有加载到指定的类,则当前类执行findClass()尝试查找。因此我们只需要继承ClassLoader,然后实现findClass()方法即可。

实现自定义类加载器

  首先定义了自己的类加载器如下:

/**
 * 自定义类加载器
 */
public class MyClassLoader extends ClassLoader {

    private String rootDir;/*自定义类加载的查找class的路径*/

    /*指定该类加载器会查找的rootDir目录,和父加载器*/
    public MyClassLoader(String rootDir, ClassLoader parent){
        super(parent);
        this.rootDir = rootDir;

    }

    /*指定该类加载器会查找的rootDir目录*/
    public MyClassLoader(String rootDir){
        this.rootDir = rootDir;
    }


    /**
     * 自定义自己的类加载器,如没有要改变类加载顺序的必要的话,则重写findClass方法,因为这个方法是JDK预留了给我们实现的,
     * 否则就需要修改loadClass的实现。
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //<1>.根据类的全路径(包含包名)类名和放置的目录确定类文件的路径
        String className = name.substring(name.lastIndexOf(".")+1)+ ".class";
        String classFile = rootDir + File.separator + className;
        FileInputStream fileInputStream = null;
        byte[] classData = null;
        try {
            //<2>.将class文件读取到字节数组
            fileInputStream = new FileInputStream(new File(classFile));
            classData = new byte[fileInputStream.available()];
            fileInputStream.read(classData,0,classData.length);
            //<3>.将字节数据创建一个class
            return defineClass(name,classData,0,classData.length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (fileInputStream != null){
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //<4>如果父类加载器不是自定义的,上面的加载过程没加载成功,则此调用会throw ClassNotFoundException
        return super.findClass(name);
    }
}

  定义一个用来测试的类Person

public class Person {
    private String name = "Coco";
    private int age = 23;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

  测试类,rootDir定义为了"D:/class/",所以MyClassLoader会从该目录下,加载指定的类。

public class CustomClassLoaderTest {
    /*定义了一个目录存放class文件,这个其实可以修改为可配置参数*/
    private static final String rootDir = "D:/class/";

    public static void main(String[] args) throws Exception {
         /*<1> 从指定的目录下查找对应的class文件,进行加载,然后创建该对象,如果加载存在则加载成功,则类加载器应为MyClassLoader*/
        MyClassLoader classLoader = new MyClassLoader(rootDir);
        Class c = classLoader.loadClass("com.learn.classloader.custom.Person");
        Object object = c.newInstance();
        Method getNameMethod = c.getMethod("getName");
        Method getAgeMethod = c.getMethod("getAge");
        System.out.println("name:" + getNameMethod.invoke(object) + ",age:" + getAgeMethod.invoke(object));
        System.out.println("类加载器为:" + object.getClass().getClassLoader());
    }
}

  因为该Person类,在IDE中编译后放在了classpath,而classpath默认是由ApplicationClassLoader进行加载的,而MyClassLoader的parent为ApplicationClassLoader,所以如果在IDE中执行测试程序,根据双亲委派机制,Person类的类加载器将一直是ApplicationClassLoader,下图是运行的结果:

  显然,直接在IDE中执行测试类是没有办法使用我们自定义的类加载器实现类加载的,这一切的原因就是Person类在classpath中,所以解决方法就是将编译后的Person类,放到"D:/class/"目录下,这个是例子中定义的目录,根据具体情况自己指定。如下图所示:

  接下来,我们先将项目打包成Jar包,此时Jar中含有编译后的Person类,然后需要将该类从中删除掉,然后再运行Jar程序,如果不删除jar包中的Peson类,则会仍会是ApplicationClassLoader,如下所示:

  下面删除jar包中的Person类,那么就会从"D:/class"里进行加载,也就是自定义的类加载器去加载的,如下所示,显然类加载器已经是MyClassLoader了:

总结

  在不打破双亲委托机制的前提下,自定义ClassLoader主要是实现了findClass()方法,这里主要是通过实例演示了自定义类加载的实现和原理。具体如何查找自己定义的类,例如如果是jar则读取jar包中的类,可以根据自己的实际需求自己实现。