自定义类加载器

假设,现在有一个Hello.xlass 文件,里面有一个hello()方法,但是此文件内容是一个所有字节(x=255-x)被处理后的文件,那么你应该如何正确读取这个文件呢?

这里就需要自定义类加载器,来加载这个文件了。

首先,我们还是看一下Java的类加载过程。

类的生命周期 1.加载:找Class文件 2.验证:验证格式和依赖 3.准备:为类变量(static修饰的变量)分配内存并设置初始零值。注意,此时实例变量还没有分配内存。 4.解析:符号解析为引用 5.初始化:构造器,实例变量分配内存并赋值,静态变量赋值,静态代码块 6.使用 7.卸载

三类加载器 启动类加载器 扩展类加载器 应用类加载器

加载器的特点:双亲委派;负责依赖;缓存加载。

对于双亲委派模型:一个类加载器在进行加载时,不会自己去尝试加载这个类。先看看有没有加载过这个类,然后将这个请求委派给父类加载器去完成,只有当父加载器反馈自己无法完成加载请求时,子加载器才会自己去尝试加载这个类。

这样做的好处是,防止同名的类出现混乱,也能提高安全性。举个例子,比如java.lang.Object这个类,无论哪个类加载器加载时,最终都会委派给Bootstrap加载器去加载,这就保证了整个系统运行过程中的Object都是同一个类。否则,如果用户自己编写了一个java.lang.Object类,并放在程序的classpath中,最终系统将会出现多个不同的Object类,整个Java体系就变得一团混乱了。

在实现自己的ClassLoader之前,先看一下JDK中的ClassLoader是怎么实现的:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 1. 首先,检查是否被加载过了
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 2. 没有加载过,就交给父类去加载
                        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) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // 3. 还是没有加载到,才交给自己去加载
                    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;
        }
    }

上面代码主要逻辑是:

1、首先查找.class是否被加载过。

2、如果.class文件没有被加载过,那么会去找加载器的父加载器。如果父加载器不是null,那么就执行父加载器的loadClass方法,把类加载请求一直向上抛,直到父加载器为null(这个时候就到了Bootstrap ClassLoader)为止。

3、如果父加载器没有成功,就交给子类加载器去加载。

看一下findClass这个方法:

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

没有任何实现,直接抛出了一个异常,而且是protected的,所以:这个方法就是用于继承后,需要重写

从上面的分析可以知道:

1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可。

2、如果想打破双亲委派模型,那么就重写整个loadClass方法。

自定义的类加载器如下:

  • 重写findClass方法;
  • 获取字节数组;
  • 根据要求处理字节数组;
  • 使用新的字节数组定义类。

import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * 自定义类加载器
 * <p>
 * 参考资料:
 * https://www.cnblogs.com/xrq730/p/4847337.html
 * https://segmentfault.com/a/1190000012925715
 * https://github.com/sodawy/JAVA-000/tree/main/Week_01
 */
public class MyClassLoader extends ClassLoader {
    public static final byte DIGITAL_255 = (byte) 255;
    private String filePath;

    public MyClassLoader(String filePath) {
        this.filePath = filePath;
    }

    //重写该方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = getClassBytes(filePath);

            // 根据要求处理字节码
            byte[] deBytes = handleByte(bytes);

            // 使用新的字节数组定义类
            return defineClass(name, deBytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    /**
     * 根据自定义的要求处理字节码
     *
     * @param oldBytes 原来的字节数组
     * @return 放回处理后的数组
     */
    private byte[] handleByte(byte[] oldBytes) {
        byte[] newBytes = new byte[oldBytes.length];

        for (int i = 0; i < oldBytes.length; i++) {
            newBytes[i] = (byte) (DIGITAL_255 - oldBytes[i]);
        }
        return newBytes;
    }

    /**
     * 获取字节数组
     * @param filePath class文件路径
     * @return 字节数组
     * @throws Exception
     */
    private byte[] getClassBytes(String filePath) throws Exception {
        return Files.readAllBytes(Paths.get(filePath));
    }
}

自定义类加载器写完后,进行测试:


import org.junit.Test;

import java.lang.reflect.Method;

public class TestMyClassLoader {

    @Test
    public void test() throws Exception {
        String filePath = "D:\\Hello\\Hello.xlass";
        //使用自定义类加载器加载类
        MyClassLoader myClassLoader = new MyClassLoader(filePath);
        Class<?> clazz = myClassLoader.loadClass("Hello");

        //实例化对象
        Object obj = clazz.newInstance();
        //获取声明的方法
        Method method = clazz.getDeclaredMethod("hello");
        //方法调用
        method.invoke(obj);

        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}

至此,我们就完成了如何自定义类加载器。

自定义类加载器的应用
  • Tomcat上可以部署多个不同的应用,但是它们可以使用同一份类库的不同版本。
  • 对于非.class的文件,需要转为Java类,就需要自定义类加载器。
  • 加密解密。