黑马程序员 java_类加载器
------- android培训、java培训、期待与您交流! ----------
类加载器(ClassLoader)
类加载器是负责加载类的对象。ClassLoader 类是一个抽象类。如果给定类的二进制名称,那么类加载器会试图查找或生成构成类定义的数据。一般策略是将名称转换为某个文件名,然后从文件系统读取该名称的“类文件”。简单可理解为类加载器可见将一个class文件,生成对象的字节码对象。当我们运行一个class文件时,系统会自动运行类加载器,我们也可以显示调用类加载器。
Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个类负责加载特定位置的类:BootStrap,ExtClassLoader,AppClassLoader。类加载器也是Java类,因为其他是java类的类加载器本身也要被类加载器加载,所以第一个类加载器不是java类,这正是BootStrap。
Java虚拟机中的所有类装载器采用具有父子关系的树形结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类装载器为其父级类加载。
类加载器之间的父子关系和管辖范围图
可以看出每一个类加载器都有对应的加载目录。此外我们可以自定义类加载器,用于加载用户指定的目录。
类加载器的委托机制
当Java虚拟机要加载一个类时,加载器的加载顺序。
首先当前线程的类加载器去加载线程中的第一个类。
如果类A中引用了类B,Java虚拟机将使用加载类A的类装载器来加载类B。
还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类装载器去加载类,这就是类加载器的委托模式。类装载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子孙类装载器去进行真正加载。当回退到最初的类装载器时,如果它自己也不能完成类的装载,那就应报告ClassNotFoundException异常。
package cn.itcast.day2;
public class ClassLoaderTest {
public static void main(String[] args) {
//获取ClassLoaderTest的类加载器
System.out.println(
ClassLoaderTest.class.getClassLoader().getClass().getName());
//获取System的类加载器
System.out.println(System.class.getClassLoader());
//获取类加载器的层次结构关系。
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
null
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null
可以看出ClassLoaderTest是由AppClassLoader加载,而System类的加载器打印的是null,在通过循环获取类加载器时,可以看出ExtClassLoader是AppClassLoader的父类,而ExtClassLoader再次为null,这是因为ExtClassLoader的父类BootStrap不是java类,所以打印出null。
自定义类加载器
主要方法
loadClass(String name) 使用指定的二进制名称来加载类。
findClass(String name) 使用指定的二进制名称查找类。
defineClass(String name, byte[] b, int off, int len) 将一个 byte 数组转换为 Class 类的实例。
在加载一个类时,首先需要调用loadClass方法,loadClass会去调用findClass,当类被找到后,使用defineClass返回一个字节码文件。其中loadClass方法会首先调用最顶层类加载器的findClass方法,如果类没找到,才会调用子类的findClass方法,直到最初的哪个类加载器的findClass方法,如果还是没找到类,则抛ClassNotFoundException。
自定义的类加载器的必须继承ClassLoader,此外还需复写findClass方法。
由于loadClass方法的目的就是调用findClass方法,且loadClass已经在ClassLoader中定义好,所以我们在自定义类加载器时不需要复写,而defineClass方法也已经在ClassLoader中定义好了,也不需要复写。
所以我们在自定义类加载器时只需要复写findClass方法即可。
练习
使用自定义加载器加载一个类。
编程步骤:
编写一个对文件内容进行简单加密的程序。
编写了一个自己的类装载器,可实现对加密过的类进行装载和解密。
编写一个程序调用类加载器加载类,在源程序中不能用该类名定义引用变量,因为编译器无法识别这个类。程序中可以除了使用ClassLoader.load方法之外,还可以使用设置线程的上下文类加载器或者系统类加载器,然后再使用Class.forName。
源文件
package cn.itcast.day2;
import java.util.Date;
public class ClassLoaderAttachment extends Date {
@Override
public String toString() {
// TODO Auto-generated method stub
return "hello world";
}
}
对源文件进行加密处理
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
//获取源文件和目的目录
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);
}
}
此处发现一个小问题,在上面代码中,如果传入的路径层级过多,就会被截断,造成路径不全,运行时抛出文件没有找到异常。
自定义类加载器,通过复写findClass方法,实现对加密文件的解密。
package cn.itcast.day2;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class MyClassLoader extends ClassLoader {
private static void cypher(InputStream ips,OutputStream ops) throws Exception{
int b=-1;
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.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(name,bytes,0,bytes.length);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public MyClassLoader(){
}
public MyClassLoader(String classDir){
//需要解密的文件的路径
this.classDir = classDir;
}
}
获取解密后的文件,此时需要先删除classPath中的ClassLoaderAttachment.class文件,如果不删,此文件将被AppClassLoader加载,这是由于委托机制造成的,此时被加载的ClassLoaderAttachment.class文件不是我们加密后的,而是eclipse自动生成的,所以应当删除。
package cn.itcast.day2;
import java.util.Date;
//通过此类的方法获取解密后的文件。
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
Class clazz =
new MyClassLoader("itcastlib").loadClass("cn.itcast.day2.ClassLoaderAttachment");
Date dl = (Date)clazz.newInstance();
System.out.println(dl);
}
}
如果让AppClassLoader加载加密后的文件,只需将加密后的文件复制到classPath中。运行时将报出异常。
表示AppClassLoader不能加载此类,只有自定义的加载器才能加载此加密类。
一个类加载器的高级问题分析
自定义一个web项目Myservlet需要在tomcat中运行,它父类为HttpServlet存在于tomcat中。当Myservlet运行时,发现它是由tomcat中的类加载器加载的。
如果我们想让java虚拟机中类加载器来加载Myservlet,可以将Myservlet的jar包放在JRE/lib/ext/目录下。但此时会发生异常,无法加载Httpservlet类,这是因为加载MyServlet的父类的加载器,需要和MyServlet一样,而HttpServlet不在JRE/lib/ext/目录下,所以无法加载,如果我们将HttpServlet的jar包也放到JRE/lib/ext/下,这样就可以加载了。
------- android培训、java培训、期待与您交流! ----------