一、类加载器及其委托机制的深入分析
什么是类加载器和类加载器的作用:类加载器就是加载类的工具,Java程序中用到一个类,java虚拟机首先要把这个类的字节码加载到内存中来,通常这个字节码的原始信息放在硬盘上的classpath指定的目录下,把.class中的内容加载到硬盘中来,再对它进行一些处理,处理完的结果就是字节码。这个过程就是类加载器在工作。
Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个类负责加载特定位置的类:BootStrap, ExtClassLoader, AppClassLoader
类加载器也是Java类,因为其他java类的类加载器本身也要被类加载器加载,显然必须有第一个类加载器不是java类,这正是BootStrap(嵌套在Java虚拟机内核中的C++语言写的二进制代码)。
Java虚拟机中的所有类加载器蚕蛹具有父子关系的树形结构进行组织,在实例化每个类加载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类加载器为其父级类加载。
ClassLoader loader = ClassLoaderTest.class.getClassLoader();
while(loader != null) {
System.out.println(loader.getClass().getName());
loader = loader.getParent();
}
System.out.println(loader);
屏幕则会输出AppClassLoader, ExtClassLoader, null. 体现了类加载器的父子关系。
可以自己写一个类加载器,构造方法 ClassLoader()和ClassLoader(ClassLoader parent)指定一个父亲,不指定的话有一个默认的父亲。
类加载器的委托机制
每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类加载器去加载类,这就是类加载器的委托机制。类加载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子孙类加载器去进行真正的加载。当回退到最初的类加载器时,如果它自己也不能完成类的加载,那就应该报告ClassNotFoundException异常。
当Java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?
首先当前线程的类加载器去加载线程中的第一个类。(每个线程都有getcContextClassLoader()和setContextClassLoader(ClassLoader cl))
如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器在加载类B。
还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
每个类加载器加载类时,又先委托给其上级类加载器。
当所有祖宗类加载器没有加载到类,回到发起者加载器,还加载不了,则抛ClassNotFoundException,不是再去找发起者类加载器的儿子,因为没有getChild方法,即使有,那么有多个儿子,找哪一个呢?
这样的好处是可以集中管理,避免出现多份字节码。
有一道面试题,能不能自己写一个类叫java.lang.System,为了不让我们写System类,类加载采用委托机制,这样就可以保证爸爸们优先,也就是总是使用爸爸们能找到的类,这样总是使用java系统提供的System。(自己写类加载器就可以,也就是说自己写,但是要避开委托加载机制)。
二、编写自己的类加载器
知识讲解
自定义的类加载器必须继承ClassLoader(抽象类)
loadClass方法与findClass方法(loadClass先找父亲,然后找findClass,因此,只用重写findClass就可以了)这是模板方法设计模式
模板方法设计模式:中的流程父类已经做好,只是一些细节无法确定,因此空出来留待子类去完成。
父类 -> loadClass/findClass (流程在父类中)/得到class文件,把class文件的内容转换成字节码--> defineClass() 可以实现把一个字节数组转换成class
子类1(自己做的代码)
子类2(自己做的代码)
defineClass方法
JDK的例子:
class NetworkClassLoader extends ClassLoader {
String host;
int port;
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
//load the class data from the connection
...
}
}
编程步骤
编写一个对文件内容进行简单加密的程序。
public class MyClassLoader {
public static void main(String[] args) throws Exception{
String srcPath = args[0];
String destDir = args[1];
FileInputStream fis = new FileInputStream(srcPath);
String destFileName = srcPath.substring(srcPath.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 = -1;
while ((b = ips.read()) != -1) {
ops.write(b ^ 0xff);
}
}
}
编写了一个自己的类加载器,可实现对加密过的类进行装载和解密。
在MyClassLoader中,让MyClassLoader extends ClassLoader,然后加上下面代码:
private String classDir;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String classFileName = classDir + "/" + name + ".class";
FileInputStream fis;
try {
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;
}
编写一个程序调用类加载器加载类,在源程序中不能用该类名定义引用变量,因为编译器无法识别这个类。程序中可以除了使用ClassLoader.load方法之外,还可以使用设置线程的上下文类加载器或者系统类加载器,然后再使用Class.forName。
在ClassLoaderTest中编写。
//Class clazz = new MyClassLoader("itcastlib").loadClass("ClassLoaderAttachment");
//ClassLoaderAttachment d1 = (ClassLoaderAttachment)clazz.newInstance();
Class clazz = new MyClassLoader("itcastlib").loadClass("ClassLoaderAttachment");//如果要父类加载器加载是要有包名的
Date d1 = (Date)clazz.newInstance();
System.out.println(d1);
在Elcipse中,window菜单下选Show View --> Problems, 上面会说有什么错误。
实验步骤
对不带包名的class文件进行加密,加密结果存放到另外一个目录,例如:java MyClassLoader MyTest.class F:\itcast
运行加载类的程序,结果能够被正常加载,但打印出来的类加载器名称为AppClassLoader:java MyClassLoader MyTest F:\itcast
用加密后的类文件替换CLASSPATH环境下的类文件,再执行上一步操作就出问题了,错误说明是AppClassLoader类加载器加载失败。
删除CLASSPATH环境下的类文件,再执行上一步操作就没问题了。
三、类加载器的一个高级问题的实验分析
编写一个能打印出自己的类加载器名称和当前类加载器的父子结构关系链的MyServlet,正常发布后,看到打印结果为WebAppClassloader。
在MyEclipse中,new --> Web Project, 建一个itcastweb工程,在src下new--> Servlet,包名写成cn.itcast.itcastweb.web.servlets体现出清晰的层级 ,这里只勾选doGet就可以了。
其中有一个PrintWriter out对象,类似于System.out,但是这个out对象是输出到浏览器中的。
public class MyServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();// it out is explorer
ClassLoader loader = this.getClass().getClassLoader();
while(loader != null) {
System.out.println(loader.getClass().getName());
loader = loader.getParent();
}
out.close();
}
}
现在点部署按钮,就是把当前这个web project放进tomcat中去,配置好后,启动tomcat就可以访问这个项目了:这里由于我启动的是MyEclipse自带的tomcat,在Run/Stop/Restart MyEclipse Servers按钮中启动MyEclipse tomcat,然后打开浏览器输入localhost:8080/itcastweb/servlet/MyServlet就可以看到和视频一样的效果了。
把MyServlet.class文件打jar包,放到ext目录中,重启tomcat,发现找不到HttpServlet的错误。
把Servlet.jar也放到ext目录中,问题解决了,打印的结果是ExtclassLoader。
父级类加载器加载的类无法引用只能被子级类加载器加载的类
黑马程序员—类加载器的深入讲解与应用
最新推荐文章于 2014-07-14 14:25:35 发布