------- android培训、java培训、期待与您交流! ----------
类加载器:
总结:
我们知道,javac编译器编译源文件后会产生.class类文件,而该类文件是不能被直接运行的,它需要被一种叫类加载器的东西加载进内存产生字节码文件后才能创建对象、操作属性、方法等,而实现这一过程的就是类加载器。类加载器就是一个加载类文件的工具,类加载器也是一个java类,也需要被加载。所以,有一个顶层的类加载器,它就是BootStrap类加载器, BootStrap不是一个java类,不需要被任何类加载器加载,它是存在于java内核中底层用C++写的一个二进制文件,当java虚拟机启动的时候该加载器就会运行起来。 java虚拟机中可以安装多个类加载器,系统默认有三个主要的类加载器:BootStrap,ExtClassLoader,AppClassLoader。每个负责加载特定位置的类。
代码演示:
publicclass Test {
publicstaticvoid main(String[] args) throws Exception {
System.out.println(
Test.class.getClassLoader().getClass().getName()
);//打印sun.misc.Launcher$AppClassLoader,是Test类加载器的名字。getClassLoader可以得到Test类的类加载器对象,因此也可以得到字节码文件
System.out.println(
System.class.getClassLoader()//打印空。原因是System的类加载器是BootStrap,BootStrap不是一个java类,不能用java程序去获得。
);
ClassLoader loader = Test.class.getClassLoader();//获得Test类加载器
while(loader != null){
/*
* 这里第一次循环打印sun.misc.Launcher$AppClassLoader是Test类的类加载器
* 第二次打印sun.misc.Launcher$ExtClassLoader是Test类的类加载器的类加载器
* 这里有一个层级关系。
* */
}
System.out.println(loader);//打印空
System.out.println(loader.getClass().getName());
loader = loader.getParent();//得到父加载器,当循环到第二次时不能这样得到ExtClassLoader的类加载器,loader为空,再次判断时条件为假跳出循环
}
System.out.println(loader);
}
}
类加载器之间的父子关系和管辖范围
每个类加载器都有特定的管辖范围。比如AppClassLoader加载器专门加载CLASSPATH
指定的所有jar或目录,还可以定义一个类加载器去加载指定的目录。
类加载器的委托机制
当java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?
首先当前线程的类加载器去加载线程中的第一个类。
如果类A中引用了类B,java虚拟机将使用加载A的加载器去加载B。
还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
每个类加载器加载类时,又先委托给其上级类加载器(每个类加载器都要先给上级的父类先加载)
一级一级往上走,没有再一级一级往下返回来。
当所有祖宗加载器没有加载到类,回到发起者类加载器,还加载不了,则抛ClassNotFoundException,
不是再去找发起者类加载器的子类,因为根本没getChild方法,即使有,那么多个子类,找哪个一个呢?
就算我们派一个AppClassLoader去加载类,它也是先从上往下走,先给BootStrap 加载如果它的目录里没有这个类,再给ExtClassLoader加载,同理,没有最后再返回给AppClassLoader加载,再没有的话,就不会往下走了,就报异常了。每次都是先给父类加载,这是为了统一管理的好处。比如我们自定义的加载器先加载的话,那两个自定义的都加载了 就出现两份字节码了,所以要统一管理。
有一道面试题,能不能自己写个类叫java.lang.System类?
从上面的委托机制我们可以清楚地知道,爸爸们总是优先的,也就是总是使用爸爸们能找到的类,这样就总是使用java系统提供的System。
自定义类加载器:
自定义类加载器可以加载指定目录下的类,这样就可以摆脱委托机制 编写自己的类加载器了。并且在加载的过程中进行加密和解密的动作,让别的加载器加载不了,只能用自己定义的加载器去加载。让一个类成为类加载器必须继承抽象类ClassLoader,并且覆盖findclass方法。
也就是说子类继承ClassLoader后,都会走父类中的流程,父类会调用loadClass方法,如果在父类指定的目录下没找到会调用findClass方法,当子类覆盖findClass方法后调用的就是子类的findClass方法了,在子类的findClass方法中可以通过defineclass方法把指定的二进制数据转成字节码文件对象,来完成加载的动作。不覆盖loadClass的原因是因为要保留父类中的流程,我们不可能自己定义一套流程。
ClassLoader中的三个方法需要搞清:
findclass:说白了就是自己干,局部细节自己干,但还是要先去父类下查找,父类下不可能有自己定义的类,因此会让子类去加载。
loadClass:使用指定的二进制名称来加载类,并且实现以下顺序搜索类:1、调用 findLoadedClass(String)
来检查是否已经加载类。2、在父类加载器上调用 loadClass方法。如果父类加载器为null,则使用虚拟机的内置类加载器。 3、调用 findClass(String)方法查找类。
defineclass:将指定的class文件转换为字节码文件
代码演示:
ClassLoaderTest类
package cn.itcast.classLoader;
import java.util.Date;
publicclass ClassLoaderTest {
publicstaticvoid main(String[] args) throws Exception {
/*
* MyClassLoader是一个自定义类加载器,覆盖了findClass方法,会走父类中定义好的流程,指定加载itcastlib目录下的类,当调用loadClass方法后,
* 会先到父类下去查找指定的类文件(这可能是由于继承的关系,子类构造函数中都含有父类super引用,子类创建对象会先执行super(),),没找到后
* 调用findClass方法,由于自定义类加载器覆盖了findClass方法,因此会调用子类中的findClass方法,而子类中的findClass方法中在通过defineClass把指定的类文件
* 转成字节码文件对象。
*/
Class clazz = new MyClassLoader("itcastlib").loadClass("cn.itcast.calssLoader.ClassLoaderAttachment");
Date d1 = (Date)clazz.newInstance();//通过自定义类加载器加载后的字节码文件创建对象实例
System.out.println(d1);
}
}
ClassLoaderAttachment类
package cn.itcast.classLoader;
import java.util.Date;
publicclassClassLoaderAttachmentextends Date {
public String toString(){//覆盖toString方法
return"hello,itcast";
}
}
MyClassLoader类
package cn.itcast.classLoader;
import java.io.*;
publicclass MyClassLoader extends ClassLoader{
publicstaticvoid main(String[] args) throws Exception {
String src = args[0];//编译时传入的第一个参数,指需要加密的类文件(绝对路径)。
String des = args[1];//编译时传入的第二个参数,指产生新文件路径(相对路径)
String desFileName = src.substring(src.lastIndexOf("\\")+1);//截取类文件名称
String desPath = des+"\\"+ desFileName;//新文件路径/类文件名
FileInputStream fis = new FileInputStream(src);//创建指定文件字节输入流对象
FileOutputStream fos = new FileOutputStream(desPath);//创建指定文件字节输出流对象
cypher(fis,fos);//在流的转换过程中通过加密方法加密
fis.close();//关闭字节输入流
fos.close();//关闭字节输出流
}
//定义加密和解密方法
privatestaticvoid cypher(InputStream ips,OutputStream ops) throws Exception{
int b=-1;
while((b=ips.read())!=-1){
ops.write(b^0xff);//一个数异或同一个数两次还是这个数,实现加密和解密的过程
}
}
private String classDir;
//覆盖findClass方法
@Override
protected Class<?> findClass(String name) throwsClassNotFoundException {
String classFileName = classDir + "\\" +name.substring(name.lastIndexOf('.')+1) + ".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) {
e.printStackTrace();
}
returnnull;
}
public MyClassLoader(){
}
//该构造函数传入一个目录,即自定义加载器需要加载类的目录。
public MyClassLoader(String classDir){
this.classDir = classDir;
}
}
类加载器的一个高级问题的实验分析
过程分析:编写一个MyServlet类,该类继承HttpServlet类并覆盖doGet方法,在tomcat配置文件中指定好该类的路径,并发布到tomcat服务器中,tomcat加载的时候会读取配置文件的信息,就会用tomcat定义好的类加载器(WebAppClassLoader)去加载配置文件下的路径二进制文件。由于是继承关系,HttpServlet也是由 WebAppClassLoader所加载的。因此当用户发出Http请求时,浏览器解析请求的信息并发给tomcat服务器,tomcat得到用户要请求的信息便知道要加载的是哪个类文件了。但是,当我们把MyServlet类打成jar包后并放到ext目录中,由于该目录是jvm虚拟机ExtClassLoader加载器要查找的目录,虽然定义了自定义类加载器,但是在前面说过,还是会走父类中的流程,因此ExtClassLoader类加载器会找到MyServlet类文件并加载,tomcat中定义的加载器就加载不到MyServlet这个类文件了。ExtClassLoader在加载MyServlet类文件的时候会出现异常,原因就是MyServlet是继承HttpServlet的,在加载MyServlet类文件时也要加载HttpServlet类,但是etc目录下没有HttpServlet这个二进制文件,这时我们就要把tomcat中有HttpServlet类文件的jar包放到etc目录下,这是就又能正常运行了。原理图如下:
代码示例:
publicclassMyservlet extendsHttpServlet
{
public voiddoGet(HttpServletRequest request,HttpServletResponse response)throws ServletException,IOexception
{
response.setcontentType("text/html");//设置文本类型
PrintWriter out = response.getWriter();//获取文本输出流对象
ClassLoaderloader = this.getClass().getClassloader();//获取本类字节码文件的类加载器对象
while(loader!=null)
{
out.println(loader.getClass().getName() + "<br>"); //在浏览器中打印类加载器的名字,"<br>"是HTML的换行标签
loader =loader.getParent(); //获取类加载器的父类
}
out.colse();//关闭流
}
}