黑马程序员——基础加强(类加载器)

时间:2021-10-08 11:32:45

-----------android培训java培训、java学习型技术博客、期待与您交流!------------


1、定义:简单说,类加载器就是加载类的工具。

        在java程序中用到一个类,出现了这个类的名字。java虚拟机首先将这个类的字节码加载到内存中,通常这个类的字节码的原始信息放在硬盘上的classpath指定的目录下,把.class文件的内容加载到内存里面来,再对它进行处理,处理之后的结果就是字节码。这些工作就是类加载器在操作。

2、类加载器作用:将.class文件中的内容变为字节码加载进内存。

3、默认类加载器:

        1)Java虚拟机中可安装多个类加载器,系统默认的有三个主要的,每个类负责加载特定位置的类:BootStrap、ExtClassLoader、AppClassLoader

        2)类加载器本身也是Java类,因为它是Java类的加载器,本身也需要被类加载器加载,显然必须有第一个类加载器而不是java类的,这正是BootStrap。它是嵌套在Java虚拟机内核中的,已启动即出现在虚拟机中,是用c++写的一段二进制代码。所以不能通过java程序获取其名字,获得的只能是null。

4、Java虚拟机中的所有类加载器采用父子关系的树形结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类装载器为其父级类加载。

5、类加载器之间的父子关系和管辖范围图

黑马程序员——基础加强(类加载器)

二、类加载器的委托机制

1、每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类加载器去加载类,这就是类加载器的委托模式。

2、加载类的方式

        当Java虚拟机要加载一个类时,到底要用哪个类加载器加载呢?

         1)首先,当前线程的类加载器去加载线程中的第一个类。

         2)若A引用类B(继承或者使用了B),Java虚拟机将使用加载类的类加载器来加载类B

         3)还可直接调用ClassLoaderLoaderClass()方法,来指定某个类加载器去加载某个类。

2、每个类加载器加载类时,又先委托给上级类加载器。

        类装载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子孙类加载器去进行加载。当回退到最初的发起者类装载器时,如果它自己也不能完成类的装载,那就会抛出ClassNotFoundException异常。这时就不会再委托发起者加载器的子类去加载了,如果它还有子类的话。

        简单说,就是先由发起者将类一级级委托为BootStrap,从父级开始找,找到了直接返回,没找到再助剂让其子级找,直到发起者,再没找到就报异常。

3、委托机制的优点:可以集中管理,不会产生多字节码重复的现象。

补充:面试题

        可不可以自己写个类为:java.lang.System呢?

        回答:第一、通常是不可以的,由于类加载器的委托机制,会先将System这个类一级级委托给最*的BootStrap,由于BootStrap在其指定的目录中加载的是rt.jar中的类,且其中有System这个类,那么就会直接加载自己目录中的,也就是Java已经定义好的System这个类,而不会加载自定义的这个System

        第二、但是还是有办法加载这个自定义的System类的,此时就不能交给上级加载了,需要用自定义的类加载器加载,这就需要有特殊的写法才能去加载这个自定义的System类的。

三、自定义类加载器

1、自定义的类加载器必须继承抽象类ClassLoader,要覆写其中的findClass(String name)方法,而不用覆写loadClass()方法。

2、覆写findClass(Stringname)方法的原因:

        1)在loadClass()内部是会先委托给父级,当父级找不到后返回,再调用findClass(String name)方法,也就是你自定义的类加载器去找。所以只需要覆写findClass方法,就能实现用自定义的类加载器加载类的目的。

        因为,一般自定义类加载器,会把需要加载的类放在自己指定的目录中,而java中已有的类加载器是不知道你这个目录的,所以会找不到。这样才会调用你复写的findClass()方法,用你自定义的类加载器去指定的目录加载类。

        2)这是一种模板方法设计模式。这样就保留了loadClass()方法中的流程(这个流程就是先找父级,找不到再调用自定义的类加载器),而我们只需复写findClass方法,实现局部细节就行了。

        ClassLoader提供了一个protected  Class<?>defineClass(String name, byte[] b, int off, int len)方法,只需要将类对应的class文件传入,就可以将其变为字节码。

3、编程步骤:

        1)编写一个对文件内容进行简单加密的程序

        2)编写好了一个自己的类加载器,可实现对加密过来的类进行加载和解密。

        3)编写一个程序,调用类加载器加载类,在源程序中不能用该类名定义引用变量,因为编译器无法识别这个类,程序中除了可使用ClassLoaderloadClass方法外,还可以使用设置线程的上下文类加载器或系统类加载器,然后再使用Class.forName

4、编码步骤:

        1)对不带包名的class文件进行加密,加密结果存放到另外一个目录,例如: java MyClassLoader MyTest.class F:\itcast

        2)运行加载类的程序,结果能够被正常加载,但打印出来的类装载器名称为AppClassLoaderjava MyClassLoader MyTest F:\itcast

        3)用加密后的类文件替换CLASSPATH环境下的类文件,再执行上一步操作就出问题了,错误说明是AppClassLoader类装载器装载失败。

        4)删除CLASSPATH环境下的类文件,再执行上一步操作就没问题了。

package haHa.lib;

import java.io.*;

//自定义一个类加载器
/*
*类加载器的工作流程:类加载器有委托机制,当要加载某类时会逐级的向上委托父类去查找该类:也就是说如果我们要加载一个我们自定义的一个类并且把它放在一个普通的目录里,虚拟机在加载该类时会先用当前线程里的类加载器
*加载该类,当前线程的类加载器会先将该类委托给他的父类去加载,他的父类又会委托他的父类去加载直到将该类委托给他们的根加载器BootStrap,就这样BootStrap会在所能查找的路径里查找该类,如果找不到就会将该
*加载命令逐级的返回如果找到该类就由找到者进行加载如果找不到就会逐级的返回直到命令发起者记载器。如果命令发起者也找不到该类那么虚拟机就会抛出找不到该类异常。
*委托机制是在loadClass()方法中来完成的:
*查找API可以看出:loadClass():protected Class<?> loadClass(String name,
boolean resolve)
throws ClassNotFoundException
使用指定的二进制名称来加载类。此方法的默认实现将按以下顺序搜索类:

1. 调用 findLoadedClass(String) 来检查是否已经加载类。


2. 在父类加载器上调用 loadClass 方法。如果父类加载器为 null,则使用虚拟机的内置类加载器。


3. 调用 findClass(String) 方法查找类。


如果使用上述步骤找到类,并且 resolve 标志为真,则此方法将在得到的 Class 对象上调用 resolveClass(Class) 方法。

鼓励用 ClassLoader 的子类重写 findClass(String),而不是使用此方法。

* */
class LoaderTest1{
public static void main(String[] args) throws Exception {

//使用自定义的类加载器加载指定的类
Class myloader=new MyClassLoader("cn.heima.day9-13").loadClass("haHa.lib.EnumTest");
EnumTest et=(EnumTest)myloader.newInstance();//创建实例对象
}

}
//自定义一个类加载器
class MyClassLoader extends ClassLoader{//自定义的类加载器都要继承ClassLoader
//因为有委托机制的存在,并且查找API可知委托机制是在LoadClass()方法来完成的
//所以在自定义类加载器的时候直接定义findLass()方法就可以,父类在他们管辖的路径下找不到就会返回自定义的加载器来找要加载的类
private String dirpath;
public MyClassLoader(String name){
this.dirpath=name;
}
@Override
protected Class<?> findClass(String name){
name =name.substring(name.lastIndexOf('.')+1);//类的名字是包名.类名所以这句可以讲类名提取出来
String classfieldname=name+".class";//完整类文件名
//将类文件中的数据通过字符流记载进内存
InputStream ips=null;
try {
ips = new FileInputStream(classfieldname);
} catch (FileNotFoundException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
ByteArrayOutputStream bos=new ByteArrayOutputStream();//创建存储流中字节的数组
try {
ips.close();
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}//关闭此文件输入流并释放与此流有关的所有系统资源;
byte[] by=bos.toByteArray();//创建一个新分配的 byte 数组。其大小是此输出流的当前大小,并且缓冲区的有效内容已复制到该数组中
return defineClass(null, by, 0, by.length);
}
//增加一个加密方法
//加密方法过程:将文件中的每个字节和某个数字异或后写出
public void fun(InputStream ips,OutputStream ops) throws IOException{
//ips.read();
int b =-1;
while((b=ips.read())!=-1){
ops.write(b);
ops.flush();
}
ops.close();

}

}