第一部分:概述
一、定义
类加载器是负责加载类的对象。每个 Class 对象都包含一个对定义它的 ClassLoader 的引用。
二、作用
1、Java中用到一个类,JVM会先将类的字节码加载到内存中。通常字节码的原始信息放在classpath的指定目录下,class 文件的内容被类加载器加载、并进行相应处理后,得到字节码文件。
2、类加载器也是java类,因为其他是java类的加载器本身也要被类加载器加载,显然必须有第一个类加载器不是java类,就是 BootStrap。
示例代码:
package day.day20; /* * 类加载器 */ public class ClassLoaderDemo { public static void main(String[] args) { //获取当前类的类加载器,结果为 sun.misc.Launcher$AppClassLoader System.out.println(ClassLoaderDemo.class.getClassLoader().getClass() .getName()); /* * 获取System类的类加载器 * 结果为null 说明System类由BootStrap(不是类)加载 */ System.out.println(System.class.getClassLoader()); } }
三、类加载器的体系
Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个类负责加载特定位置的类。如图:
第二部分:委托管理机制
一、概述
当java虚拟机要加载一个类时,到底派哪个类加载器去加载呢?
1、首先派当前线程的类加载器去加载线程中的第一个类。
Thread类中有以下方法:
ClassLoader getContextClassLoader() 返回该线程的上下文 ClassLoader
void setContextClassLoader(ClassLoader cl) 设置该线程的上下文 ClassLoader
2、如果A类中引用了B类(调用或继承),Java虚拟机将使用加载A类的类加载器来加载B类。
3、通过直接调用Class类中的方法来指定某个类加载器去加载某个类。方法如下:
Class<?> loadClass(String name) 使用指定的二进制名称来加载类。
protected Class<?> loadClass(String name, boolean resolve) 使用指定的二进制名称来加载类
ClassLoader getClassLoader() 返回该类的类加载器
二、委托机制
1、每个类加载器加载类时,先委托给其上级类加载器。当所有(多级)父类加载器没有加载到类,则依次返回到发起者类加载器,还加载不到,则抛出ClassNotFoundException。
2、Java虚拟机中的所有类加载器采用具有父子关系的树形结构进行组织。
在实例化每个类加载器对象(ClassLoader)时,需要为其指定一个父级类加载器对象或者默认采用系统加载器为其父类加载。
ClassLoader方法:
protected ClassLoader(ClassLoader parent) 使用指定的、用于委托操作的父类加载器创建新的类加载器
ClassLoader getParent() 返回委托的父类加载器
3、委托机制的好处
确保内存中字节码的唯一性。
如底层A类加载器加载到了System类、B加载器也加载到了System类。子类加载器直接加载会导致内存中出现两份相同的字节码。而委托机制(给父类找)避免了这一情况,当A类与B类的父类已经加载过System类则其不会再次加载,也就不会出现多份字节码重复的现象。
注意:可不可与自己写一个类叫 java.lang.System
为了不让我们写System类,类加载器采用委托机制,这样可以保证父类加载器优先,也就是:总是使用父类加载类,这样总是使用java系统中提供的类System。但是可以自定义加载器来完成(撇开委托加载机制)。
第三部分:自定义类加载器
一、如何定义
1、自定义的类加载器必须继承ClassLoader(抽象类)
ClassLoader 类是一个抽象类。如果给定类的二进制名称,那么类加载器会试图查找或生成构成类定义的数据。一般策略是将名称转换为某个文件名,然后从文件系统读取该名称的“类文件”。
2、覆盖findClass方法
protected Class<?> findClass(String name) 使用指定的二进制名称(name=包名.类名)查找类
ClassLoader类中的其他方法:
Class<?> loadClass(String name) 使用指定的二进制名称来加载类(该方法中已经定义了委托机制)
protected Class<?> defineClass(String name, byte[] b, int off, int len) 将一个 byte 数组转换为 Class 类的实例
/* loadClass方法与findClass方法关系(模板方法模式) 自己写的类不用再写loadClass方法,因为loadClass内部会去找父类,父类返回来时,接着调用findClass() */ class ClassLoader{ public Class loadClass(String name){ ...; findClass(String name); ...; } protected abstract Class findClass(String name); }
二、示例代码
自定义类加载器:
被加密的类:package day.day20; /* * 自定义类加载器 */ import java.io.*; public class MyClassLoader extends ClassLoader{ //加密后的文件路径 private String path; public MyClassLoader(){} public MyClassLoader(String path){ this.path = path; } @Override//覆盖方法 protected Class<?> findClass(String name) throws ClassNotFoundException { try { FileInputStream in = new FileInputStream(path); ByteArrayOutputStream out = new ByteArrayOutputStream(); //对文件进行解密到out中 cypher(in, out); //将字节流中的数据转移到数组中 byte[] bytes = out.toByteArray(); //将解密后字节数组转换成Class类的实例 in.close(); return this.defineClass("day.day20.CypherDemo",bytes, 0, bytes.length); } catch (Exception e) { e.printStackTrace(); } return super.findClass(name);//调用父类的findClass } //对文件内容进行加密/解密 public static void cypher(InputStream in,OutputStream out) throws IOException{ int b = 0; while((b=in.read())!=-1){ out.write(b ^ 0xff);//异或加密 再次异或解密 } } public static void main(String[] args) throws Exception{ String srcPath = args[0]; String destPath = args[1]; //对某个类的class文件进行加密处理 FileInputStream in = new FileInputStream(srcPath); FileOutputStream out = new FileOutputStream(destPath); cypher(in,out); in.close(); out.close(); } }
测试类:package day.day20; import java.util.Date; /* * 被加密的类(故意定义的继承关系) */ public class CypherDemo extends Date{ public String toString(){ return "被加密的类运行成功"; } }
运行步骤:package day.day20; /* * 自定义类加载器 * 加载文件示例 */ import java.io.*; import java.util.*; public class ClassLoaderDemo { public static void main(String[] args) throws Exception{ //被加密前正常运行,被加密后运行出错(调用的是默认的加载器) // System.out.println(new CypherDemo()); //第一个路径:加密后的文件 第二个路径:加载的类名 Class clazz = new MyClassLoader("e:\\CypherDemo.class").loadClass("day.day20.CypherDemo"); /* * 这里不能直接写成被加密后的类的类名,因为类的class文件已经不再是原来的文件,而强转类型需要依赖于原来的数据。 * 因此强转为父类类型。 */ // CypherDemo c = (CypherDemo)clazz.newInstance();报错 Date c = (Date)clazz.newInstance(); System.out.println(c); } }
1、先通过测试类运行被加密文件的源文件,生成一个CypherDemo.class文件。
2、运行类加载器类,对CypherDemo.class文件进行加密。用加密后的文件覆盖CypherDemo.class原文件,则再用测试类运行该类时发生ClassFormatError
3、在自定义类加载器类MyClassLoader的findClass方法中对被加密后的数据进行解密并转成Class对象。
4、在测试类中用自定义类加载器加载被加密后的CypherDemo.class,并获取到字节码,再通过反射的方式调用类中的方法。
注意:
1、第4步中不能将反射后获取到的对象强转为CypherDemo,因为CypherDemo.class文件的数据已经被加密,而其父类Date是存在的。
2、完成上述工作后运行代码还是会出现ClassFormatError,这是因为存在委托管理机制,即父类类加载器优先加载。由于classpath路径下存在CypherDemo.class,该文件就会被AppClassLoader加载,由于数据已经被加密,因此加载时报错。
3、只有将classpath路径下的CypherDemo.class删除,自定义类加载器的父类们均未加载到该类,因此依次返回到发起者MyClassLoader,自定义的类加载器就会到指定的目录下去加载已经被解密后的类。
三、示例(二)
注意:
1、定义时:class MyServlet extends HttpServlet,程序正常运行时,MyServlet由WebAppClassLoader加载,因MyServlet类引用了HttpServlet,因此HttpServlet也由WebAppClassLoader加载。
2、当将MyServlet的jar包存放目录改为 JRE/lib/ext/ 下时,MyServlet由ExtClassLoader加载,故HttpServlet也由因ExtClassLoader加载。父类无法加载,ExtClassLoader能够加载MyServlet,但不能加载HttpServlet,故加载失败。
3、解决办法:将HttpServlet的jar包放在 JRE/lib/ext/ 下。