您的位置:首页 > 编程语言 > Java开发

深入理解Java类加载器:Java类加载原理解析

2015-01-16 16:38 585 查看
/article/2002201.html

1 基本信息

  每个开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载。Java的类加载机制是技术体系中比较核心的部分,虽然和大部分开发人员直接打交道不多,但是对其背后的机理有一定理解有助于排查程序中出现的类加载失败等技术问题,对理解java虚拟机的连接模型和java语言的动态性都有很大帮助。

2 Java虚拟机类加载器结构简述

2.1 JVM三种预定义类型类加载器

  我们首先看一下JVM预定义的三种类型类加载器,当一个 JVM启动的时候,Java缺省开始使用如下三种类型类装入器:

  启动(Bootstrap)类加载器:引导类装入器是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

  扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将<
Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

  系统(System)类加载器:系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java
-classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。

  除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器,这个将在后面单独介绍。

2.2 类加载双亲委派机制介绍和分析

在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和扩展类加载器为例作简单分析。



图一 标准扩展类加载器继承层次图



图二系统类加载器继承层次图

通过图一和图二我们可以看出,类加载器均是继承自java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下java.lang.ClassLoader中几个最重要的方法:

[java]
view plaincopyprint?





//加载指定名称(包括包名)的二进制类型,供用户调用的接口

public Class<?> loadClass(String name)
throws ClassNotFoundException{ … }

//加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用

protected synchronized Class<?> loadClass(String name,
boolean resolve) throws ClassNotFoundException{ … }

//findClass方法一般被loadClass方法调用去加载指定名称类,供继承用

protected Class<?> findClass(String name)
throws ClassNotFoundException { … }

//定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承

//(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)

protected final Class<?> defineClass(String name,
byte[] b, int off,
int len) throws ClassFormatError{ … }

//加载指定名称(包括包名)的二进制类型,供用户调用的接口
public Class<?> loadClass(String name) throws ClassNotFoundException{ … }

//加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }

//findClass方法一般被loadClass方法调用去加载指定名称类,供继承用
protected Class<?> findClass(String name) throws ClassNotFoundException { … }

//定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承
//(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }

  通过进一步分析标准扩展类加载器(sun.misc.Launcher$ExtClassLoader)和系统类加载器(sun.misc.Launcher$AppClassLoader)的代码以及其公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码可以看出,都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。既然这样,我们就可以通过分析java.lang.ClassLoader中的loadClass(String
name)方法的代码就可以分析出虚拟机默认采用的双亲委派机制到底是什么模样:

[java]
view plaincopyprint?





public Class<?> loadClass(String name)
throws ClassNotFoundException {

return loadClass(name, false);

}

protected synchronized Class<?> loadClass(String name,
boolean resolve)
throws ClassNotFoundException {

// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {

//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载

try {
if (parent !=
null) {
//如果存在父类加载器,就委派给父类加载器加载

c = parent.loadClass(name,
false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,

//通过调用本地方法native findBootstrapClass0(String name)

c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {

// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能

c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {

// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,
//通过调用本地方法native findBootstrapClass0(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

  通过上面的代码分析,我们可以对JVM采用的双亲委派类加载机制有了更感性的认识,下面我们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:



图三 类加载器默认委派关系图

  上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:

[java]
view plaincopyprint?





public class LoaderTest {

public static
void main(String[] args) {
try {
System.out.println(ClassLoader.getSystemClassLoader());

System.out.println(ClassLoader.getSystemClassLoader().getParent());

System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());

} catch (Exception e) {

e.printStackTrace();
}
}
}

public class LoaderTest {

public static void main(String[] args) {
try {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
}

  说明:通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器。

  代码输出如下:

[plain]
view plaincopyprint?





sun.misc.Launcher$AppClassLoader@6d06d69c sun.misc.Launcher$ExtClassLoader@70dea4e null

sun.misc.Launcher$AppClassLoader@6d06d69c
sun.misc.Launcher$ExtClassLoader@70dea4e
null

  通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,就是说标准扩展类加载器本身强制设定父类加载器为null我们还是借助于代码分析一下。

  我们首先看一下java.lang.ClassLoader抽象类中默认实现的两个构造函数:

[java]
view plaincopyprint?





protected ClassLoader() {

SecurityManager security = System.getSecurityManager();
if (security !=
null) {
security.checkCreateClassLoader();
}
//默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器

this.parent = getSystemClassLoader();

initialized = true;
}

protected ClassLoader(ClassLoader parent) {

SecurityManager security = System.getSecurityManager();
if (security !=
null) {
security.checkCreateClassLoader();
}
//强制设置父类加载器
this.parent = parent;

initialized = true;
}

protected ClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
//默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器
this.parent = getSystemClassLoader();
initialized = true;
}

protected ClassLoader(ClassLoader parent) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
//强制设置父类加载器
this.parent = parent;
initialized = true;
}

  我们再看一下ClassLoader抽象类中parent成员的声明:

[java]
view plaincopyprint?





// The parent class loader for delegation

private ClassLoader parent;

// The parent class loader for delegation
private ClassLoader parent;


  声明为私有变量的同时并没有对外提供可供派生类访问的public或者protected设置器接口(对应的setter方法),结合前面的测试代码的输出,我们可以推断出:

  1. 系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

  2. 扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

  现在我们可能会有这样的疑问:扩展类加载器(ExtClassLoader)的父类加载器被强制设置为null了,那么扩展类加载器为什么还能将加载任务委派给启动类加载器呢?



图四 标准扩展类加载器和系统类加载器成员大纲视图



图五 扩展类加载器和系统类加载器公共父类成员大纲视图

  通过图四和图五可以看出,标准扩展类加载器和系统类加载器及其父类(java.net.URLClassLoader和java.security.SecureClassLoader)都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。有关java.lang.ClassLoader中默认的加载委派规则前面已经分析过,如果父加载器为null,则会调用本地方法进行启动类加载尝试。所以,图三中,启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系事实上是仍就成立的。(在后面的用户自定义类加载器部分,还会做更深入的分析)。

2.3 类加载双亲委派示例

  以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子。首先在IDE中建立一个简单的java应用工程,然后写一个简单的JavaBean如下:

[java]
view plaincopyprint?





package classloader.test.bean;

public class TestBean {

public TestBean() { }

}

package classloader.test.bean;

public class TestBean {

public TestBean() { }
}

  在现有当前工程中另外建立一测试类(ClassLoaderTest.java)内容如下:

  测试一:

[java]
view plaincopyprint?





package classloader.test.bean;

public class ClassLoaderTest {

public static
void main(String[] args) {
try {
//查看当前系统类路径中包含的路径条目

System.out.println(System.getProperty("java.class.path"));

//调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean

Class typeLoaded = Class.forName("classloader.test.bean.TestBean");

//查看被加载的TestBean类型是被那个类加载器加载的

System.out.println(typeLoaded.getClassLoader());
} catch (Exception e) {

e.printStackTrace();
}
}
}

package classloader.test.bean;

public class ClassLoaderTest {

public static void main(String[] args) {
try {
//查看当前系统类路径中包含的路径条目
System.out.println(System.getProperty("java.class.path"));
//调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean
Class typeLoaded = Class.forName("classloader.test.bean.TestBean");
//查看被加载的TestBean类型是被那个类加载器加载的
System.out.println(typeLoaded.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}

  对应的输出如下:

[plain]
view plaincopyprint?





C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes

sun.misc.Launcher$AppClassLoader@73d16e93

C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
sun.misc.Launcher$AppClassLoader@73d16e93

  说明:当前类路径默认的含有的一个条目就是工程的输出目录。

  测试二:

  将当前工程输出目录下的TestBean.class打包进test.jar剪贴到<Java_Runtime_Home>/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,结果如下:

[plain]
view plaincopyprint?





C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes

sun.misc.Launcher$ExtClassLoader@15db9742

C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
sun.misc.Launcher$ExtClassLoader@15db9742

  对比测试一和测试二,我们明显可以验证前面说的双亲委派机制,系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。

  测试三:

  将test.jar拷贝一份到<Java_Runtime_Home>/lib下,运行测试代码,输出如下:

[plain]
view plaincopyprint?





C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes

sun.misc.Launcher$ExtClassLoader@15db9742

C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
sun.misc.Launcher$ExtClassLoader@15db9742

  测试三和测试二输出结果一致。那就是说,放置到<Java_Runtime_Home>/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载<Java_Runtime_Home>/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除<Java_Runtime_Home>/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String
name, boolean resolve)方法中设置相应断点运行测试三进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。

3 java程序动态扩展方式

  Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。

  运行时动态扩展java应用程序有如下两个途径:

3.1 调用java.lang.Class.forName(…)加载类

  这个方法其实在前面已经讨论过,在后面的问题2解答中说明了该方法调用会触发哪个类加载器开始加载任务。这里需要说明的是多参数版本的forName(…)方法:

[java]
view plaincopyprint?





public static Class<?> forName(String name,
boolean initialize, ClassLoader loader)
throws ClassNotFoundException

public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException

  这里的initialize参数是很重要的。它表示在加载同时是否完成初始化的工作(说明:单参数版本的forName方法默认是完成初始化的)。有些场景下需要将initialize设置为true来强制加载同时完成初始化。例如典型的就是利用DriverManager进行JDBC驱动程序类注册的问题。因为每一个JDBC驱动程序类的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序使用。这就要求驱动程序类必须被初始化,而不单单被加载。Class.forName的一个很常见的用法就是在加载数据库驱动的时候。如
Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用来加载 Apache Derby 数据库的驱动。

3.2 用户自定义类加载器

  通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍,这里就简要叙述一下一般用户自定义类加载器的工作流程吧(可以结合后面问题解答一起看):

  1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2;

  2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真实虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;

  3、调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…), loadClass(…)转而抛异常,终止加载过程(注意:这里的异常种类不止一种)。

  说明:这里说的自定义类加载器是指JDK 1.2以后版本的写法,即不覆写改变java.lang.loadClass(…)已有委派逻辑情况下。

  整个加载类的过程如下图:



图六 自定义类加载器加载类的过程

4 常见问题分析

4.1 由不同的类加载器加载的指定类还是相同的类型吗?

  在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后再对比测试一下测试结果。

4.2 在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

  Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:

[java]
view plaincopyprint?





//java.lang.Class.java

publicstatic Class<?> forName(String className) throws ClassNotFoundException {

return forName0(className,
true, ClassLoader.getCallerClassLoader());

}

//java.lang.ClassLoader.java

// Returns the invoker's class loader, or null if none.

static ClassLoader getCallerClassLoader() {

// 获取调用类(caller)的类型

Class caller = Reflection.getCallerClass(3);

// This can be null if the VM is requesting it

if (caller == null) {

return null;

}
// 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader

return caller.getClassLoader0();

}

//java.lang.Class.java

//虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使用此方法

native ClassLoader getClassLoader0();

//java.lang.Class.java
publicstatic Class<?> forName(String className) throws ClassNotFoundException {
return forName0(className, true, ClassLoader.getCallerClassLoader());
}

//java.lang.ClassLoader.java
// Returns the invoker's class loader, or null if none.
static ClassLoader getCallerClassLoader() {
// 获取调用类(caller)的类型
Class caller = Reflection.getCallerClass(3);
// This can be null if the VM is requesting it
if (caller == null) {
return null;
}
// 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader
return caller.getClassLoader0();
}

//java.lang.Class.java
//虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使用此方法
native ClassLoader getClassLoader0();


4.3 在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?

  前面讲过,在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:

[java]
view plaincopyprint?





//摘自java.lang.ClassLoader.java

protected ClassLoader() {
SecurityManager security = System.getSecurityManager();

if (security != null) {

security.checkCreateClassLoader();
}
this.parent = getSystemClassLoader();

initialized = true;
}

//摘自java.lang.ClassLoader.java
protected ClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.parent = getSystemClassLoader();
initialized = true;
}

  我们再来看一下对应的getSystemClassLoader()方法的实现:

[java]
view plaincopyprint?





private static
synchronized void initSystemClassLoader() {

//...
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

scl = l.getClassLoader();
//...
}

private static synchronized void initSystemClassLoader() {
//...
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
scl = l.getClassLoader();
//...
}

  我们可以写简单的测试代码来测试一下:

[java]
view plaincopyprint?





System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());

System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());

  本机对应输出如下:

[plain]
view plaincopyprint?





sun.misc.Launcher$AppClassLoader@73d16e93

sun.misc.Launcher$AppClassLoader@73d16e93

  所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:

  1. <Java_Runtime_Home>/lib下的类;

  2. < Java_Runtime_Home >/lib/ext下或者由系统变量java.ext.dir指定位置中的类;

  3. 当前工程类路径下或者由系统变量java.class.path指定位置中的类。

4.4 在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?

  JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:

  即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到<Java_Runtime_Home>/lib下的类,但此时就不能够加载<Java_Runtime_Home>/lib/ext目录下的类了。

  说明:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5。

4.5 编写自定义类加载器时,一般有哪些注意点?

  1、一般尽量不要覆写已有的loadClass(...)方法中的委派逻辑

  一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:

[java]
view plaincopyprint?





//用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)

public class WrongClassLoader
extends ClassLoader {

public Class<?> loadClass(String name)
throws ClassNotFoundException {

return this.findClass(name);

}

protected Class<?> findClass(String name)
throws ClassNotFoundException {

// 假设此处只是到工程以外的特定目录D:\library下去加载类

// 具体实现代码省略
}
}

//用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)
public class WrongClassLoader extends ClassLoader {

public Class<?> loadClass(String name) throws ClassNotFoundException {
return this.findClass(name);
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
// 假设此处只是到工程以外的特定目录D:\library下去加载类
// 具体实现代码省略
}
}

  通过前面的分析我们已经知道,这个自定义类加载器WrongClassLoader的默认类加载器是系统类加载器,但是现在问题4种的结论就不成立了。大家可以简单测试一下,现在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工程类路径上的类都加载不上了。

[java]
view plaincopyprint?





//问题5测试代码一
public class WrongClassLoaderTest {

publicstaticvoid main(String[] args) {
try {
WrongClassLoader loader = new WrongClassLoader();

Class classLoaded = loader.loadClass("beans.Account");

System.out.println(classLoaded.getName());
System.out.println(classLoaded.getClassLoader());

} catch (Exception e) {

e.printStackTrace();
}
}
}

//问题5测试代码一
public class WrongClassLoaderTest {

publicstaticvoid main(String[] args) {
try {
WrongClassLoader loader = new WrongClassLoader();
Class classLoaded = loader.loadClass("beans.Account");
System.out.println(classLoaded.getName());
System.out.println(classLoaded.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}

  这里D:"classes"beans"Account.class是物理存在的。输出结果:

[plain]
view plaincopyprint?





java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)

at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:106)

at WrongClassLoader.findClass(WrongClassLoader.java:40)
at WrongClassLoader.loadClass(WrongClassLoader.java:29)

at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)

at java.lang.ClassLoader.defineClass1(Native Method)

at java.lang.ClassLoader.defineClass(ClassLoader.java:620)

at java.lang.ClassLoader.defineClass(ClassLoader.java:400)

at WrongClassLoader.findClass(WrongClassLoader.java:43)
at WrongClassLoader.loadClass(WrongClassLoader.java:29)

at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)

Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object

at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:620)

at java.lang.ClassLoader.defineClass(ClassLoader.java:400)

at WrongClassLoader.findClass(WrongClassLoader.java:43)

at WrongClassLoader.loadClass(WrongClassLoader.java:29)
at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)

java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:106)
at WrongClassLoader.findClass(WrongClassLoader.java:40)
at WrongClassLoader.loadClass(WrongClassLoader.java:29)
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
at WrongClassLoader.findClass(WrongClassLoader.java:43)
at WrongClassLoader.loadClass(WrongClassLoader.java:29)
at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
at WrongClassLoader.findClass(WrongClassLoader.java:43)
at WrongClassLoader.loadClass(WrongClassLoader.java:29)
at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)

  这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass()引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。

[java]
view plaincopyprint?





//问题5测试二
//用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)

public class WrongClassLoader
extends ClassLoader {

protected Class<?> findClass(String name)
throws ClassNotFoundException {

//假设此处只是到工程以外的特定目录D:\library下去加载类

//具体实现代码省略

}
}

//问题5测试二
//用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)
public class WrongClassLoader extends ClassLoader {

protected Class<?> findClass(String name) throws ClassNotFoundException {
//假设此处只是到工程以外的特定目录D:\library下去加载类
//具体实现代码省略
}
}

  将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出结果如下:

[plain]
view plaincopyprint?





beans.Account WrongClassLoader@1c78e57

beans.Account
WrongClassLoader@1c78e57

  2、正确设置父类加载器

  通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。

  3、保证findClass(String name)方法的逻辑正确性

  事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。

4.6 如何在运行时判断系统类加载器能加载哪些路径下的类?

  一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。

  二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty("java.class.path")。

4.7 如何在运行时判断标准扩展类加载器能加载哪些路径下的类?

  方法之一:

[java]
view plaincopyprint?





import java.net.URL;

import java.net.URLClassLoader;

public class ClassLoaderTest {

/**
* @param args the command line arguments

*/
public static
void main(String[] args) {
try {
URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();

for (int i =
0; i < extURLs.length; i++) {
System.out.println(extURLs[i]);
}
} catch (Exception e) {

//…
}
}
}

import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoaderTest {

/**
* @param args the command line arguments
*/
public static void main(String[] args) {
try {
URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (int i = 0; i < extURLs.length; i++) {
System.out.println(extURLs[i]);
}
} catch (Exception e) {
//…
}
}
}

  本机对应输出如下:

[plain]
view plaincopyprint?





file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/access-bridge-64.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/cldrdata.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/dnsns.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jaccess.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jfxrt.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/localedata.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/nashorn.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunec.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunjce_provider.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunmscapi.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunpkcs11.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/zipfs.jar

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/zipfs.jar


5 开发自己的类加载器

  在前面介绍类加载器的代理委派模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类
com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

  方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。

  类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

  在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。

5.1 文件系统类加载器

  第一个类加载器用来加载存储在文件系统上的Java字节代码。完整的实现如下所示。

[java]
view plaincopyprint?





package classloader;

import java.io.ByteArrayOutputStream;

import java.io.File;
import java.io.FileInputStream;

import java.io.IOException;

import java.io.InputStream;

// 文件系统类加载器
public class FileSystemClassLoader
extends ClassLoader {

private String rootDir;

public FileSystemClassLoader(String rootDir) {

this.rootDir = rootDir;

}

// 获取类的字节码
@Override

protected Class<?> findClass(String name)
throws ClassNotFoundException {

byte[] classData = getClassData(name);
// 获取类的字节数组
if (classData == null) {

throw
new ClassNotFoundException();
} else {
return defineClass(name, classData,
0, classData.length);
}
}

private byte[] getClassData(String className) {

// 读取类文件的字节
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);

ByteArrayOutputStream baos = new ByteArrayOutputStream();

int bufferSize =
4096;
byte[] buffer = new
byte[bufferSize];
int bytesNumRead =
0;
// 读取类文件的字节码
while ((bytesNumRead = ins.read(buffer)) != -1) {

baos.write(buffer, 0, bytesNumRead);

}
return baos.toByteArray();

} catch (IOException e) {

e.printStackTrace();
}
return null;

}

private String classNameToPath(String className) {

// 得到类文件的完全路径
return rootDir + File.separatorChar

+ className.replace('.', File.separatorChar) +
".class";
}
}

package classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

// 文件系统类加载器
public class FileSystemClassLoader extends ClassLoader {

private String rootDir;

public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}

// 获取类的字节码
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);  // 获取类的字节数组
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
// 读取类文件的字节
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 读取类文件的字节码
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

private String classNameToPath(String className) {
// 得到类文件的完全路径
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}

  如上所示,类 FileSystemClassLoader继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写
loadClass()方法,而是覆写 findClass()方法。


  类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。

  加载本地文件系统上的类,示例如下:

[java]
view plaincopyprint?





package com.example;

public class Sample {

private Sample instance;

public void setSample(Object instance) {

System.out.println(instance.toString());
this.instance = (Sample) instance;

}
}

package com.example;

public class Sample {

private Sample instance;

public void setSample(Object instance) {
System.out.println(instance.toString());
this.instance = (Sample) instance;
}
}


[java]
view plaincopyprint?





package classloader;

import java.lang.reflect.Method;

public class ClassIdentity {

public static
void main(String[] args) {
new ClassIdentity().testClassIdentity();

}

public void testClassIdentity() {

String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes";

FileSystemClassLoader fscl1 =
new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);

String className = "com.example.Sample";

try {
Class<?> class1 = fscl1.loadClass(className);
// 加载Sample类
Object obj1 = class1.newInstance();
// 创建对象
Class<?> class2 = fscl2.loadClass(className);

Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);

setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {

e.printStackTrace();
}
}
}

package classloader;

import java.lang.reflect.Method;

public class ClassIdentity {

public static void main(String[] args) {
new ClassIdentity().testClassIdentity();
}

public void testClassIdentity() {
String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try {
Class<?> class1 = fscl1.loadClass(className);  // 加载Sample类
Object obj1 = class1.newInstance();  // 创建对象
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
}

  运行输出:com.example.Sample@7852e922

5.2 网络类加载器

  下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。

  类 NetworkClassLoader负责通过网络下载Java类字节代码并定义出Java类。它的实现与FileSystemClassLoader类似。

[java]
view plaincopyprint?





package classloader;

import java.io.ByteArrayOutputStream;

import java.io.InputStream;

import java.net.URL;

public class NetworkClassLoader
extends ClassLoader {

private String rootUrl;

public NetworkClassLoader(String rootUrl) {

// 指定URL
this.rootUrl = rootUrl;

}

// 获取类的字节码
@Override

protected Class<?> findClass(String name)
throws ClassNotFoundException {

byte[] classData = getClassData(name);

if (classData == null) {

throw
new ClassNotFoundException();
} else {
return defineClass(name, classData,
0, classData.length);
}
}

private byte[] getClassData(String className) {

// 从网络上读取的类的字节
String path = classNameToPath(className);
try {
URL url = new URL(path);

InputStream ins = url.openStream();
ByteArrayOutputStream baos =
new ByteArrayOutputStream();
int bufferSize =
4096;
byte[] buffer =
new byte[bufferSize];

int bytesNumRead =
0;
// 读取类文件的字节

while ((bytesNumRead = ins.read(buffer)) != -1) {

baos.write(buffer, 0, bytesNumRead);

}
return baos.toByteArray();

} catch (Exception e) {

e.printStackTrace();
}
return null;

}

private String classNameToPath(String className) {

// 得到类文件的URL

return rootUrl + "/"

+ className.replace('.',
'/') + ".class";

}
}

package classloader;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;

public class NetworkClassLoader extends ClassLoader {

private String rootUrl;

public NetworkClassLoader(String rootUrl) {
// 指定URL
this.rootUrl = rootUrl;
}

// 获取类的字节码
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
// 从网络上读取的类的字节
String path = classNameToPath(className);
try {
URL url = new URL(path);
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 读取类文件的字节
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

private String classNameToPath(String className) {
// 得到类文件的URL
return rootUrl + "/"
+ className.replace('.', '/') + ".class";
}
}

  在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。我们使用接口的方式。示例如下:

  客户端接口:

[java]
view plaincopyprint?





package classloader; public interface Versioned { String getVersion(); }
package classloader;

public interface Versioned {

String getVersion();
}


[java]
view plaincopyprint?





package classloader;

public interface ICalculator
extends Versioned {

String calculate(String expression);
}

package classloader;

public interface ICalculator extends Versioned {

String calculate(String expression);
}

  网络上的不同版本的类:

[java]
view plaincopyprint?





package com.example;

import classloader.ICalculator;

public class CalculatorBasic
implements ICalculator {

@Override

public String calculate(String expression) {

return expression;

}

@Override
public String getVersion() {

return "1.0";

}

}

package com.example;

import classloader.ICalculator;

public class CalculatorBasic implements ICalculator {

@Override
public String calculate(String expression) {
return expression;
}

@Override
public String getVersion() {
return "1.0";
}

}


[java]
view plaincopyprint?





package com.example;

import classloader.ICalculator;

public class CalculatorAdvanced
implements ICalculator {

@Override

public String calculate(String expression) {

return "Result is " + expression;

}

@Override
public String getVersion() {

return "2.0";

}

}

package com.example;

import classloader.ICalculator;

public class CalculatorAdvanced implements ICalculator {

@Override
public String calculate(String expression) {
return "Result is " + expression;
}

@Override
public String getVersion() {
return "2.0";
}

}

  在客户端加载网络上的类的过程:

[java]
view plaincopyprint?





package classloader;

public class CalculatorTest {

public static
void main(String[] args) {
String url = "http://localhost:8080/ClassloaderTest/classes";

NetworkClassLoader ncl = new NetworkClassLoader(url);

String basicClassName = "com.example.CalculatorBasic";

String advancedClassName = "com.example.CalculatorAdvanced";

try {
Class<?> clazz = ncl.loadClass(basicClassName);
// 加载一个版本的类
ICalculator calculator = (ICalculator) clazz.newInstance();
// 创建对象
System.out.println(calculator.getVersion());

clazz = ncl.loadClass(advancedClassName);
// 加载另一个版本的类
calculator = (ICalculator) clazz.newInstance();

System.out.println(calculator.getVersion());
} catch (Exception e) {

e.printStackTrace();
}
}

}

package classloader;

public class CalculatorTest {

public static void main(String[] args) {
String url = "http://localhost:8080/ClassloaderTest/classes";
NetworkClassLoader ncl = new NetworkClassLoader(url);
String basicClassName = "com.example.CalculatorBasic";
String advancedClassName = "com.example.CalculatorAdvanced";
try {
Class<?> clazz = ncl.loadClass(basicClassName);  // 加载一个版本的类
ICalculator calculator = (ICalculator) clazz.newInstance();  // 创建对象
System.out.println(calculator.getVersion());
clazz = ncl.loadClass(advancedClassName);  // 加载另一个版本的类
calculator = (ICalculator) clazz.newInstance();
System.out.println(calculator.getVersion());
} catch (Exception e) {
e.printStackTrace();
}
}

}


参考文献:

http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html

http://www.ibm.com/developerworks/cn/java/j-lo-classloader/

1 线程上下文类加载器

  线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

  前面提到的类加载器的代理模式并不能解决 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 的 Apache Xerces所包含的 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 的实现中都会用到。

  Java默认的线程上下文类加载器是系统类加载器(AppClassLoader)。以下代码摘自sun.misc.Launch的无参构造函数Launch()。

[java]
view plaincopyprint?





// Now create the class loader to use to launch the application

try {
loader = AppClassLoader.getAppClassLoader(extcl);

} catch (IOException e) {
throw new InternalError(

"Could not create application class loader" );

}

// Also set the context class loader for the primordial thread.

Thread.currentThread().setContextClassLoader(loader);

// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader" );
}

// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);


  使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。典型的例子有:通过线程上下文来加载第三方库jndi实现,而不依赖于双亲委派。大部分java application服务器(jboss, tomcat..)也是采用contextClassLoader来处理web服务。还有一些采用hot swap特性的框架,也使用了线程上下文类加载器,比如 seasar (full
stack framework in japenese)。

  线程上下文从根本解决了一般应用不能违背双亲委派模式的问题。使java类加载体系显得更灵活。随着多核时代的来临,相信多线程开发将会越来越多地进入程序员的实际编码过程中。因此,在编写基础设施时, 通过使用线程上下文来加载类,应该是一个很好的选择。

  当然,好东西都有利弊。使用线程上下文加载类,也要注意保证多个需要通信的线程间的类加载器应该是同一个,防止因为不同的类加载器导致类型转换异常(ClassCastException)。

  defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)是java.lang.Classloader提供给开发人员,用来自定义加载class的接口。使用该接口,可以动态的加载class文件。例如在jdk中,URLClassLoader是配合findClass方法来使用defineClass,可以从网络或硬盘上加载class。而使用类加载接口,并加上自己的实现逻辑,还可以定制出更多的高级特性。

  下面是一个简单的hot swap类加载器实现。hot swap即热插拔的意思,这里表示一个类已经被一个加载器加载了以后,在不卸载它的情况下重新再加载它一次。我们知道Java缺省的加载器对相同全名的类只会加载一次,以后直接从缓存中取这个Class object。因此要实现hot swap,必须在加载的那一刻进行拦截,先判断是否已经加载,若是则重新加载一次,否则直接首次加载它。我们从URLClassLoader继承,加载类的过程都代理给系统类加载器URLClassLoader中的相应方法来完成。

[java]
view plaincopyprint?





package classloader;

import java.net.URL;

import java.net.URLClassLoader;

/**
* 可以重新载入同名类的类加载器实现
* 放弃了双亲委派的加载链模式,需要外部维护重载后的类的成员变量状态

*/
public class HotSwapClassLoader
extends URLClassLoader {

public HotSwapClassLoader(URL[] urls) {

super(urls);

}

public HotSwapClassLoader(URL[] urls, ClassLoader parent) {

super(urls, parent);

}

// 下面的两个重载load方法实现类的加载,仿照ClassLoader中的两个loadClass()

// 具体的加载过程代理给父类中的相应方法来完成

public Class<?> load(String name)
throws ClassNotFoundException {

return load(name,
false);
}

public Class<?> load(String name,
boolean resolve) throws ClassNotFoundException {

// 若类已经被加载,则重新再加载一次

if (null !=
super.findLoadedClass(name)) {

return reload(name, resolve);

}
// 否则用findClass()首次加载它

Class<?> clazz = super.findClass(name);

if (resolve) {

super.resolveClass(clazz);

}
return clazz;
}

public Class<?> reload(String name,
boolean resolve) throws ClassNotFoundException {

return new HotSwapClassLoader(super.getURLs(),
super.getParent()).load(
name, resolve);
}
}

package classloader;

import java.net.URL;
import java.net.URLClassLoader;

/**
* 可以重新载入同名类的类加载器实现
* 放弃了双亲委派的加载链模式,需要外部维护重载后的类的成员变量状态
*/
public class HotSwapClassLoader extends URLClassLoader {

public HotSwapClassLoader(URL[] urls) {
super(urls);
}

public HotSwapClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}

// 下面的两个重载load方法实现类的加载,仿照ClassLoader中的两个loadClass()
// 具体的加载过程代理给父类中的相应方法来完成
public Class<?> load(String name) throws ClassNotFoundException {
return load(name, false);
}

public Class<?> load(String name, boolean resolve) throws ClassNotFoundException {
// 若类已经被加载,则重新再加载一次
if (null != super.findLoadedClass(name)) {
return reload(name, resolve);
}
// 否则用findClass()首次加载它
Class<?> clazz = super.findClass(name);
if (resolve) {
super.resolveClass(clazz);
}
return clazz;
}

public Class<?> reload(String name, boolean resolve) throws ClassNotFoundException {
return new HotSwapClassLoader(super.getURLs(), super.getParent()).load(
name, resolve);
}
}

  两个重载的load方法参数与ClassLoader类中的两个loadClass()相似。在load的实现中,用findLoadedClass()查找指定的类是否已经被祖先加载器加载了,若已加载则重新再加载一次,从而放弃了双亲委派的方式(这种方式只会加载一次)。若没有加载则用自身的findClass()来首次加载它。

  下面是使用示例:

[java]
view plaincopyprint?





package classloader;

public class A {

private B b;

public void setB(B b) {

this.b = b;
}

public B getB() {

return b;
}
}

package classloader;

public class A {

private B b;

public void setB(B b) {
this.b = b;
}

public B getB() {
return b;
}
}


[java]
view plaincopyprint?





package classloader; public class B { }

package classloader;

public class B {

}


[java]
view plaincopyprint?





package classloader;

import java.lang.reflect.InvocationTargetException;

import java.lang.reflect.Method;

import java.net.MalformedURLException;

import java.net.URL;

public class TestHotSwap {

public static
void main(String args[]) throws MalformedURLException {

A a = new A();
// 加载类A
B b = new B(); // 加载类B

a.setB(b); // A引用了B,把b对象拷贝到A.b

System.out.printf("A classLoader is %s\n", a.getClass().getClassLoader());

System.out.printf("B classLoader is %s\n", b.getClass().getClassLoader());

System.out.printf("A.b classLoader is %s\n", a.getB().getClass().getClassLoader());

try {
URL[] urls = new URL[]{
new URL("file:///C:/Users/JackZhou/Documents/NetBeansProjects/classloader/build/classes/") };

HotSwapClassLoader c1 = new HotSwapClassLoader(urls, a.getClass().getClassLoader());

Class clazz = c1.load("classloader.A");
// 用hot swap重新加载类A
Object aInstance = clazz.newInstance();
// 创建A类对象
Method method1 = clazz.getMethod("setB", B.class);
// 获取setB(B b)方法
method1.invoke(aInstance, b); // 调用setB(b)方法,重新把b对象拷贝到A.b

Method method2 = clazz.getMethod("getB");
// 获取getB()方法
Object bInstance = method2.invoke(aInstance);
// 调用getB()方法
System.out.printf("Reloaded A.b classLoader is %s\n", bInstance.getClass().getClassLoader());

} catch (MalformedURLException | ClassNotFoundException |

InstantiationException | IllegalAccessException |

NoSuchMethodException | SecurityException |
IllegalArgumentException | InvocationTargetException e) {

e.printStackTrace();
}
}
}

package classloader;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;

public class TestHotSwap {

public static void main(String args[]) throws MalformedURLException {
A a = new A();  // 加载类A
B b = new B();  // 加载类B
a.setB(b);  // A引用了B,把b对象拷贝到A.b
System.out.printf("A classLoader is %s\n", a.getClass().getClassLoader());
System.out.printf("B classLoader is %s\n", b.getClass().getClassLoader());
System.out.printf("A.b classLoader is %s\n", a.getB().getClass().getClassLoader());

try {
URL[] urls = new URL[]{ new URL("file:///C:/Users/JackZhou/Documents/NetBeansProjects/classloader/build/classes/") };
HotSwapClassLoader c1 = new HotSwapClassLoader(urls, a.getClass().getClassLoader());
Class clazz = c1.load("classloader.A");  // 用hot swap重新加载类A
Object aInstance = clazz.newInstance();  // 创建A类对象
Method method1 = clazz.getMethod("setB", B.class);  // 获取setB(B b)方法
method1.invoke(aInstance, b);    // 调用setB(b)方法,重新把b对象拷贝到A.b
Method method2 = clazz.getMethod("getB");  // 获取getB()方法
Object bInstance = method2.invoke(aInstance);  // 调用getB()方法
System.out.printf("Reloaded A.b classLoader is %s\n", bInstance.getClass().getClassLoader());
} catch (MalformedURLException | ClassNotFoundException |
InstantiationException | IllegalAccessException |
NoSuchMethodException | SecurityException |
IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
}
}

  运行输出:

[java]
view plaincopyprint?





A classLoader is sun.misc.Launcher$AppClassLoader@73d16e93

B classLoader is sun.misc.Launcher$AppClassLoader@73d16e93

A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93

Reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93

A classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
B classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
Reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93

  HotSwapClassLoader加载器的作用是重新加载同名的类。为了实现hot swap,一个类在加载过后,若重新再加载一次,则新的Class object的状态会改变,老的状态数据需要通过其他方式拷贝到重新加载过的类生成的全新Class object实例中来。上面A类引用了B类,加载A时也会加载B(如果B已经加载,则直接从缓存中取出)。在重新加载A后,其Class object中的成员b会重置,因此要重新调用setB(b)拷贝一次。你可以注释掉这行代码,再运行会抛出java.lang.NullPointerException,指示A.b为null。

  注意新的A Class object实例所依赖的B类Class object,如果它与老的B Class object实例不是同一个类加载器加载的, 将会抛出类型转换异常(ClassCastException),表示两种不同的类。因此在重新加载A后,要特别注意给它的B类成员b传入外部值时,它们是否由同一个类加载器加载。为了解决这种问题, HotSwapClassLoader自定义的l/oad方法中,当前类(类A)是由自身classLoader加载的, 而内部依赖的类(类B)还是老对象的classLoader加载的。

2 何时使用Thread.getContextClassLoader()?

  这是一个很常见的问题,但答案却很难回答。这个问题通常在需要动态加载类和资源的系统编程时会遇到。总的说来动态加载资源时,往往需要从三种类加载器里选择:系统或程序的类加载器、当前类加载器、以及当前线程的上下文类加载器。在程序中应该使用何种类加载器呢?

  系统类加载器通常不会使用。此类加载器处理启动应用程序时classpath指定的类,可以通过ClassLoader.getSystemClassLoader()来获得。所有的ClassLoader.getSystemXXX()接口也是通过这个类加载器加载的。一般不要显式调用这些方法,应该让其他类加载器代理到系统类加载器上。由于系统类加载器是JVM最后创建的类加载器,这样代码只会适应于简单命令行启动的程序。一旦代码移植到EJB、Web应用或者Java
Web Start应用程序中,程序肯定不能正确执行。

  因此一般只有两种选择,当前类加载器和线程上下文类加载器。当前类加载器是指当前方法所在类的加载器。这个类加载器是运行时类解析使用的加载器,Class.forName(String)和Class.getResource(String)也使用该类加载器。代码中X.class的写法使用的类加载器也是这个类加载器。

  线程上下文类加载器在Java 2(J2SE)时引入。每个线程都有一个关联的上下文类加载器。如果你使用new Thread()方式生成新的线程,新线程将继承其父线程的上下文类加载器。如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都使用系统类加载器作为上下文类加载器。Web应用和Java企业级应用中,应用服务器经常要使用复杂的类加载器结构来实现JNDI(Java命名和目录接口)、线程池、组件热部署等功能,因此理解这一点尤其重要。

  为什么要引入线程的上下文类加载器?将它引入J2SE并不是纯粹的噱头,由于Sun没有提供充分的文档解释说明这一点,这使许多开发者很糊涂。实际上,上下文类加载器为同样在J2SE中引入的类加载代理机制提供了后门。通常JVM中的类加载器是按照层次结构组织的,目的是每个类加载器(除了启动整个JVM的原初类加载器)都有一个父类加载器。当类加载请求到来时,类加载器通常首先将请求代理给父类加载器。只有当父类加载器失败后,它才试图按照自己的算法查找并定义当前类。

  有时这种模式并不能总是奏效。这通常发生在JVM核心代码必须动态加载由应用程序动态提供的资源时。拿JNDI为例,它的核心是由JRE核心类(rt.jar)实现的。但这些核心JNDI类必须能加载由第三方厂商提供的JNDI实现。这种情况下调用父类加载器(原初类加载器)来加载只有其子类加载器可见的类,这种代理机制就会失效。解决办法就是让核心JNDI类使用线程上下文类加载器,从而有效的打通类加载器层次结构,逆着代理机制的方向使用类加载器。

  顺便提一下,XML解析API(JAXP)也是使用此种机制。当JAXP还是J2SE扩展时,XML解析器使用当前类加载器方法来加载解析器实现。但当JAXP成为J2SE核心代码后,类加载机制就换成了使用线程上下文加载器,这和JNDI的原因相似。

  好了,现在我们明白了问题的关键:这两种选择不可能适应所有情况。一些人认为线程上下文类加载器应成为新的标准。但这在不同JVM线程共享数据来沟通时,就会使类加载器的结构乱七八糟。除非所有线程都使用同一个上下文类加载器。而且,使用当前类加载器已成为缺省规则,它们广泛应用在类声明、Class.forName等情景中。即使你想尽可能只使用上下文类加载器,总是有这样那样的代码不是你所能控制的。这些代码都使用代理到当前类加载器的模式。混杂使用代理模式是很危险的。

  更为糟糕的是,某些应用服务器将当前类加载器和上下文类加器分别设置成不同的ClassLoader实例。虽然它们拥有相同的类路径,但是它们之间并不存在父子代理关系。想想这为什么可怕:记住加载并定义某个类的类加载器是虚拟机内部标识该类的组成部分,如果当前类加载器加载类X并接着执行它,如JNDI查找类型为Y的数据,上下文类加载器能够加载并定义Y,这个Y的定义和当前类加载器加载的相同名称的类就不是同一个,使用隐式类型转换就会造成异常。

  这种混乱的状况还将在Java中存在很长时间。在J2SE中还包括以下的功能使用不同的类加载器:

  (1)JNDI使用线程上下文类加载器。

  (2)Class.getResource()和Class.forName()使用当前类加载器。

  (3)JAXP使用上下文类加载器。

  (4)java.util.ResourceBundle使用调用者的当前类加载器。

  (5)URL协议处理器使用java.protocol.handler.pkgs系统属性并只使用系统类加载器。

  (6)Java序列化API缺省使用调用者当前的类加载器。

  这些类加载器非常混乱,没有在J2SE文档中给以清晰明确的说明。

  该如何选择类加载器?

  如若代码是限于某些特定框架,这些框架有着特定加载规则,则不要做任何改动,让框架开发者来保证其工作(比如应用服务器提供商,尽管他们并不能总是做对)。如在Web应用和EJB中,要使用Class.gerResource来加载资源。

  在其他情况下,我们可以自己来选择最合适的类加载器。可以使用策略模式来设计选择机制。其思想是将“总是使用上下文类加载器”或者“总是使用当前类加载器”的决策同具体实现逻辑分离开。往往设计之初是很难预测何种类加载策略是合适的,该设计能够让你可以后来修改类加载策略。

  考虑使用下面的代码,这是作者本人在工作中发现的经验。这儿有一个缺省实现,应该可以适应大部分工作场景:

[java]
view plaincopyprint?





package classloader.context;

/**
* 类加载上下文,持有要加载的类
*/
public class ClassLoadContext {

private final Class m_caller;

public final Class getCallerClass() {

return m_caller;

}

ClassLoadContext(final Class caller) {

m_caller = caller;
}
}

package classloader.context;

/**
* 类加载上下文,持有要加载的类
*/
public class ClassLoadContext {

private final Class m_caller;

public final Class getCallerClass() {
return m_caller;
}

ClassLoadContext(final Class caller) {
m_caller = caller;
}
}


[java]
view plaincopyprint?





package classloader.context; /** * 类加载策略接口 */ public interface IClassLoadStrategy { ClassLoader getClassLoader(ClassLoadContext ctx); }

package classloader.context;

/**
* 类加载策略接口
*/
public interface IClassLoadStrategy {

ClassLoader getClassLoader(ClassLoadContext ctx);
}


[java]
view plaincopyprint?





/**
* 缺省的类加载策略,可以适应大部分工作场景
*/
public class DefaultClassLoadStrategy
implements IClassLoadStrategy {

/**
* 为ctx返回最合适的类加载器,从系统类加载器、当前类加载器

* 和当前线程上下文类加载中选择一个最底层的加载器
* @param ctx
* @return
*/
@Override
public ClassLoader getClassLoader(final ClassLoadContext ctx) {

final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader();

final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();

ClassLoader result;

// If 'callerLoader' and 'contextLoader' are in a parent-child

// relationship, always choose the child:

if (isChild(contextLoader, callerLoader)) {

result = callerLoader;
} else if (isChild(callerLoader, contextLoader)) {

result = contextLoader;
} else {
// This else branch could be merged into the previous one,

// but I show it here to emphasize the ambiguous case:

result = contextLoader;
}
final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();

// Precaution for when deployed as a bootstrap or extension class:

if (isChild(result, systemLoader)) {

result = systemLoader;
}

return result;

}

// 判断anotherLoader是否是oneLoader的child

private boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){

//...
}

// ... more methods

}

/**
* 缺省的类加载策略,可以适应大部分工作场景
*/
public class DefaultClassLoadStrategy implements IClassLoadStrategy {

/**
* 为ctx返回最合适的类加载器,从系统类加载器、当前类加载器
* 和当前线程上下文类加载中选择一个最底层的加载器
* @param ctx
* @return
*/
@Override
public ClassLoader getClassLoader(final ClassLoadContext ctx) {
final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader();
final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
ClassLoader result;

// If 'callerLoader' and 'contextLoader' are in a parent-child
// relationship, always choose the child:
if (isChild(contextLoader, callerLoader)) {
result = callerLoader;
} else if (isChild(callerLoader, contextLoader)) {
result = contextLoader;
} else {
// This else branch could be merged into the previous one,
// but I show it here to emphasize the ambiguous case:
result = contextLoader;
}
final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
// Precaution for when deployed as a bootstrap or extension class:
if (isChild(result, systemLoader)) {
result = systemLoader;
}

return result;
}

// 判断anotherLoader是否是oneLoader的child
private boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){
//...
}

// ... more methods
}

  决定应该使用何种类加载器的接口是IClassLoaderStrategy,为了帮助IClassLoadStrategy做决定,给它传递了个ClassLoadContext对象作为参数。ClassLoadContext持有要加载的类。

  上面代码的逻辑很简单:如调用类的当前类加载器和上下文类加载器是父子关系,则总是选择子类加载器。对子类加载器可见的资源通常是对父类可见资源的超集,因此如果每个开发者都遵循J2SE的代理规则,这样做大多数情况下是合适的。

  当前类加载器和上下文类加载器是兄弟关系时,决定使用哪一个是比较困难的。理想情况下,Java运行时不应产生这种模糊。但一旦发生,上面代码选择上下文类加载器。这是作者本人的实际经验,绝大多数情况下应该能正常工作。你可以修改这部分代码来适应具体需要。一般来说,上下文类加载器要比当前类加载器更适合于框架编程,而当前类加载器则更适合于业务逻辑编程。

  最后需要检查一下,以便保证所选类加载器不是系统类加载器的父亲,在开发标准扩展类库时这通常是个好习惯。

  注意作者故意没有检查要加载资源或类的名称。Java XML API成为J2SE核心的历程应该能让我们清楚过滤类名并不是好想法。作者也没有试图检查哪个类加载器加载首先成功,而是检查类加载器的父子关系,这是更好更有保证的方法。

  下面是类加载器的选择器:

[java]
view plaincopyprint?





package classloader.context;

/**
* 类加载解析器,获取最合适的类加载器
*/
public abstract
class ClassLoaderResolver {

private static IClassLoadStrategy s_strategy;
// initialized in <clinit>
private static
final int CALL_CONTEXT_OFFSET =
3; // may need to change if this class is redesigned

private static
final CallerResolver CALLER_RESOLVER;
// set in <clinit>

static {
try {
// This can fail if the current SecurityManager does not allow

// RuntimePermission ("createSecurityManager"):

CALLER_RESOLVER = new CallerResolver();

} catch (SecurityException se) {

throw new RuntimeException("ClassLoaderResolver: could not create CallerResolver: " + se);

}
s_strategy = new DefaultClassLoadStrategy();
//默认使用缺省加载策略
}

/**
* This method selects the best classloader instance to be used for

* class/resource loading by whoever calls this method. The decision

* typically involves choosing between the caller's current, thread context,

* system, and other classloaders in the JVM and is made by the {@link IClassLoadStrategy}

* instance established by the last call to {@link #setStrategy}.

*
* @return classloader to be used by the caller ['null' indicates the

* primordial loader]

*/
public static
synchronized ClassLoader getClassLoader() {

final Class caller = getCallerClass(0);
// 获取执行当前方法的类
final ClassLoadContext ctx =
new ClassLoadContext(caller); // 创建类加载上下文

return s_strategy.getClassLoader(ctx);
// 获取最合适的类加载器
}

public static
synchronized IClassLoadStrategy getStrategy() {

return s_strategy;
}

public static
synchronized IClassLoadStrategy setStrategy(final IClassLoadStrategy strategy) {

final IClassLoadStrategy old = s_strategy;
// 设置类加载策略
s_strategy = strategy;
return old;
}

/**
* A helper class to get the call context. It subclasses SecurityManager

* to make getClassContext() accessible. An instance of CallerResolver

* only needs to be created, not installed as an actual security manager.

*/
private static
final class CallerResolver
extends SecurityManager {
@Override

protected Class[] getClassContext() {

return
super.getClassContext(); // 获取当执行栈的所有类,native方法

}

}

/*
* Indexes into the current method call context with a given

* offset.
*/
private static Class getCallerClass(final
int callerOffset) {
return CALLER_RESOLVER.getClassContext()[CALL_CONTEXT_OFFSET

+ callerOffset]; // 获取执行栈上某个方法所属的类

}
}

package classloader.context;

/**
* 类加载解析器,获取最合适的类加载器
*/
public abstract class ClassLoaderResolver {

private static IClassLoadStrategy s_strategy;  // initialized in <clinit>
private static final int CALL_CONTEXT_OFFSET = 3;  // may need to change if this class is redesigned
private static final CallerResolver CALLER_RESOLVER;  // set in <clinit>

static {
try {
// This can fail if the current SecurityManager does not allow
// RuntimePermission ("createSecurityManager"):
CALLER_RESOLVER = new CallerResolver();
} catch (SecurityException se) {
throw new RuntimeException("ClassLoaderResolver: could not create CallerResolver: " + se);
}
s_strategy = new DefaultClassLoadStrategy();  //默认使用缺省加载策略
}

/**
* This method selects the best classloader instance to be used for
* class/resource loading by whoever calls this method. The decision
* typically involves choosing between the caller's current, thread context,
* system, and other classloaders in the JVM and is made by the {@link IClassLoadStrategy}
* instance established by the last call to {@link #setStrategy}.
*
* @return classloader to be used by the caller ['null' indicates the
* primordial loader]
*/
public static synchronized ClassLoader getClassLoader() {
final Class caller = getCallerClass(0); // 获取执行当前方法的类
final ClassLoadContext ctx = new ClassLoadContext(caller);  // 创建类加载上下文
return s_strategy.getClassLoader(ctx);  // 获取最合适的类加载器
}

public static synchronized IClassLoadStrategy getStrategy() {
return s_strategy;
}

public static synchronized IClassLoadStrategy setStrategy(final IClassLoadStrategy strategy) {
final IClassLoadStrategy old = s_strategy;  // 设置类加载策略
s_strategy = strategy;
return old;
}

/**
* A helper class to get the call context. It subclasses SecurityManager
* to make getClassContext() accessible. An instance of CallerResolver
* only needs to be created, not installed as an actual security manager.
*/
private static final class CallerResolver extends SecurityManager {
@Override
protected Class[] getClassContext() {
return super.getClassContext();  // 获取当执行栈的所有类,native方法
}

}

/*
* Indexes into the current method call context with a given
* offset.
*/
private static Class getCallerClass(final int callerOffset) {
return CALLER_RESOLVER.getClassContext()[CALL_CONTEXT_OFFSET
+ callerOffset];  // 获取执行栈上某个方法所属的类
}
}

  可通过调用ClassLoaderResolver.getClassLoader()方法来获取类加载器对象,并使用其ClassLoader的接口如loadClass()等来加载类和资源。此外还可使用下面的ResourceLoader接口来取代ClassLoader接口:

[java]
view plaincopyprint?





package classloader.context;

import java.net.URL;

public class ResourceLoader {

/**
* 加载一个类
*
* @param name
* @return
* @throws java.lang.ClassNotFoundException

* @see java.lang.ClassLoader#loadClass(java.lang.String)

*/
public static Class<?> loadClass(final String name)
throws ClassNotFoundException {

//获取最合适的类加载器
final ClassLoader loader = ClassLoaderResolver.getClassLoader();

//用指定加载器加载类
return Class.forName(name,
false, loader);
}

/**
* 加载一个资源
*
* @param name
* @return
* @see java.lang.ClassLoader#getResource(java.lang.String)

*/
public static URL getResource(final String name) {

//获取最合适的类加载器
final ClassLoader loader = ClassLoaderResolver.getClassLoader();

//查找指定的资源
if (loader !=
null) {
return loader.getResource(name);

} else {

return ClassLoader.getSystemResource(name);

}
}

// ... more methods ...

}

package classloader.context;

import java.net.URL;

public class ResourceLoader {

/**
* 加载一个类
*
* @param name
* @return
* @throws java.lang.ClassNotFoundException
* @see java.lang.ClassLoader#loadClass(java.lang.String)
*/
public static Class<?> loadClass(final String name) throws ClassNotFoundException {
//获取最合适的类加载器
final ClassLoader loader = ClassLoaderResolver.getClassLoader();
//用指定加载器加载类
return Class.forName(name, false, loader);
}

/**
* 加载一个资源
*
* @param name
* @return
* @see java.lang.ClassLoader#getResource(java.lang.String)
*/
public static URL getResource(final String name) {
//获取最合适的类加载器
final ClassLoader loader = ClassLoaderResolver.getClassLoader();
//查找指定的资源
if (loader != null) {
return loader.getResource(name);
} else {
return ClassLoader.getSystemResource(name);
}
}

// ... more methods ...
}

  ClassLoadContext.getCallerClass()返回的类在ClassLoaderResolver或ResourceLoader使用,这样做的目的是让其能找到调用类的类加载器(上下文加载器总是能通过Thread.currentThread().getContextClassLoader()来获得)。注意调用类是静态获得的,因此这个接口不需现有业务方法增加额外的Class参数,而且也适合于静态方法和类初始化代码。具体使用时,可以往这个上下文对象中添加具体部署环境中所需的其他属性。

3 类加载器与Web容器

  对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于
Web 容器提供的类。
这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

  绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:

  (1)每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。

  (2)多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。

  (3)当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。

4 类加载器与OSGi

  OSGi是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse就是基于OSGi 技术来构建的。

  OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的
Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。
模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性 org.osgi.framework.bootdelegation的值即可。

  假设有两个模块 bundleA 和 bundleB,它们都有自己对应的类加载器 classLoaderA 和 classLoaderB。在 bundleA 中包含类 com.bundleA.Sample,并且该类被声明为导出的,也就是说可以被其它模块所使用的。bundleB 声明了导入 bundleA 提供的类 com.bundleA.Sample,并包含一个类 com.bundleB.NewSample继承自 com.bundleA.Sample。在 bundleB 启动的时候,其类加载器 classLoaderB
需要加载类 com.bundleB.NewSample,进而需要加载类 com.bundleA.Sample。由于 bundleB 声明了类 com.bundleA.Sample是导入的,classLoaderB 把加载类 com.bundleA.Sample的工作代理给导出该类的 bundleA 的类加载器 classLoaderA。classLoaderA 在其模块内部查找类 com.bundleA.Sample并定义它,所得到的类 com.bundleA.Sample实例就可以被所有声明导入了此类的模块使用。对于以
java开头的类,都是由父类加载器来加载的。如果声明了系统属性 org.osgi.framework.bootdelegation=com.example.core.*,那么对于包 com.example.core中的类,都是由父类加载器来完成的。

  OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在 Java 虚拟机中,带来了很大的灵活性。不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:

  (1)如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath中指明即可。

  (2)如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。

  (3)如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了 NoClassDefFoundError异常,首先检查当前线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader()就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话,可以首先通过 class.getClassLoader()来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader()来设置当前线程的上下文类加载器。

总结

  类加载器是 Java 语言的一个创新。它使得动态安装和更新软件组件成为可能。本文详细介绍了类加载器的相关话题,包括基本概念、代理模式、线程上下文类加载器、与 Web 容器和 OSGi 的关系等。开发人员在遇到 ClassNotFoundException和 NoClassDefFoundError等异常的时候,应该检查抛出异常的类的类加载器和当前线程的上下文类加载器,从中可以发现问题的所在。在开发自己的类加载器的时候,需要注意与已有的类加载器组织结构的协调。

参考文献:

https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

http://www.blogjava.net/lihao336/archive/2009/09/17/295489.html

http://kenwublog.com/structure-of-java-class-loader
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: