黑马程序员——类加载器

来源:互联网 发布:nginx 根据ip转发 编辑:程序博客网 时间:2024/04/29 01:27
------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------


1  类加载器概述

1.1  简介

       所谓类加载器,就是加载类的工具。我们在前面的博客中曾将反复说道,当使用到了某个类的静态成员(比如,静态方法、静态成员变量,以及构造方法。虽然没有明确定义,但构造方法也是静态方法),Java虚拟机就会将这个类加载到内存中。那么加载这个类的过程,包括了在classpath中寻找这个类对应的“.class”文件,对该“.class”文件进行读取,并对其进行一定地处理,最终将其转换为字节码对象(Class对象),缓存到堆内存中等一些列操作,而这些操作实际就是由类加载器完成的。所以一言以蔽之,Java中所有类的加载工作实际就是由类加载器实现的。

       实际上,大多数类加载器也是Java类(除了一种,下面会讲到),因此类加载器也是需要其他类加载器来加载的,那么显然总有一个类加载器不是Java类(否则类加载操作将是一个无限循环),这就是唯一不是由Java语言编写的类加载器——BootStrap。BootStrap类加载器是由C++语言编写的,嵌套在Java虚拟机内核中的,当Java虚拟机启动时,BootStrap类加载器也就同时在工作了,它的工作就是去加载那些最底层的一些Java类,比如其他类加载器,然后再由这些类加载器去加载其他类。

       在Java虚拟机中安装有多个类加载器,系统默认的主要有三个,除了上面提到的BootStrap以外,系统默认的还有ExtClassLoader和AppClassLoader类加载器,这三种类加载器专门负责加载特定路径下的类。我们将在后面的内容中对这三种类加载器分别进行介绍。

1.2  简单演示

       我们编写一个简单的测试类,获取这个测试类的类加载器,并打印它的类名,代码如下。

代码1:

public class ClassLoaderTest {public static void main(String[] args) {test();}private static void test(){//获取此测试类的类加载器String className = ClassLoaderTest.class.getClassLoader().getClass().getName();System.out.println(className);       //获取System类的类加载器ClassLoader classLoader = System.class.getClassLoader();System.out.println(classLoader);}}
执行结果为:

sun.misc.Launcher$AppClassLoader

null

代码说明:   

       (1)  从执行结果可知,测试类ClassLoaderTest是由AppClassLoader加载的。以上代码中getClassLoader方法的作用是返回加载此Class对象的类加载器对象,其API文档如下,

       public ClassLoader getClassLoader():返回该类的类加载器。有些实现可能使用null来表示引导类加载器(BootStrap)。如果该类由引导类加载器加载,则此方法在这类实现中将返回null。

       (2)  正是由于(1)中getClassLoader的API文档所说的原因,加载System类的类加载器就是BootStrap,因此调用System类Class对象的getClassLoader方法的返回值是null。这并不代表说System类没有对应的类加载器,而是代表它是由非Java类类加载器BootStrap加载的。

       通过以上的演示代码,简单了解了获取一个类对应的类加载器的方法,并说明了不同类是由不同的类加载器加载的。那么为什么不同的类需要由不同的类加载器加载,三种默认类加载器之间的关系又是怎样的呢?回答这个问题就需要引入类加载器的委托加载机制,下面我们将详细介绍这一机制的运作原理。

1.3  类加载器的委托加载机制

1)  三种默认类加载器的分工

       前面提到的三种类加载器之间是按照一种类似于父子关系的形式,组织成树状结构,而BootStrap显然是根节点,ExtClassLoader是BootStrap的子节点,AppClassLoader又是ExtClassLoader的子节点。今后在实际开发中,我们可能会自定义类加载器,那么这些自定义类加载器通常又是AppClassLoader的子节点。为了验证上述的树状结构,可以使用以下代码进行验证。

代码2:

public class ClassLoaderTest {public static void main(String[] args) {test();}private static void test(){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

sun.misc.Launcher$ExtClassLoader

null

代码说明:

       以上代码的关键就是通过调用类加载器对象的getParent方法获取到此类加载器的父类加载器,getParent方法的API文档为,

       public final ClassLoader getParent():返回委托的父类加载器。一些实现可能使用null来表示引导类加载器。如果类加载器的父类加载器就是引导类加载器,则此方法将在这样的实现中返回null。那么加载测试类ClassLoaderTest类的是AppClassLoder,而AppClassLoader的父类加载是ExtClassLoder,而最终打印的null表示ExtClassLoader的父类加载器就是BootStrap。

       那么以上所说的类加载器树状结构可表示为如下结构图。


上图左侧表示的就是不同类加载器之间以子父类形式组成的额树状结构,而右侧表示的是不同类加载器负责加载的类范围。比如BootStrap负责加载rt.jar包中的类(几乎所有的Java标准类库中的类都在rt.jar包中),ExtClassLoader则负责加载jre/lib/ext目录下的指定类,AppClassLoader类则专门负责由程序员自己编写并存放到Classpath目录下的类(也包括jar包中的类)。由此可知,在通常情况下,我们自己编写的类都是由AppClassLoader加载的。那么ExtClassLoader又是如何工作的呢?

2)  ExtClassLoader的作用

       为了演示ExtClassLoader的类加载效果,可以将某个自定义类打包为“.jar”文件,并存方法到jar/lib/ext/目录下,打包方法为,以代码2中的测试类ClassLoaderTest为例,在eclipse软件项目视图下,右键单击ClassLoaderTest,选择“Export”,在打开的“Export”对话框中,以此选择“Java”——>“JARfile”,然后点击“Next”。在随后打开的“JAR Export”对话框的“Selectthe export destination”栏中的“JAR file”中指定输出路径,也就是jre/lib/ext目录(若安装有多个Java版本,一定要注意输出到当前工作空间使用的JDK版本的路径下),并为jar包指定一个名称,比如“itcast.jar”,最后点击“Finish”即完成将“.class”打包为“.jar”文件的操作,类加载器可以自动加载“.jar”包中的类。

       将测试类ClassLoaderTest导出至jre/lib/ext目录下以后,再次执行代码2,执行结果为,

sun.misc.Launcher$ExtClassLoader

null

       从结果来看,原来需要由AppClassLoader加载的ClassLoaderTest类,现在默认地被ExtClassLoader所加载,而我们所作唯一改动就是将ClassLoaderTest类“.class”文件从Classpath目录转移到了jre/lib/ext目录中,那么由于ExtClassLoader专门负责加载这一路径下的类,因此ClassLoaderTest不再被AppClassLoader类加载,而是默认地被ExtClassLoader加载了。

       实际上ext目录是extend的缩写,意思是扩展。也就是说,我们可以将自定义的常用工具类以“.jar”包形式存放到ext目录中,作为对Java标准类库中类功能的一个扩展和补充。当我们在Java标准类库中,或者其他第三方类库中找不到所需功能的类时,就可以自定义一个工具类,并将其达到为“.jar”文件并存储到jre/lib/ext目录下。

3)  类加载器委托加载机制

       至此大家可能会有一个这样的疑惑:虽然我们将测试类ClassLoaderTest的“.class”文件复制了一份到jre/lib/ext目录下,但是原来的Classpath路径下也还是有一份ClassLoaderTest的“.class”文件的,那么为什么Java虚拟机会调用ExtClassLoader去到ext目录加载,而不调用AppClassLoader去到Classpath指定的路径下加载呢?从表面上,子父类加载器之间似乎有优先级的区别,那么这里就涉及到了类加载器的委托加载机制。

       假设我们要自定义一个类加载器,比如定义类名叫MyClassLoder,如上文所说,自定义类加载器通常都要成为AppClassLoader的子节点,也就是要挂到父类加载器上,这一点可以通过ClassLoader的构造方法体现,如下所示。

       protected ClassLoader(ClassLoader parent):使用指定的、用于委托操作的父类加载器创建新的类加载器。参数parent指地就是父类加载器。

       protected ClassLoader():使用方法getSystemClassLoader()返回的ClassLoader创建一个新的类加载器,将该加载器作为父类加载器。也就是说,即使没有手动指定一个父类加载器,系统也会默认为其指定一个。

       那么前述三种默认类加载器,以及自定义类加载就可以通过上述指定父类加载器的方式,形成一个树状结构。那么在说明这个树状结构的工作原理之前,我们先来了解一下单个类加载器的工作原理。当Java虚拟机需要加载一个类A的时候,系统默认首先调用当前线程的类加载器去尝试加载A。当前线程的类加载器负责加载执行该线程中的代码时需要被加载加载的类和资源,如果没有特殊指定,当前线程的类加载器就是加载本类(包含有main方法的可执行类)“.class”文件的类加载器,也称为上下文类加载器(ContextClassLoader)。通常这个上下文类加载器默认是AppClassLoader,这可以通过在自定义类内部调用ClassLoader类的静态方法getSystemClassLoader来验证。Thread类中提供了获取/设置这一默认类加载器的方法getContextClassLoader/setContextClassLoader,获取和设置的类加载器称为上下文加载器,由它负责线程类的加载,以及线程在执行过程中需要加载的其他类。

       那么假如线程默认的类加载器(也就是上下文类加载器)尝试加载类A时,发现类A还有父类B,那么类加载器加载完类A以后(创建类A对应的Class对象)就会转而去加载类B,直至加载到根父类为止。那么当某个类加载器成功加载到类A以后,与类A相关的所有类(包括类A的所有父类,以及类中调用的其他类)都将由该类加载器去完成加载操作。如果我们不希望,由系统默认的类加载器加载类A及其相关类,也可以自定义类加载器,并调用其loadClass方法来调用加载指定类。

       以上所说的是单个类加载器的类加载工作原理。那么上述树状结构又是如何工作的呢?假设AppClassLoader就是此线程的默认类加载器,由它去完成对类A的加载,那么由类加载器的委托加载机制决定了,AppClassLoader不会自己先去Classpath路径下去加载指定类,而是委托其父类加载器ExtClassLoader,而ExtClassLoader进而委托BootStrap,然后由BootStrap首先去到jre/lib/rt.jar包中查找类A的“.class”文件是否存在,如果存在则加载类A,那么与类A相关的类则全部由BootStrap负责加载;如果不存在,则向下委托给子类加载器ExtClassLoader去到jre/lib/ext目录下去查找,若查找成功则类A及其相关类则由ExtClassLoader来加载;如果还是查找不到,则继续向下委托给AppClassLoader去到Classpath路径下查找是否存在类A,如果存在则加载,如果不存在,此时即使AppClassLoader类加载器下还挂有其他子类加载器,也不会进一步委托子类加载器了,而是抛出不存在指定类的异常,因为AppClassLoader是类加载操作的发起者,如果连发起者本身都找不到指定类,则不再向下委托子类加载器加载,而是抛出ClassNotFoundException异常。以上描述的过程,就是类的委托加载机制

       从上述类的委托加载机制可以看出,几乎每加载一个类都会调用到BootStrap去进行类“.class”文件的查找工作,甚至ExtClassLoader的调用频率也是很高的。为什么要设定如此复杂而繁琐的类加载机制呢?假如AppClassLoader下挂有多个自定义类加载器,那么在程序执行过程中,可能需要由多个类加载器,多次加载同一个类(比如System类),如果不遵循委托加载机制,那么就有可能在内存中创建多个指定类的字节码对象(Class对象),这就违背了字节码对象唯一性的原则,就有可能出现错误。而委托加载机制,通过不断委托给父类加载器的方式,规避了这一问题的发生——父类加载器在尝试加载某个类之前,首先会判断之前是否加载过同一个类,如果曾经加载过,则直接返回内存中的那一份字节码对象即可,既完成了类加载操作,又保证了字节码对象的唯一性。

       上述类的委托加载机制也就解释了,当把ClassLoaderTest类的“.class”文件存放到ext路径下时,将自动由ExtClassLoader加载,而不再由AppClassLoader加载的原因了:因为如果父类加载器能够加载到指定类,则不再向下委托给子类加载了。

 

小知识点1:

       有一个面试题是这样问的:能不能自定义一个System类?答:通常不可以。因为即使自己编写了一个System类,也只能存放到ext目录或者Classpath路径,而根据类的委托加载机制,首先会由根类加载器BootStrap去到jre/lib/rt.jar包中去加载System类,因此我们的自定义System类是不能被加载到的。唯一的方法就是自定义一个类加载器,并在复写ClassLoader类实现委托加载机制的方法时,去掉委托加载代码,即可由自定义类加载器完成对自定义System类的加载。

 

2  自定义类加载器

       了解了类加载器的作用、分类,以及委托加载机制,我们就可以尝试着自己编写一个类加载器了。

需求:编写一个名为MyClassLoader的自定义类加载器,专门加载某个特定路径下的类,这个特定路径下的类只能由MyClassLoader去加载,而不能由其他类加载器去加载,并且,MyClassLoader会能够对所加载的类进行加密操作,即使别的类加载器能够访问到特定路径由于不知道解密原理,还是无法读取此特定路径下的类。

       我们就按照上述需求去编写一个自定义类加载器,在编写以前,首先要了解自定义类加载器的定义规则。

2.1  自定义类加载器的相关准备

       在自定义类加载器以前,需要先了解相关的一些知识。

       首先,自定义类加载器需要复写ClassLoader类。当然,在继承ClassLoader的同时,还需要复写一些关键方法,才能实现上述需求。关键方法有两个,分别是loadClass以及findClass,其中findClass是被loadClass调用的。这两个方法的API文档如下。

       public Class<?> loadClass(String name) throws ClassNotFoundException:使用指定的二进制名称来加载类。参数name就是需要被加载的类的字符串形式全类名。通过调用此方法,传递字符串形式的类名,则返回该类的字节码对象。

       protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException:使用指定的二进制名称来加载类。此方法的默认实现将按以下顺序搜索类:

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

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

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

       protected Class<?> findClass(String name) throws ClassNotFoundException:是用指定的二进制名称查找类。此方法应该被类加载器的实现重写,该实现按照委托加载模型来加载类。在通过父类加载器检查所请求的类后,此方法将被loadClass方法调用。默认实现抛出一个ClassNotFoundException。

       由此可知,委托加载机制的实现代码正是定义在了loadClass方法中,只要在复写此方法时不实现委托加载机制,也就是不去调用父类加载器上的loadClass方法,而直接调用findClass方法即可绕过委托加载机制,直接由自定义类加载器在指定路径下,加载指定类。

       这种方法理论上虽然可行,但是经实际测试发现,由于所有类都是Object的子类,因此在加载所需类以前,必须先加载根父类Object,这通常是由BootStrap去完成,但如果在复写loadClass方法时去掉了委托加载机制,那么根据单个类加载器的类加载原理:当类加载器在指定路径中加载到所需类的“.class”文件以后,与该类相关的所有类,比如父类,都将由此类加载器发起加载操作。所谓发起加载操作,并不是指这些相关类真的只由此类加载器加载,而是同样遵循委托加载机制的方式进行相关类的加载操作。但是在上述例子中,由于完全去掉了委托加载机制,那么所有相关类都只能由此类加载器加载。但是这其中的Object类的“.class”文件并不存在于自定义类加载器的特定加载路径中,因此就会抛出找不到“Object.class”的异常,因此不能通过直接绕过委托加载机制的方式实现自定义类加载器加载指定路径下指定“.class”文件的需求。关于这一问题的解决方法我们将在后面的2.3节中进行说明。

       当然findClass方法是必须要复写的,因为是在该方法内部定义去哪里查找类的“.class”文件,并以什么样的方式加载。那么loadClass方法与findClass方法的定义就体现了模板方法设计模式。由父类的某个方法,比如loadClass方法,来定义功能实现的一个总体流程,就像一个模板。而流程中的一些细节功能,要由子类去具体实现,那么父类可以定义一个专门提供给子类复写用的抽象方法,比如findClass方法,那么子类在继承父类的时候只需要复写实现细节功能的方法即可。关于模板设计模式可以参考笔者之前写的博客《设计模式2:模板设计模式》。

       从另一方面来说,如果实际开发需要遵循委托加载机制,那么只需要复写findClass方法即可,因为loadClass方法中默认实现了委托加载机制。

       实际上将“.class”加载到内存时转换为字节码对象的过程,也不需要程序员自己定义,ClassLoader类中同样已经定义好了——difineClass(),其API文档如下。

       protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError:将一个byte数组转换为Class类的实例。参数name是被加载类的字符串形式类名,字节数组是通过一个字节读取流读取被加载类的“.class”文件时获取的字节数据,通过对这两个参数进行操作,就可以返回对应类的Class对象。

2.2  自定义类加载器的加密原理

       实际上读取“.class”文件和读取普通文件的方式是类似的,也是通过一个字节读取流,读取指定路径下的指定文件,并将读取到的每个字节通过一个字节写入流写入到指定路径下的指定文件中。那么加密的过程实际就是对编译器已经编译好的正常的原始“.class”文件(存储在Classpath路径下)进行读取,对读取到的每个字节进行有规律的变换以后,再存储到一个特定路径下的同名“.class”文件中而已。那么在程序运行过程中我们将不再使用原始“.class”文件而是到特定路径下去加载加密“.class”文件,在加载的同时对其进行解密处理,否则无法正常使用所需类。

       那么我们下面要实现的变换规律(加密规律)就是把读取到的每个字节与255进行按位异或运算,这实际上就是对原始字节数据上每个二进制位上的数(0或1)进行取反操作。然后将取反后的字节数据写入到新的同名“.class”文件中,即可完成加密操作。这样一来原始“.class”文件中的字节数据就完全混乱了,如果不使用我们的自定义类加载器进行解密,那么就无法加载某个类的“.class”文件了。以下就是具备对类的“.class”文件进行加密功能的自定义类加载器代码。由于目前仅仅是对加密方法进行测试,因此并没有继承ClassLoader类,也没有复写findClass方法,这些将在后面的内容中定义。

代码3:

import java.io.IOException;import java.io.InputStream;import java.io.OutputStream; public class MyClassLoader {public static void cypher(InputStream ips, OutputStream ops) throws IOException {int by = -1;while((by = ips.read()) != -1){ops.write(by ^ 0xff);}}}
下面我们分别定义测试类ClassLoaderTest2和用于被加载的类Demo。

代码4:

//被加载类Demopublic class Demo {public String toString(){return "Hello World!";}}//测试类ClassLoaderTest2import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException; public class ClassLoaderTest2 {public static void main(String[] args) throws Exception {MyClassLoader mcl = new MyClassLoader();       cypherTest(args, mcl);}   private static void cypherTest(String[] args, MyClassLoader mcl) throws IOException {String srcPath = args[0];String destPath = args[1] + "\\" + srcPath.substring(srcPath.lastIndexOf("\\")+1);FileInputStream fis = new FileInputStream(srcPath);FileOutputStream fos = new FileOutputStream(destPath);       mcl.cypher(fis, fos);       fis.close();fos.close();}}
代码编写完毕以后,需要为自定义类加载器指定专门的类加载路径,并将通过正常编译方式存储在Classpath路径下的Demo类“.class”文件经加密处理后存储到这个专门的类加载路径中。如果是在eclipse下以项目的形式进行测试, 则可以在项目根目录下创建一个名为“lib”的文件夹。

       执行测试类ClassLoaderTest2时,向该类传递两个参数,分别表示存有原始Demo类“.class”文件的Classpath路径,以及自定义类专用加载路径,也就是“lib”文件夹。

       完成以上设置以后,执行测试类ClassLoaderTest2,则会在lib目录内也创建一个“Demo.class”文件,但这个“.class”文件加过密的,通过正常的类加载方式无法加载该“.class”文件。最后将lib目录下的“Demo.class”拷贝至原Classpath路径,将原始的正常“Demo.class”覆盖掉。再编写一个简单的测试类,在该测试类内部尝试创建一个Demo对象,并调用toString()方法。但由于加载的“Demo.class”文件已加过密,因此执行时给出了错误提示:

Exception in thread "main" java.lang.ClassFormatError:Incompatible magic value 889275713 in class file cn/tju/day2/Demo2。

由于Demo类的“.class”文件已经被加过密,文件内部的字节数据完全混乱了,已经无法被Java虚拟机解析为一个Class对象了。

       通过上述内容验证了,通过自定义的加密方法,能够实现对“.class”文件的加密功能,那么下一步工作就是完善对自定义类加载器的编写。

2.3  通过自定义类加载器加载加密“.class”文件

       正如上文所说,自定义类加载器需要继承ClassLoader,并且重点是要复写findClass方法。这一方法的主要思路是:创建文件字节读取流,并与指定类加载路径下的加密“.class”文件进行关联。再创建一个字节数组写入流对象。随后调用cypher方法,将这一对读写流传递到cypher方法中,即可将“.class”文件中的字节数据解密后转换为字节数组,缓存在字节数组写入流内,通过toByteArray方法即可获取到这一字节数组。最终调用defineClass方法,传递需要加载类的字符串形式全类名,以及字节数组即可返回对应的Class对象。自定义类加载器代码如下。

代码5:

import java.io.ByteArrayOutputStream;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream; public class MyClassLoader extends ClassLoader {private String classDir;   public MyClassLoader(){}public MyClassLoader(String classDir){this.classDir = classDir;}   public static void cypher(InputStream ips, OutputStream ops) throws IOException {int by = -1;while((by = ips.read()) != -1){ops.write(by ^ 0xff);}}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {       String simpleName = name.substring(name.lastIndexOf(".")+1);String classFileName = classDir + "\\" + simpleName +".class";       FileInputStream fis = null;ByteArrayOutputStream bos = null;try {fis = new FileInputStream(classFileName);bos = new ByteArrayOutputStream();cypher(fis, bos);           byte[] data = bos.toByteArray();return defineClass(name, data, 0, data.length);} catch (IOException e) {e.printStackTrace();} finally {try {fis.close();} catch (IOException e) {e.printStackTrace();}}       return super.findClass(name);}}
代码说明:

       (1)   MyClassLoader名为classDir的私有成员变量用于记录字符串形式的自定义类加载路径,将这一路径与调用loadClass方法时传递的类名(简单类名,去掉包名)拼在一起,即得到了所加载类“.class”文件的完整路径,将这一完成路径与文件字节读取路相关联即可对其进行读取。可通过构造方法对其进行设置。

       (2)  实际上解密的关键就是对加密的“.class”文件再次调用cypher方法,也就是说,对曾经取反的字节数据再次进行取反操作,即恢复到了原始字节数据。

       (3)  findClass方法首先尝试读取指定路径下的“.class”文件并将其解析为Class对象,如果失败则返回父类加载器的加载结果。

       下面我们通过一个测试类,对自定义类加载器的解密和类加载功能进行测试,测试代码如下。

代码6:

import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.util.Date; public class ClassLoaderTest3 {public static void main(String[] args) throws Exception {MyClassLoader mcl = new MyClassLoader("lib");loadClassTest(mcl);   }private static void loadClassTest(MyClassLoader mcl)throws ClassNotFoundException, InstantiationException,IllegalAccessException{Class<?> clazz = mcl.loadClass("cn.tju.day2.Demo2");Object instance = clazz.newInstance();       System.out.println(instance);}}
执行以上代码时,我们需要通过MyClassLoader构造方法指定自定义类加载器的类加载路径,这里我们由于是在eclipse工具的一个项目中进行测试,因此路径指定为存有Demo类加密“.class”文件的“lib”相对路径即可,因为eclipse项目会通过配置文件自动将项目根目录设置为Classpath,那么最终与文件字节读取流关联起来的就是Classpath与“lib”拼接后的路径,而不需要手动指定绝对路径。

       此外,需要注意的是,在执行测试代码以前,一定要将Classpath路径下的Demo.class文件删除掉,否则由于委托加载机制,MyClassLoader的父类加载器AppClassLoader一定会先到Classpath路径下加载Demo.class文件,这样就无法达到通过自定义类加载器,加载指定路径下的指定类目标了。这也就是我们不必复写loadClass方法而完全绕过委托加载机制,也能够实现需求的方法。

代码6的执行结果为:

Hello World!

执行结果表明,自定义类加载器读取到了指定路径下的加密“.class”文件,并成功将其解密并加载为Class对象,最终创建了Demo对象。

代码说明:

       (1)  正如前文所说,在编写自定义类加载器时,复写loadClass方法,完全绕过委托加载机制的话,就会由于无法加载根父类Object而抛出异常,因此不能完全绕过委托加载机制。但是也不能应用原始的委托加载机制,否则AppClassLoader一定会去到Classpath路径下寻找需要加载类的“.class”文件,而不会去指定加载路径中查找加密“.class”文件。因此我们这里的解决方法是,删除Classpath路径下的“Demo.class”文件,令所有父类加载器都找不到所需“.class”文件,这样最终只能由自定义加载器MyClassLoader去完成类加载操作。

       (2)  关于自定义类加载器无法加载指定路径下的指定“.class”文件的问题,还可以有一种解决思路。这个问题的核心其实就是父类加载器AppClassLoader会先尝试到Classpath路径下去去加载指定“.class”文件,那么只要不让AppClassLoader进行类加载操作也同样可以解决这个问题。按照这个思路,我们在创建MyClassLoader类加载器对象时,只要将其父类加载器指定为ExtClassLoader即可。换句话说,将MyClassLoader挂到ExtClassLoder下,那么BootStrap和ExtClassLoader必然在各自的特定路径是加载不到所需类的,因此最终只能委托MyClassLoader去指定路径加载“.class”文件。可以通过以下代码将MyClassLoader挂到ExtClassLoader下。

代码7:

ClassLoader cl = Thread.currentThread().getContextClassLoader().getParent();MyClassLoader mcl = new MyClassLoader(cl);
第一行代码的含义是,获取到当前线程对象,进而获取到当前线程的上下文类加载器,通常这个类加载器是AppClassLoader,再获取到AppClassLoader的父类加载器即为ExtClassLoader,然后在MyClassLoader时将其父类加载指定为该ExtClassLoader即可。

3  类加载器的另一个常用功能——资源文件的读取

3.1  利用ClassLoader读取资源文件

ClassLoader类中定义有这样的一个方法,

       public InputStream getResourceAsStream(String name):返回读取指定资源的输入流。调用该方法时,传递某个资源文件(比如配置文件)的字符串形式路径,则返回与此路径相关联的字节读取流。该方法常常应用于配置文件的读取。

下面我们针对该方法进行一个简单的演示。

代码7:

import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader; public class ClassLoaderTest4 {public static void main(String[] args) throws IOException {InputStream inStream =  ClassLoaderTest3.class.getClassLoader().getResourceAsStream("abc/123/config.properties");       if(inStream != null){BufferedReader bufr = new BufferedReader(new InputStreamReader(inStream));String line = null;while((line = bufr.readLine()) != null){System.out.println(line);}}}}
比如在某个项目“bin/abc/123”路径中存有一个名为“config.properties”的普通文本配置文件,那么通过以上代码对其进行读取的结果为:

name=David

age=31

ID=12345

getResourceAsStream方法将在底层按照指定的路径查找是否存在名为“config.properties”的文件,如果找到则将该文件路径与一个字节读取流进行关联,并返回,若没有查找到,则返回null。

代码说明:

       在指定文件路径时,在上下级目录之间既可以使用“\\”进行分隔,也可以使用“/”进行分隔。但要注意,无论使用哪种分隔符,不能在路径最前端添加“/”,但可以添加“\\”,原因将在下面解释。

       利用类加载器加载配置文件的例子在实际开发中是十分常见的,因为通常相关配置文件和“.class”文件是存放在同一个目录下,如果将存有“.class”文件的路径设置为Classpath,那么读取配置时只需要指定配置文件相对Classpath根路径的相对路径即可,就像代码7所示。如果使用类似eclipse的开发工具进行开发,那么项目所在根路径下的“bin”目录将包含在Classpath路径下,此时配置文件的路径只需要指定相对“bin”目录的相对路径即可。

       以上是利用类加载器对象加载文件的一个特点:只能加载Classpath路径下的某个文件,而且只能指定相对Classpath的相对路径,因为当使用类加载器对象的getResourceAsStream指定某个路径时,会在指定路径前默认地添加Classpath根路径。如果尝试指定某个文件的绝对路径将会抛出异常,并且在指定路径前也不能添加“/”符号,因为这表示在指定路径前添加Classpath根路径,但是程序底层已经自动完成了这一操作。

3.2  直接利用Class对象读取资源文件

       其实Class对象也对外提供getResourceAsStream方法,实际就是调用的加载该Class对象的ClassLoader的getResourceAsStream方法,因此基本功能与ClassLoader对象是相同的,但是也两点区别:第一,可以在指定路径前添加“/”符号,同样表示添加Classpath根路径,但是由于Class类的getResourceAsStream方法不会在路径前默认添加Classpath路径,因此“/”符号的添加不会造成任何问题,拿代码7举例,

InputStream inStream =

ClassLoaderTest3.class.getClassLoader().getResourceAsStream("/abc/123/config.properties");

;第二,如果不添加“/”符号,则该方法仅在该Class对象表示的“.class”文件所在路径中寻找,如果找不到则返回null,换句话说,不必指定相对路径只需要指定文件名即可。比如拿代码7举例,路径的指定可以按照以下方式,

InputStream inStream = 

ClassLoaderTest3.class.getClassLoader().getResourceAsStream(config.properties");

但是如果想在路径前添加“/”,则就要明确指定相对Classpath的根路径的相对路径,而不能仅仅指定文件名。

由此我们可以看出,Class类的getResourceAsStream方法指定路径的方式更为灵活一些,既可以指定绝对路径,因为不会默认添加Classpath根路径,也可以指定相对路径,只需添加“/”即可,如果配置文件和“.class”文件在同一个目录下,则仅指定文件名也是可以的。

       除此以外,若使用Class对象的getResourceAsStream方法加载资源文件,若被加载文件相对“.class”文件所在目录高一级,则可以使用“../”表示上一级目录。比如,

A

|-a.file

|-B

 |-b.class

其中A和B均表示文件夹。假设希望通过“b.class”对应的Class对象加载“a.file”文件,则路径可以定义为"../a.file"。

0 0