概念
在Java程序中要用到一个类就需要JVM虚拟机把这个类的.class文件加载到内存中,处理后变成字节码,这个过程就是类加载器完成的。简介
java.lang.ClassLoader类加载器是负责加载类的对象。ClassLoader 类是一个抽象类,如果给定类的二进制名称,那么类加载器会试图查找或生成构成类定义的数据。java.lang.ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。虚拟机的内置类加载器(称为 “bootstrap class loader”)本身没有父类加载器,但是可以将它用作 ClassLoader 实例的父类加载器。
数组类的 Class 对象不是由类加载器创建的,而是由 Java 运行时根据需要自动创建。数组类的类加载器由 Class.getClassLoader() 返回,该加载器与其元素类型的类加载器是相同的;如果该元素类型是基本类型,则该数组类没有类加载器。
系统默认的三个主要类加载器
1.引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。2.扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
3.系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器。可以写自己的类加载器来在new实例之前对class文件进行加密和解密。
首先当前线程的类加载器去加载线程中的第一个类。
如果类A中引用了类B,Java虚拟机将使用加载类A的类装载器来加载类B。
还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
每个类加载器加载类时,又先委托给其上级类加载器。
当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则抛ClassNotFoundException。
getParent() 返回该类加载器的父类加载器。
类加载器之间的父子关系和管辖范围图
类加载器的委托机制
当Java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?首先当前线程的类加载器去加载线程中的第一个类。
如果类A中引用了类B,Java虚拟机将使用加载类A的类装载器来加载类B。
还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
每个类加载器加载类时,又先委托给其上级类加载器。
当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则抛ClassNotFoundException。
ClassLoader类常见应用
java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,比较重要的方法如下所示。getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
resolveClass(Class< ?> c) 链接指定的 Java 类。
实验步骤:
对不带包名的class文件进行加密,加密结果存放到另外一个目录,例如: java MyClassLoader MyTest.class F:\itcast
运行加载类的程序,结果能够被正常加载,但打印出来的类装载器名称为AppClassLoader:java MyClassLoader MyTest F:\itcast
用加密后的类文件替换CLASSPATH环境下的类文件,再执行上一步操作就出问题了,错误说明是AppClassLoader类装载器装载失败。
删除CLASSPATH环境下的类文件,再执行上一步操作就没问题了。
findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
resolveClass(Class< ?> c) 链接指定的 Java 类。
自定义类加载器
通过以上知识点的学习,编写了一个自己的类装载器,可实现对加密过的类进行装载和解密。实验步骤:
对不带包名的class文件进行加密,加密结果存放到另外一个目录,例如: java MyClassLoader MyTest.class F:\itcast
运行加载类的程序,结果能够被正常加载,但打印出来的类装载器名称为AppClassLoader:java MyClassLoader MyTest F:\itcast
用加密后的类文件替换CLASSPATH环境下的类文件,再执行上一步操作就出问题了,错误说明是AppClassLoader类装载器装载失败。
删除CLASSPATH环境下的类文件,再执行上一步操作就没问题了。
public class MyClassLoader extends ClassLoader{
/**
* @param args
*/
public static void main(String[] args) throws Exception {
// 在Main函数的Arguments里设置加载类的路径和加密过后的目标目录
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);
}
}
private String classDir;
@Override
//实现findClass方法达到自定义类加载器作用效果
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String classFileName = classDir + "\\" + name.substring(name.lastIndexOf('.')+1) + ".class";
try {
FileInputStream fis = new FileInputStream(classFileName);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//解密过程
cypher(fis,bos);
fis.close();
System.out.println("aaa");
byte[] bytes = bos.toByteArray();
return defineClass(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;
}
}
一个类加载器的高级问题分析
编写一个能打印出自己的类加载器名称和当前类加载器的父子结构关系链的MyServlet,正常发布后,看到打印结果为WebAppClassloader。把MyServlet.class文件打jar包,放到ext目录中,重启tomcat,报发现找不到HttpServlet的错误。
把servlet.jar也放到ext目录中,问题解决了,打印的结果是ExtclassLoader 。
结论:父级类加载器加载的类无法引用只能被子级类加载器加载的类,原理如下图: