概述
public abstract class ClassLoaderextends Object类加载器是负责加载类的对象。ClassLoader 类是一个抽象类。如果给定类的二进制名称,那么类加载器会试图查找或生成构成类定义的数据。一般策略是将名称转换为某个文件名,然后从文件系统读取该名称的“类文件”。
所谓类加载器,顾名思义,就是加载类的工具,它的作用是将Java的字节码文件加载到内存中
JVM中可以安装多个类加载器,系统默认的有三个类加载器,
BootStrap,ExtClassLoader,AppClassLoader
ExtClassLoader与AppClassLoader都是一个java类,所以必须有一个类加载器来加载这两个类加载器,正是BootStrap
类加载器的委托机制
JVM中所有的类加载器采用具有父子关系的树状结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类装载器为其父级类加载。
当Java虚拟机要加载一个类时,究竟会用哪一个类加载器去加载?
首先当前线程的类加载器去加载线程中的第一个类。
如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器来加载类B。
还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
每个类加载器加载类时,又先委托给其上级类加载器。
当所有父类加载器没有加载到类,回到发起者类加载器,还加载不了,则抛ClassNotFoundException异常
由于这个机制,最好将有继承关系的一些类字节码文件放到同一级目录下,确保加载A类的加载器,不会因为找不到与之相关的B类而报错
类加载器之间的父子关系和管辖范围图
下面看一个例子
1: public static void main(String[] args) {
2:
3: //自己定义的类存放在classpath中,所以由appClassLoader来加载
4: ClassLoader cl = ClassLoaderTest.class.getClassLoader();
5: while(cl != null){
6: System.out.println(cl.getClass().getName());
7: cl = cl.getParent();
8: }
9:
10: //由于这些系统lib中的类是由BootStrap加载的,它并不是一个类,所以无法获取ClassLoader
11: System.out.println(System.class.getClassLoader());
12: System.out.println(Date.class.getClassLoader());
13: }
定义自己的类加载器
ClassLoader类实现了将硬盘的字节码文件分析其结构,并加载到内存的操作
其中有两个方法
loadClass与findClass
loadClass()封装了前面提到的委托模式的实现。该方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。
findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。
这其实是利用的模板设计方法,ClassLoader将写好的委托模式代码放在loadClass中,而具体的加载的过程代码放在findClass中,由子类自行定义
在定义自己的类加载器的时候,我们还可以实现对字节码文件的加密解密操作
首先,先定义自己的加密器
1: /**
2: * @author Shawn
3: * 加密器
4: */
5: public class Encipher {
6: private String myLibDir;
7: private String srcDir;
8: private int encryCode = 0xff;
9:
10: public Encipher(String myLibDir, String srcDir) {
11: super();
12: this.myLibDir = myLibDir;
13: this.srcDir = srcDir;
14: }
15:
16: //加密,并将加密后的文件放入自己的libDir中
17: public void encipher(String name) throws Exception{
18: buildEntryLibDir(name);
19: String encryFile = classNameToPath(myLibDir, name);
20: String srcFile = classNameToPath(srcDir,name);
21:
22: FileInputStream fis = new FileInputStream(srcFile);
23: FileOutputStream fos = new FileOutputStream(encryFile);
24:
25: int b = -1;
26: while( ( b = fis.read()) != -1){
27: fos.write(b ^ encryCode);
28: }
29:
30: fis.close();
31: fos.close();
32: }
33:
34: private String classNameToPath(String dir,String className) {
35: return dir + File.separatorChar
36: + className.replace('.', File.separatorChar) + ".class";
37: }
38:
39: private void buildEntryLibDir(String className){
40: String path = myLibDir + File.separatorChar + className.substring(
41: 0,
42: className.lastIndexOf("."));
43: File libDir = new File(path);
44: if(!libDir.exists())
45: libDir.mkdirs();
46: }
47: }
接下来,对已经写好一个测试类进行加密
1: public class Code {
2:
3: @Override
4: public String toString(){
5: return "Hello,world!";
6: }
7: }
1: public class ClassLoaderTest2 {
2:
3: /**
4: * @param args
5: * @throws Exception
6: */
7: public static void main(String[] args) throws Exception {
8: String myLibDir = "D:";
9: String srcDir = "bin";
10: Encipher en = new Encipher(myLibDir,srcDir);
11: en.encipher("classloader.Code");
12: }
13: }
这个时候,如果将自己lib下的Code.class文件覆盖到原来目录,那么编译器就会报错
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Code cannot be resolved to a type
说明此类已经不能用JVM自己的类加载器解析了
好了,现在定义自己的类加载器
1: public class MyClassLoader extends ClassLoader {
2:
3: //类加载器需要加载的类文件存放目录
4: private String rootDir;
5:
6: public MyClassLoader(String rootDir) {
7: super();
8: this.rootDir = rootDir;
9: }
10:
11:
12: //重写Classloader中findClass方法
13: @Override
14: protected Class<?> findClass(String name) throws ClassNotFoundException{
15: byte[] b = getBytes(name);
16: if(b == null)
17: throw new ClassNotFoundException();
18: else
19: //将读到的数据转为字节码对象,并返回
20: return defineClass(name,b, 0, b.length);
21: }
22:
23: private byte[] getBytes(String name) {
24: String fileName = classNameToPath(name,rootDir);
25: //定义字节数组流,将读到的数据写到字节数组中
26: ByteArrayOutputStream baos = new ByteArrayOutputStream();
27:
28: FileInputStream fis = null;
29:
30: try {
31: fis = new FileInputStream(fileName);
32:
33: int b = -1;
34: while((b = fis.read()) != -1){
35: baos.write(b^0xff);
36: }
37: } catch (FileNotFoundException e) {
38: // TODO Auto-generated catch block
39: e.printStackTrace();
40: } catch (IOException e) {
41: // TODO Auto-generated catch block
42: e.printStackTrace();
43: }
44: return baos.toByteArray();
45: }
46:
47: //将带有包名的className转为路径
48: private String classNameToPath(String className,String dir) {
49: return dir + File.separatorChar
50: + className.replace('.', File.separatorChar) + ".class";
51: }
52: }
这个时候,只要将原来目录的Code.class文件删除,确保JVM自己的类加载器找不到这个类,我们就可以做下面的测试,自己加载自己的类了
1: public class ClassLoaderTest2 {
2:
3: /**
4: * @param args
5: * @throws Exception
6: */
7: public static void main(String[] args) throws Exception {
8: String myLibDir = "D:";
9:
10: MyClassLoader mcl = new MyClassLoader(myLibDir);
11: Class cls = mcl.loadClass("classloader.Code");
12: System.out.println(cls.newInstance());
13: }
14: }