Java高新技术 类加载器
知识概要:
(1)类加载器简介
(2)类加载器的委托机制
(3)自定义类加载器的编写原理分析
(4)一个类加载器的高级问题分析
类加载器简介
简要介绍什么是类加载器和类加载器的作用
Java虚拟机中可以安装多个类加载器系统默认三个主要类加载器,每个类加载器负责加载特定位置的类:
BootStrap,ExtClassLoader,AppClassLoader
类加载器也是Java类,因为java类的类加载器本身也要被类加载器加载
显然必须有第一个类加载器不是java类,这正是BootStrap。
Java虚拟机中的所有类装载器采用具有父子关系的树形结构进行组织在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象
或者默认采用系统类装载器为其父级类加载。
代码示例:
package com.itheima.day02;
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(ClassLoaderTest.class.getClassLoader().
getClass().getName());
System.out.println(System.class.getClassLoader());
}
}
由上面的示例可以看到ClassLoaderTest类是由AppClassLoader类加载器加载的。
System类是由BootStrap类加载器加载的
注意
JVM内核启动的时候,BootStrap就已经被加载了,它是内嵌在JVM内核中的,是用C++语言编写的二进制代码,因此不需要其他类加载器加载。
Java虚拟机中的所有类装载器采用了具有父子关系的树形结构进行组织。
package cn.itheima.day2;import java.util.Date;public class ClassLoaderTest { public static void main(String[] args) throws Exception{ ClassLoader loader = ClassLoaderTest.class.getClassLoader(); while(loader !=null) { System.out.println(loader.getClass().getName()); loader = loader.getParent(); } System.out.println(loader); }}
运行结果:
sun.misc.Launcher$AppClassLoader sun.misc.Launcher$ExtClassLoader null
Java虚拟机中的所有类装载器采用具有父子关系的树形结构进行组织
在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象
或者默认采用系统类装载器为其父级类加载。
类加载器之间的父子关系和管辖范围图
小实验:将ClassLoaderTest类的class文件,打成jar包,放置在JRE/lib/ext/*.jar,它会由哪个类加载器加载呢
ExtClassLoader
类加载器的委托机制
当Java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?
首先当前线程的类加载器去加载线程中的第一个类。
如果类A中引用了类B,Java虚拟机将使用加载类A的类装载器来加载类B。还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。 每个类加载器加载类时,又先委托给其上级类加载器。 当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则抛ClassNotFoundException,不是再去找发起者类加载器的儿子,因为没有getChild方法,即使有,那有多个儿子,找哪一个呢?
注意:
自定义类加载器的编写原理分析每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类装载器去加载类,这就是类加载器的
委托模式。类加载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子
孙类装载器去进行真正的加载。当回退到最初的类装载器时,如果它自己也不能完成类的装加载,
那就会告ClassNotFoundException异常
类加载器中的loadClass方法内部实现了父类委托机制,因此我们没有必要自己覆盖loadClass,否则需要自己去实 现父类委托机制。我们只需要覆盖findClass方法。loadClass方法中调用了findClass方法,使用的是模板设计模 式。我们得到了Class文件后,就可以通过defineClass方法将二进制数据转换成字节码。这就是自定义类加载器的编写原理。
API文档中的例子:
知识讲解:
自定义的类加载器的必须继承ClassLoader loadClass方法与findClass方法 defineClass方法 编程步骤: 1.编写一个对文件内容进行简单加密的程序。 2.编写了一个自己的类装载器,可实现对加密过的类进行装载和解密。 3.编写一个程序调用类加载器加载类,在源程序中不能用该类名定义引用变量,因为编译器无法识别这个类。程序中可以除了使用ClassLoader.load方法之外,还可以使用设置线程的上下文类加载器或者系统类加载器,然后再使用Class.forName。 实验步骤: 1.对不带包名的class文件进行加密,加密结果存放到另外一个目录 2.运行加载类的程序,结果能够被正常加载,但打印出来的类装载器名称为AppClassLoader 3.用加密后的类文件替换CLASSPATH环境下的类文件,再执行上一步操作就出问题了,错误说明是AppClassLoader类装载器装载失败。 4.删除CLASSPATH环境下的类文件,再执行上一步操作就没问题了。
将此class文件,覆盖项目中bin\com\itheima\day02目录下的ClassLoaderAttachment.class文件,重新执行
ClassLoaderTest类中的main方法,就会报错。这是因为AppClassLoader已经无法正确加载编码后的class文件了,
只有我们自己写一个带有解码功能的类加载器才可以。
代码示例:
package cn.itheima.day2;import java.util.Date; public class ClassLoaderAttachment extends Date { public String toString() { return "黑马程序员"; }}
package cn.itheima.day2;import java.io.*; //继承抽象类ClassLoader public class MyClassLoader extends ClassLoader { public static void main(String[] args) throws Exception { //传入两个参数,源和目标 String scrPath = args[0]; String destDir = args[1]; //将数据读取到输入流中,并写入到输出流中 FileInputStream fis = new FileInputStream(scrPath); String destFileName =scrPath.substring(scrPath.lastIndexOf('\\')+1); String destPath = destDir + "\\" + destFileName; FileOutputStream fos = new FileOutputStream(destPath); //加密数据 cypher(fis,fos); fis.close(); fos.close(); } //定义加密数据的方法 private static void cypher(InputStream ips,OutputStream ops)throws Exception{ int b = 0; while((b=ips.read())!=-1){ ops.write(b ^ 0xff); } } //定义全局变量 private String classDir; @Override//覆写findClass方法,自定义类加载器 protected Class findClass(String name) throws ClassNotFoundException { String classFileName = classDir + "\\" + name + ".class"; try { //将要加载的文件读取到流中,并写入字节流中 FileInputStream fis = new FileInputStream(classFileName); ByteArrayOutputStream bos = new ByteArrayOutputStream(); cypher(fis,bos); fis.close(); byte[] bytes = bos.toByteArray(); return defineClass(bytes, 0, bytes.length); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } //如果没找到类,则用父级类加载器加载 return super.findClass(name); } //构造函数 public MyClassLoader(){} public MyClassLoader(String classDir){ this.classDir = classDir; } }
package com.itheima.day02; import java.util.Date; public class ClassLoaderTest { public static void main(String[] args) throws Exception { Class clazz = new MyClassLoader("ClassLoaderLib").loadClass("ClassLoaderAttachment"); Date d = (Date)clazz.newInstance(); System. out.println(d); } }
注意:
1、之所以让ClassLoaderAttachment类继承Date是因为,如果直接写
ClassLoaderAttachment d = (ClassLoaderAttachment)clazz.newInstance();这条语句,编译的时候就会报错,因为此时的 ClassLoaderAttachment在AppClassLoader加载进内存后就无法识别。所以需要通过借助一个父类对象绕过编译器。也就 是:Date d1 = (Date)clazz.newInstance();。
2、如果想让父类加载器AppClassLoader加载ClassLoaderAttachment类,则需要执行下面的语句:
Class clazz = new MyClassLoader("ClassLoaderLib").loadClass("com.itheima.day2.ClassLoaderAttachment"); 但是父类加载可能会出错。
一个类加载器的高级问题分析
编写一个能打印出自己的类加载器名称和当前类加载器的父子结构关系链的MyServlet,
正常发布后,看到打印结果为WebAppClassloader。
把MyServlet.class文件打jar包,放到ext目录中,重启tomcat,发现找不到HttpServlet的错误。 把servlet.jar也放到ext目录中,问题解决了,打印的结果是ExtclassLoader 。 父级类加载器加载的类无法引用只能被子级类加载器加载的类,原理如下图
错误原因:
在没有将MyServlet导出为jar包到jdk\jre\lib\ext目录以前,MyServlet以及其父类都是由WebAppClassLoader加载的。但是导出到jdk\jre\lib\ext目录以后,就由ExtClassLoader类加载器加载MyServlet,但是HttpServlet却不在ext目录下,所以无法加载到。而且MyServlet和HttpServlet是同一线程的,这也就意味着他们两个是由同一个类加载器加载的。ExtClassLoader类加载器加载不了,就会直接报错,而不会再交WebAppClassLoader类加载器再加载。
解决方法:将HttpServlet所在的jar包拷贝到jdk\jre\lib\ext目录下,重新启动tomcat服务器,重新在浏览器中访问
MyServlet。