类加载相关文章索引:
一文浅析Java中类加载过程
一文弄懂Java中类加载器的关系
Java中双亲委派机制的实现原理
在上文中(Java中双亲委派机制的实现原理),我们了解到双亲委派机制的实现原理,通过了解它,可以帮助我们更好自定义属于我们自己的类加载器。
想要自定义类加载器,只需简单三步:
下面我们就来实现自己的类加载器,大致流程如下:
首先,创建一个待加载的类,里面写一个普通的成员方法
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 方法里面的代码也很简单,里面已经用注释说明了。
到此,自定义类加载器已经学习完啦~ 下面进行总结。
想要实现自定义类加载器,只需三步:
完事儿~
下面贴出自定义类加载器的完整代码,可以直接 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);
}
}
技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。