Java 的ClassLoader
1. ClassLoader的用途
a: 类加载器(class loader)是用来动态的加载class文件到虚拟机当中 , 并转换成java.lang.Class类的一个实例 , 每个这样的实例用来表示一个java 类 , 因此我们可以根据 Class 的实例可以得到该类的信息 , 并通过实例的 newInstance()方法就可以创建出该类的一个对象 ,除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。为了完成加载类的这个职责,ClassLoader提供了一系列的方法 ;
b: 场景描述 , 大家都知道,当我们写好一个Java程序之后,不是管是CS还是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。
c: 程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。
2.java默认提供的ClassLoad 和自定义ClassLoad
2-1: BootStrapClassLoader: 称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JAVA_HOME/jre/lib中JDK的核心类库,如:rt.jar、resources.jar、charsets.jar等 , 以及将 -Xbootclasspath 选项指定的jar包装入工作, Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。
2-2:ExtensionClassLoader:称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar 文件 , 以及将-Djava.ext.dirs 指定目录下的jar文件加载进去 (该类继承了java.lang.ClassLoader);
2-3:AppClassLoader: 称为系统类加载器,负责加载应用程序classpath目录下的所有jar文件和class文件 ,以及将-Djava.class.path所指的目录下的类与jar文件进行加载 , 一般来说,Java 应用的类都是由它来完成加载的 , 可以通过 ClassLoader.getSystemClassLoader()来获取它 (该类继承了java.lang.ClassLoader );
2-4:自定义类加载器:通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,用户自定义 ClassLoader 可以根据
用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。
3.ClassLoad 的层次结构和加载类的过程
3-1:classload 的层次结构
3-2:classload搜索类的过程
a: ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承关
系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其
它ClassLoader实例的的父类加载器。
b: 当Classloader在进行类加载时,首先会自底向上挨个检查是否已经加载了指定类,如果已经加载则直接返回该类
的引用。如果到最高层也没有加载过指定类,那么会自顶向下挨个尝试加载,如果高层加载器没有加载成功 , 高
层加载器会把加载任务转交给低层加载器进行加载 , 直到用户自定义类加载器,如果还不能成功,就会抛 出类没
有找到异常
3-3:classload 加载类的过程
类加载器会自顶向下挨个尝试加载,如果高层加载器没有加载成功 , 高层加载器会把加载任务转交给低层加载器
进行加载 , 直到用户自定义类加载器,如果还不能成功,就会抛 出类没有找到异常
3-4:测试两个类是否相同
JVM在判定两个class是否相同时,不仅要判断两个(包名+类名)是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。现在通过实例来验证上述所描述的是否正确:
1) ; 在web服务器上建一个org.classloader.simple.NetClassLoaderSimple.java类
1. package org.classloader.simple;
2. public class NetClassLoaderSimple {
3. private NetClassLoaderSimple instance;
4. public void setNetClassLoaderSimple(Object obj) {
5. this.instance = (NetClassLoaderSimple)obj;
6. }
7. }
org.classloader.simple.NetClassLoaderSimple类的setNetClassLoaderSimple方法接收一个Object类型参数,并将它强制转换成org.classloader.simple.NetClassLoaderSimple类型。
2)、测试两个class是否相同(NetWorkClassLoader.java)
1. package classloader;
2. public class NewworkClassLoaderTest {
3. public static void main(String[] args) {
4. try {
5. //测试加载网络中的class文件
6. String rootUrl = "http://localhost:8080/httpweb/classes";
7. String className = "org.classloader.simple.NetClassLoaderSimple";
8. NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl);
9. NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl);
10. Class<?> clazz1 = ncl1.loadClass(className);
11. Class<?> clazz2 = ncl2.loadClass(className);
12. Object obj1 = clazz1.newInstance();
13. Object obj2 = clazz2.newInstance();
14. clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2);
15. } catch (Exception e) {
16. e.printStackTrace();
17. }
18. }
19.
20. }
首先获得网络上一个class文件的二进制名称,然后通过自定义的类加载器NetworkClassLoader创建两个实例,并根据网络地址分别加载这份class,并得到这两个ClassLoader实例加载后生成的Class实例clazz1和clazz2,最后将这两个Class实例分别生成具体的实例对象obj1和obj2,再通过反射调用clazz1中的setNetClassLoaderSimple方法。
3)、查看测试结果
3-5:定义自己的classloader
既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?
因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。
定义自已的类加载器分为两步:
1、继承java.lang.ClassLoader
2、重写父类的findClass方法
读者可能在这里有疑问,父类有那么多方法,为什么偏偏只重写findClass方法?
因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法。下图是API中ClassLoader的loadClass方法:
在该代码中必须要说明的一点是, 该自定义类加载器的并没有指定父加载器。
JVM规范中规定在不指定父类加载器的情况下, 默认采用系统类加载器作为其父加载器, 所以在使用该自定义类加载器时, 需要加载的类不能在类路径中, 否则的话依据类加载器的代理/委托原则, 待加载类会由系统类加载器加载,
这样自定义类加载器想要实现的, 诸如类的热替换, 多版本共存, 将变的不可实现。 如果我们一定想要把自定义加载器需要加载的类放在类路径中, 应该怎么办呢, 答案是把自定义类加载器的父加载器设置为null。
JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器。
需要注意的是自定义类加载器不同的父加载器决定了加载类的不同的可见性。
下面的代码示例是一个把自定义类加载器的父加载器设置为null时, 如何处理加载类的不同可见性。
3-6:类的加载方式
除了上面提到的通过自定义类加载器加载类, 我们通常会使用下面的两种方式来加载类
3-6-1: 隐式加载
A a = new A() ;
如果程序运行到这段代码时还没有A类,那么JVM会请求装载当前类的类装器来装载类。
3-6-2: 显示加载
01 | //效果相同, 执行类的初始化 | |||||||||||
02 | Class.forName("test.A"); | |||||||||||
03 | Class.forName("test.A", true, this.getClass().getClassLoader()) | |||||||||||
04 | //效果相同, 不执行类的初始化 | |||||||||||
05 | getClass().getClassLoader().loadClass("test.A"); | |||||||||||
06 | Class.forName("test.A", false, this.getClass().getClassLoader()); | |||||||||||
07 | //效果相同, 不执行类的初始化 | |||||||||||
08 | ClassLoader.getSystemClassLoader().loadClass("test.A"); | |||||||||||
09 | Class.forName("test.A", false, Classloader.getSystemClassLoader()); | |||||||||||
10 | //效果相同, 不执行类的初始化 | |||||||||||
11 | Thread.currentThread().getContextClassLoader().loadClass("test.A") | |||||||||||
12 | Class.forName("test.A", false, Thread.currentThread().getContextClassLoader()); |
获得Class 类的实体方式:
1.Class.forName(包名+类名) ; 2.具体类.Class ;3.具体对象.getClass() ;
4.通过ClassLoader类加载器中loadClass(包名+类名) ;
通过线程来获得该线程的上下文加载器
Thread.currentThread().getContextClassLoader();
3-7:上下文类加载器( ContextClassLoader)
类java.lang.Thread中的方法 getContextClassLoader()和setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。
Java 应用运行的初始线程的上下文类加载器是系统类加载器。
在线程中运行的代码可以通过此类加载器来加载类和资源。
正常的双亲委派模型中,下层的类加载器可以使用上层父加载器加载的对象,但是上层父类的加载器不可以使用子类加载的对象。
而有些时候程序的确需要上层调用下层,这时候就需要线程上下文加载器来处理。
1 | Thread.currentThread().getContextClassLoader() |
前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。
常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。
这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 ApacheXerces所包含的 jar 包。
SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。
这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。
而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。
它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
线程上下文类加载器正好解决了这个问题。
如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。
在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。
在5.自定义类加载器中我们的自定义类加载器CustomCL, 如果放到tomcat下的web应用中去使用会出现什么问题呢, 例如在自定义加载器 待加载的类中使用第三方类, 这个时候自定义加载器不能加载的类会交由系统加载器加载, 而该第三类不存在于类路径中, 只存在于该webApp下, 显然是加载不到的, 为了解决这个问题, 这个时候我们就需要上下文类加载器来解决这个问题了。
Webapp 类装载器:
应用层的类装载器,每个应用程序都会创建一个单独的类装载器。该类装载器只能本应用程序中可见。
所有/WEB-INF/classes目录下未压缩的类文件,资源文件都由该类装载器加载。
所有/WEB-INF/lib目录下压缩后Jar/zip文件都由该类装载器加载
显然上面我们说到的问题应该用webapp类加载器来加载第三方类, 那我们在自定义类加载器中如何获得webapp类加载器呢, 在tomcat6中启动webapp线程的上下文类加载器被设置为webapp类加载器了, 所以我们可以通过如下代码来完成加载。
protected Class loadClass(String name, boolean resolve)
throwsClassNotFoundException {
Class cls = null;
cls =findLoadedClass(name);
if(!this.dynaclazns.contains(name)&& cls == null)
cls =getSystemClassLoader().loadClass(name);
if (cls == null)
//自定义加载器和系统加载器均不能正常加载的类, 交由上下文加载器加载
cls = Thread.currentThread().getContextClassLoader().loadClass(name);
if(cls == null)
throw newClassNotFoundException(name);
if (resolve)
resolveClass(cls);
return cls;
}
其实ContextClassLoader就是Thread的一个属性而已, 我们当然可以不使用ContextClassLoader, 自己找个地方把classLoader保存起来, 在需要获取的时候能得到此classLoader就可以。
1.自定义类加载器的其他应用
热加载
每次创建一个新的类加载器, 我们修改下上面示例中的ClassIdentity类, 让他可以实现热加载。
public class ClassIdentity extends Thread {
public static voidmain(String[] args) {
newClassIdentity().start();
}
public void run() {
while(true) {
this.testClassIdentity();
try {
Thread.sleep(30 *1000L);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
public voidtestClassIdentity() {
String classDataRootPath ="C:\\Documents and Settings\\Administrator\\workspace\\Classloader\\classData";
FileSystemClassLoaderfscl1 = new FileSystemClassLoader(classDataRootPath);
String className ="com.example.Sample";
try {
Class<?> class1= fscl1.loadClass(className);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行该代码, 在运行过程中我们修改Sample类, 并覆盖原Sample类。
2. 类加密
指一般意义上的加密, 通过自定义加载器解密载入加密类
3. 应用隔离
非常典型的应用就是web容器
参考文件地址
http://blog.csdn.net/xyang81/article/details/7292380
http://weli.iteye.com/blog/1682625
http://my.oschina.net/huzorro/blog/96791
http://www.cnblogs.com/ericchen/archive/2011/01/15/1936130.html