介绍完基本的类加载器后,我们再扩展一点,如果在加载过程中,还使用到了其他对象的引用,那我们加载是怎样进行的呢?
上代码(用我们上篇文字所自定义的加载器,然后去加载MySample和MyCat类,因MySample里面主动创建了MyCat对象,因此对MySampl的加载过程中,也产生了对MyCat的对象引用),最后我们看输出结果
public class MySample {
/* static {
System.out.println("sample static block invoke");
}*/
public MySample() {
//后续测试中,new对象时即为对类的首次主动使用,因此需要初始化,需要进行加载动作
System.out.println("sample class load:" + this.getClass().getClassLoader());
new MyCat();
}
public class MyCat {
public MyCat() {
System.out.println("myCat load:" + this.getClass().getClassLoader());
}
使用自定义的加载器进行加载
public class MyTest2 {
public static void main(String[] args) throws Exception {
//首先我们用自定义的加载器去加载sample
MyTest1 load1 = new MyTest1(null, "load1");
Class<?> aClass = load1.loadClass("jp.zhang.classloader.MySample");
//
Object o = aClass.newInstance();
}
通过MySample的字节码文件进行反射创建对象时,调用了空参构造方法,所以又调用了MyCat的构造方法,得出的输出如下:
由此得出的结论:加载类的过程时,如果还产生了其他类的对象引用,那么也会使用该类的加载器去加载被引用的类,在加载的过程中,同样也是遵循双亲委托机制。
为了证明还是遵循双亲委托,我们再做个实验:我们指定自定义器的加载路径(目的是为了区分不同的场景,在整个加载过程中,能看出使用不同的加载器)在classPath路径中,先保留MySample.class文件,将MyCat.class移到我们指定的路径中,最新的自定义类加载器代码:
package jp.zhang.classloader;
import java.io.*;
/**
* created by: marshal
* createDate: 2019/12/16 10:19 PM
*/
public class MyTest1 extends ClassLoader {
public String classLoaderName;//为了后面获取classloader名称时使用自定义名称
public ClassLoader parent;//指定使用自己的父类加载器,同时因为双亲模式,如果需要真正使用到自定义的加载器加载,需要将父类加载器置为null,或者将class文件放入到别的路径中。
//自定义父类加载器
public MyTest1(ClassLoader parent, String classLoaderName) {
super(parent);
this.parent = parent;
this.classLoaderName = classLoaderName;
}
//自定义加载器
public MyTest1(String classLoaderName) {
super();
this.classLoaderName = classLoaderName;
}
public String path;
public void setPath(String path) {
this.path = path;
}
public static final String CLASS_PREFIX = ".class";
/**
* classLoader,根据文档说明,需要复写其findClass方法
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//1、先要将类的字节名称转换成字节码数组
// System.out.println("find class method invoke");
byte[] data = this.loadClassData(name);
//2需要将获取到的字节数组转换成class对象,后续如果需获取到该class文件代表类的对象
return defineClass(name, data, 0, data.length);
}
/**
* 通过类的路径,将名称对应的类文件读取里面的内容,转换成byte数组
*
* @param name
* @return
*/
private byte[] loadClassData(String name) {
System.out.println("start transform file to byte array for class");
InputStream in = null;
ByteArrayOutputStream out = null;
try {
//将二进制名称转换成字节码路径
String replace = name.replace(".", "/");
//如果path设定,就使用path路径的代码,否则默认使用classpath的target输出目录
String target = null == path ? "target/classes/" + replace : path + replace;
File file = new File(target + CLASS_PREFIX);
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
int read;
while (-1 != (read = in.read())) {
out.write(read);
}
return out.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("byte array is null");
return null;
}
public static void main(String[] args) throws Exception {
MyTest1 load1 = new MyTest1(null, "load1");
Class<?> aClass = load1.loadClass("jp.zhang.classloader.MyTest2");
System.out.println(aClass.getClassLoader());
}
}
测试:把MySample保留,将MyCat移除到指定目录,得到以下结果:
随后我们进行测试对比,将MyCat保留在classpath中,MySample移到指定目录是不是很神奇?原因也是跟双亲委托模式有关:
在第一种情况下,MySample是由app应用类加载器完成的,后续再加载MyCat时,按照上面的结论,也是用应用加载器进行加载,整个双亲下来都加载不了,本身也找不到该class文件,这也说明加载器加载对象引用的类时,也是使用相同的类加载器,而且是作为第一层的加载器,然后遵循双亲委派。
而第二种场景,父类加载不了MySample,于是由子加载器进行加载,后续到MyCat时,也是由子加载器尝试加载,因为双亲委托,刚好父类应用类加载器可以加载,所以看到对应的加载器为应用类加载器,这也是子类加载器可以看到父类加载器所加载器类的体现。
以上就是加载过程中 ,对引用对象的加载所遵循的规则,将该加载器同样加载引用,然后遵循双亲委派模式。
下一节,我们介绍加载器的命名空间。