Java中自定义类加载器

9 篇文章 0 订阅

类加载相关文章索引:
一文浅析Java中类加载过程
一文弄懂Java中类加载器的关系
Java中双亲委派机制的实现原理

在上文中(Java中双亲委派机制的实现原理),我们了解到双亲委派机制的实现原理,通过了解它,可以帮助我们更好自定义属于我们自己的类加载器。

想要自定义类加载器,只需简单三步:

  1. 继承 ClassLoader
  2. 重写 findClass() 方法
  3. 使用 defineClass() 方法生成 Class 对象

流程概述

下面我们就来实现自己的类加载器,大致流程如下:

  1. 创建一个需要被自定义加载器加载的类,并且把它放在 classpath 以外的其他地方。
  2. 继承 ClassLoader,并重写 findClass() 方法实现类加载逻辑。
  3. 使用 defineClass() 方法生成 Class 对象。
  4. 通过反射调用对象中的方法,看看是否加载成功。

下面就进行实践吧!
在这里插入图片描述

创建待加载的类

首先,创建一个待加载的类,里面写一个普通的成员方法

package com;

public class OtherClass {

    public void say(){
        System.out.println("Hello custom class loader");
    }

}

我写了一个叫做 OtherClass 的类,里面有一个 say() 的方法,方法里打印了一句话。
这里需要注意一点就是:OtherClass 我是放在 com 包下面的,它的全限定名就是:com.OtherClass。这点很重要,下面会用到。

然后,我们将 OtherClass 编译成 class 字节码,放到其他地方去。
在这里插入图片描述

我现在把它放在 F:\ClassLoaderTest\ 目录下。

为什么要把它拎出来放在其他地方而不放在工程目录下呢?是因为随着工程的编译,工程下面的所有代码,都会编译到 classpath 目录下。而通过前面文章说了,classpath下的字节码会被 Application ClassLoader 类加载器所加载。但是此时我不想让 OtherClass 被 Application ClassLoader 加载,我待会儿要用自定义的类加载器去加载它,所以我就把 OtherClass 拎出来,放到其他地方,这样 Application ClassLoader 就没办法加载它了。

实现自定义类加载器

接下来,我们实现自定义类加载器的代码

public class CustomClassLoader extends ClassLoader{......}

这里我们创建了一个叫做 CustomClassLoader 的类作为我们的自定义类加载器,它继承与 ClassLoader 抽象类。

然后我们需要重写 findClass() 方法实现类加载逻辑。所谓“类加载逻辑”,无非就是如何加载自定义的类。

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
	// 调用 getClassFileData 方法实现加载逻辑,获取到 byte[] 的返回值。
	byte[] data = getClassFileData(name);
	// byte[] 类型的 data 变量,传入 defineClass 方法生成 Class 对象并返回。
	return defineClass(name, data, 0, data.length);
}

上面代码已经解释了简单的两句话代表什么意思。下面我再详细说明一下。

当虚拟机进行类加载的时候,会调用加载器的私有方法:loadClassInternal(),这个方法里面唯一的逻辑就是调用自己的 loadClass方法。
但是我们并没有实现这两个方法。由于我们继承了 ClassLoader 抽象类,那么在进行类加载的时候就会去调用父类,也就是 ClassLoader 类中的这两个方法。
在上篇文章我们了解到,在 loadClass() 方法的逻辑里,如果父类加载失败,则会调用自己的 findClass() 方法来完成加载。这样也就保证了自定义的类加载器也是符合双亲委派的机制的。

由于上面我们把 OtherClass 移出去了,所以默认的三个类加载器是肯定加载不了的,此时就会调用我们自己实现的 findClass()方法了。

在 findClass 方法中的第一句话调用了另一个方法 getClassFileData() 方法,下面我们详细看看这个方法:

/**
 * 通过路径找到 Class 字节码,并将它转化成 byte 数组
 * @param fileFullName 待加载的Class字节码存放的位置
 * @return byte 数组
 */
private byte[] getClassFileData(String fileAbsolutePath) {
	// 正常情况下应该使用这句话,参数 fileAbsolutePath 指的是待加载的 Class 字节码存放的位置
	// 建议再使用一个单独的方法去完成路径的组装
//  File file = new File(fileFullName);
	// 我这里偷个懒,直接把路径写死了啦。
	File file = new File("F:\\ClassLoaderTest\\OtherClass.class");

	byte[] result = null;
	// 下面逻辑非常简单,就是把 File 对象转化成 byte 数组的逻辑。
	try(FileInputStream fis = new FileInputStream(file);
		BufferedInputStream bis = new BufferedInputStream(fis);
		ByteArrayOutputStream bos = new ByteArrayOutputStream())
	{
		int data;
		while ((data = bis.read()) != -1) {
			bos.write(data);
		}
		bos.flush();
		result = bos.toByteArray();
	}catch (IOException e ){
		e.printStackTrace();
	}
	return result;
}

通过上面的代码也看出来了,getClassFileData() 并不是一个神秘的方法,它就是我自己定义的一个普通的方法。它的目的就是:通过路径找到 Class 字节码,并将它转化成 byte 数组。至于为什么要转化成 byte 数组,是因为 defineClass() 方法要求这样的格式啦。

回过头我们再来看看 findClass() 方法第二句话,这里调用了 defineClass() 方法,并且将上面的获取到的 byte 数组传进去,同时还需要几个参数,分别是:类的全限定名、byte数组、偏移量和长度。
如果不了解偏移量和长度的作用的,可以参考这篇文章—Java IO流中偏移量是什么意思,里面有详细的解释。

现在我们自定义类加载器已经实现完成啦。是不是灰常简单,下面就下 main 方法里面来调用看看效果吧。

public static void main(String[] args) throws ReflectiveOperationException {
	// 创建自定义类加载器实例
	CustomClassLoader customClassLoader = new CustomClassLoader();
	// 调用 loadClass() 方法,并传入全限定名,进行加载。
	Class c = customClassLoader.loadClass("com.OtherClass");
	System.out.println(c);
	// 创建实例对象
	Object obj = c.newInstance();
	// 获取 Method 对象
	Method method = c.getDeclaredMethod("say");
	// 通过invoke方法来调用
	method.invoke(obj);
}

结果打印:

class com.OtherClass
Hello custom class loader

main 方法里面的代码也很简单,里面已经用注释说明了。

到此,自定义类加载器已经学习完啦~ 下面进行总结。

总结

想要实现自定义类加载器,只需三步:

  1. 继承 ClassLoader
  2. 重写 findClass
  3. 调用 defineClass

完事儿~

完整示例

下面贴出自定义类加载器的完整代码,可以直接 Copy 下来跑的那种哦!
前置条件:待加载的 Class 先提前准备好!
我使用还是 OtherClass。

package com;

public class OtherClass {

    public void say(){
        System.out.println("Hello custom class loader");
    }

}
/**
 * 自定义类加载器,继承于 ClassLoader 抽象类
 */
public class CustomClassLoader extends ClassLoader {

    // 用于存放我待加载的 class 文件的路径。
    private String myLibPath;

    // 通过构造器对 myLibPath 初始化。
    public CustomClassLoader(String myLibPath) {
        this.myLibPath = myLibPath;
    }

    /**
     * 重写 findClass 方法完成自己的类加载逻辑,在这一步我们可操作性最强,可以玩出很多花儿来。
     * @param name 待加载类的全限定名
     * @return 加载完成的 Class 对象
     */
    @Override
    protected Class<?> findClass(String name) {
        System.out.println("My findClass area.");
        byte[] data = getClassFileData(name);
        return defineClass(name, data, 0, data.length);
    }

    /**
     * 通过路径找到 Class 字节码,并将它转化成 byte 数组
     * @param fileAbsolutePath 待加载的Class字节码存放的位置
     * @return byte 数组
     */
    private byte[] getClassFileData(String fileAbsolutePath) {
        File file = new File(getAbsolutePath(fileAbsolutePath));

        byte[] result = null;
        // 下面逻辑非常简单,就是把 File 对象转化成 byte 数组的逻辑。
        try(FileInputStream fis = new FileInputStream(file);
            BufferedInputStream bis = new BufferedInputStream(fis);
            ByteArrayOutputStream bos = new ByteArrayOutputStream())
        {
            int data;
            while ((data = bis.read()) != -1) {
                bos.write(data);
            }
            bos.flush();
            result = bos.toByteArray();
        }catch (IOException e ){
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 获取 Class 文件的绝对路径
     * @param name class 的全限定名
     * @return class 的绝对路径
     */
    private String getAbsolutePath(String name){
        String className = name.substring(name.lastIndexOf(".") + 1) + ".class";
        return myLibPath + File.separator + className;
    }


    public static void main(String[] args) throws ReflectiveOperationException {
        // 创建自定义类加载器实例
        CustomClassLoader customClassLoader = new CustomClassLoader("F:\\ClassLoaderTest");
        // 调用 loadClass() 方法,并传入全限定名,进行加载。
        Class c = customClassLoader.loadClass("com.OtherClass");
        System.out.println(c);
        // 创建实例对象
        Object obj = c.newInstance();
        // 获取 Method 对象
        Method method = c.getDeclaredMethod("say");
        // 通过invoke方法来调用
        method.invoke(obj);
    }
}

在这里插入图片描述


技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值