类加载器双亲委托机制实例深度剖析
在上一次【https://i.cnblogs.com/posts?categoryid=1154466】分析自定义类加载器的核心方法中还差一个,如下:
public class MyTest16 extends ClassLoader { private String classLoaderName; private final String fileExtension = ".class";//要加载的字节码文件的扩展名 public MyTest16(String classLoaderName) { super();//将系统类加载器当作该类加载器的父加载器 this.classLoaderName = classLoaderName; } public MyTest16(ClassLoader parent, String classLoaderName) { super(parent);//显示指定该类加载器的父加载器 this.classLoaderName = classLoaderName; } @Override public String toString() { return "[" + classLoaderName + "]"; } @Override protected Class<?> findClass(String className) throws ClassNotFoundException { byte[] data = this.loadClassData(className); return this.defineClass(className, data, 0, data.length); } private byte[] loadClassData(String className) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; try { this.classLoaderName = this.classLoaderName.replace(".", "/"); is = new FileInputStream(new File(className + this.fileExtension)); baos = new ByteArrayOutputStream(); int ch = 0; while (-1 != (ch = is.read())) { baos.write(ch); } data = baos.toByteArray(); } catch (Exception ex) { ex.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (Exception ex) { ex.printStackTrace(); } } return data; } private static void test(ClassLoader classLoader) throws Exception { Class<?> clazz = classLoader.loadClass("com.jvm.classloader.MyTest1"); Object object = clazz.newInstance(); System.out.println(object); } public static void main(String[] args) throws Exception { MyTest16 myTest16 = new MyTest16("loader1"); test(myTest16); } }
所以点进去瞅一下官网的说明:
下面仔细读一下这个方法的javadoc:
这个findClass()也就是咱们在自定义类加载器时所覆写的。
如下:
另外再来简单看一下它的具体实现,其中发现这段:
所以自定义的类加载器一定得覆写findClass()方法。
好,了解了loadClass()之后,下面再回到咱们编写的自定义程序来,先再来运行回顾一下结果:
但是!MyTest1到底是被哪个加载器所加载的呢?这还用说,肯定是被咱们自定义的加载器嘛,要不然还辛苦写了这么多代码干嘛,好为了验证咱们的猜想,咱们打一些日志便于确认:
要是是咱们自定义的类加载器来加载的MyTest1,那findClass()肯定是会被调用的,所以咱们再次运行看结果:
呃~~什么鬼~~居然没有按预期打印,说明咱们的findClass压根就木有调用,这不是坑爹么?做了这么多工作等于是无用功,那这个MyTest1是哪个类加载器所加载的呢,下面打印探究一下:
呃~~这是为什么!!!下面分析一下:
而根据类加载器的双亲委托机制,加载类时并非首先是自己去加载,而是委托给它的父加载器去加载,而目前MyTest16的父加载器是系统类加载器,所以会它系统类加载器去加载“com.jvm.classloader.MyTest1”类,而由于MyTest1是咱们工程的类当然能够被系统类加载器所加载,所以这就是原因,这里涉及到另外一个概念,之前已经学习过,复习下,如下【https://www.cnblogs.com/webor2006/p/9016996.html】:
回到咱们这个例子,由于真正去加载类的是系统类加载器,所以系统类加载器则为定义类加载器,而自定义的MyTest16加载器和系统类加载器则为初始类加载器。
了解了这种场景之后,接下来进一步基于这个程序进行改造拓展:
为了进一步的能体会类加载器的双亲委托机制,目前程序只会从工程中去加载类,所以需要修改一下程序能将具体路径进行更改,也就是类加载器可以指定到底是从哪个目录去加载而不只是工程路径,修改如下:
public class MyTest16 extends ClassLoader { private String classLoaderName; /* 从什么地方去加载,这是个绝对路径 */ private String path; private final String fileExtension = ".class";//要加载的字节码文件的扩展名 public MyTest16(String classLoaderName) { super();//将系统类加载器当作该类加载器的父加载器 this.classLoaderName = classLoaderName; } public MyTest16(ClassLoader parent, String classLoaderName) { super(parent);//显示指定该类加载器的父加载器 this.classLoaderName = classLoaderName; } public void setPath(String path) { this.path = path; } @Override public String toString() { return "[" + classLoaderName + "]"; } @Override protected Class<?> findClass(String className) throws ClassNotFoundException { System.out.println("findClass invoked: " + className); System.out.println("class loader name: " + this.classLoaderName); byte[] data = this.loadClassData(className); return this.defineClass(className, data, 0, data.length); } private byte[] loadClassData(String className) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; className = className.replace(".", "/");//由于路径名需要以/分隔,而不是像包名以.分隔的 try { is = new FileInputStream(new File(this.path + className + this.fileExtension)); baos = new ByteArrayOutputStream(); int ch = 0; while (-1 != (ch = is.read())) { baos.write(ch); } data = baos.toByteArray(); } catch (Exception ex) { ex.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (Exception ex) { ex.printStackTrace(); } } return data; } private static void test(ClassLoader classLoader) throws Exception { Class<?> clazz = classLoader.loadClass("com.jvm.classloader.MyTest1"); Object object = clazz.newInstance(); System.out.println(object); System.out.println(object.getClass().getClassLoader()); } public static void main(String[] args) throws Exception { MyTest16 myTest16 = new MyTest16("loader1"); test(myTest16); } }
接下来开始实验:
设置一个什么路径呢?首先设置工程路径,如下:
然后再加载具体类:
好,编译运行:
很显然依然是加载的工程里面的类,当然还是由系统加载类来加载的喽,原因跟之前的那个一样。
接下来继续改造,将类的路径设置成一个非工程的路径,所以这里先在桌面创建一个目录:
然后将MyTest1的字节文件拷贝到咱们刚才创建的目录中,如下:
此时路径已经准备好,接下来修改程序将类加载器的path修改一下:
然后再将工程中生成的MyTest1的字节文件给删掉:
这又做何解释呢?因为这次设置的路径是非工程路径,并且工程里面的MyTest1的字节文件也已经被删掉了, 还是根据类加载器的双亲委托机制,首先由咱们自定义类加载器的父亲加载,很显然父亲肯定是加载不成功的,所以最终转移给咱们自定义的类加载器MyTest16去加载,所以最终是由自定义的类加载器所加载。
那如果此时将工程中的MyTest1的字节码文件又重新生成呢,如下:
代码不做任何改动再次运行:
通过这个实验是不是对类加载器的双亲委托机制有了更加清晰的认识了,下面继续改造:
也就是新建了第二个自定义的类加载器加载同一个类,其路径也一模一样,那输出会是啥呢?这里不卖关子了,看结果:
好,接下来再将MyTest1的字节码文件再次删掉再运行:
其结果完全不同,居然这次的class文件不一样的,不是一个类如果被加载了之后就不会被再次加载了么,那不这次的实验违背了这个结论,其实是不违背的,因为这里涉及到命名空间的问题,那什么是命名空间呢?
- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。