加载器概念
类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。
类加载器分类
JDK 默认提供了如下几种ClassLoader:
1、Bootstrp Class Loader
Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib或者-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类,虚拟机出于安全等因素考虑,不会加载%JAVA_HOME%/jre/lib路径下存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。
2、ExtClassLoader
Bootstrp class loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader。ExtClassLoader是用Java写的,具体来说就是sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库。
3、AppClassLoader
Bootstrp class loader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader。AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。
综上所述,它们之间的关系可以通过下图形象的描述:
双亲委托模型
Java中ClassLoader的加载采用了双亲委托机制,采用双亲委托机制加载类的时候采用如下的几个步骤:
1 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载时可以直接返回。
2 当前classLoader的缓存中没有找到被加载的类时,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader。
3 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。
线程上下文类加载器
ContextClassLoader是java.lang.Thread类的一个属性,Thread类中的方法 getContextClassLoader()和setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源。
双亲委托模型并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中(rt.jar)。这些 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 核心库的一部分,是由引导类加载器(BootstrpClassLoader)来加载的;SPI 实现的 Java 类一般是由系统类加载器(APPClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的双亲委托模型无法解决这个问题。
线程ContextClassLoader正好解决了这个问题。如果不做任何的设置,Java 应用的线程的ContextClassLoader默认就是系统上下文类(AppClassLoader)加载器。在 SPI 接口的代码中使用线程ContextClassLoader,就可以成功的加载到 SPI 实现的类。线程ContextClassLoader在很多 SPI 的实现中都会用到。
以下是一个简单的Java 应用,用于测试ContextClassLoader。
import javax.xml.parsers.DocumentBuilderFactory;
public class TestClassLoader {
public static void main(String[] args) throws Exception {
TestClassLoader test = new TestClassLoader();
test.testLoad();
}
private void testLoad() throws Exception {
// 1
System.out.println(Thread.currentThread());// Thread[main,5,main]
// 2
System.out.println(Thread.currentThread().getContextClassLoader());// sun.misc.Launcher$AppClassLoader@19821f
// 3
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(
"org.apache.xerces.jaxp.DocumentBuilderFactoryImpl", null);
}
}
注释//2显示当前主线程使用的上下文类加载器是AppClassLoader,注释//3由DocumentBuilderFactory负责加载并实例化它的一个实现类,该实现类由Apache Xerces提供,同时在newInstance中传入了一个空的加载器。
public class DocumentBuilderFactory{
public static DocumentBuilderFactory newInstance(String factoryClassName, ClassLoader classLoader){
try {
return (DocumentBuilderFactory) FactoryFinder.newInstance(factoryClassName, classLoader, false);
} catch (FactoryFinder.ConfigurationError e) {
throw new FactoryConfigurationError(e.getException(), e.getMessage());
}
}
//......
}
DocumentBuilderFactory中的newInstance方法将加载的任务交给FactoryFinder来完成。
class FactoryFinder {
static Object newInstance(String className, ClassLoader cl, boolean doFallback)
throws ConfigurationError
{
try {
//使用给定的加载器cl,获取DocumentBuilderFactoryImpl类的Class对象,此处的cl为null
Class providerClass = getProviderClass(className, cl, doFallback);
//创建DocumentBuilderFactoryImpl的实例
Object instance = providerClass.newInstance();
}
return instance;
}
catch (ClassNotFoundException x) {
throw new ConfigurationError(
"Provider " + className + " not found", x);
}
catch (Exception x) {
throw new ConfigurationError(
"Provider " + className + " could not be instantiated: " + x,
x);
}
}
//......
}
获取
DocumentBuilderFactoryImpl实现类的
Class
对象在getProviderClass方法中完成。
class FactoryFinder {
static private Class getProviderClass(String className, ClassLoader cl,
boolean doFallback) throws ClassNotFoundException
{
try {
if (cl == null) {
//1
cl = ss.getContextClassLoader();
if (cl == null) {
throw new ClassNotFoundException();
}
else {
//2由上下文类加载器完成实现类的加载
return cl.loadClass(className);
}
}
else {
return cl.loadClass(className);
}
}
catch (ClassNotFoundException e1) {
//......
}
}
//......
}
由于
cl
传入的是一个
null
值,所以需要从
ss
对象中获取一个上下文类加载器,那
ss
对象中的getContextClassLoader方法如何获取一个上下文加载器的呢?
class SecuritySupport {
ClassLoader getContextClassLoader() throws SecurityException{
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
ClassLoader cl = null;
//1
cl = Thread.currentThread().getContextClassLoader();
if (cl == null)
cl = ClassLoader.getSystemClassLoader();
return cl;
}
});
}
//......
}
该方法中的其他代码不需要管,由注释
//1
处可以看出最后
return
语句返回的
cl
加载器,就是从当前线程中获得的上下文类加载器。使用
myeclipse
调试跟踪代码可以看出,当前只有一个
mian
主线程。
主线程中的上下文类加载器默认就是AppClassLoader。方法getProviderClass()中的注释//2处使用AppClassLoader加载器来完成实现类的加载。在注释//2处为什么不使用FactoryFinder.Class.getClassLoader()来完成实现类的加载呢?
因为FactoryFinder类位于核心类库中,由Bootstrp类加载器完成加载,而实现类DocumentBuilderFactoryImpl在我们自己的java应用中,在Bootstrp加载器的搜索范围中,找不到该实现类,因此这样的方式无法完成类的加载。另外一个原因,即使将实现类放到Bootstrp加载器的搜索范围中,也是不能完成加载,因为Bootstrp只负责加载固定的核心类库,其他的字节码文件一律都不加载。
正常的双亲委派模型中,下层的类加载器可以使用上层父加载器加载的对象,但是上层父类的加载器不可能使用子类加载的对象(因为,正常情况下加载器只会向上搜索被加载的类,不会向下搜索)。而有些时候程序的确需要上层调用下层,这时候就需要线程上下文类加载器来处理,以上就是这样的一个例子。
自定义类加载器
虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。 下面将通过具体的实例来说明类加载器的开发。
文件系统类加载器
第一个类加载器用来加载存储在文件系统上的 Java 字节代码。完整的实现下所示。
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
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。一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法 loadClass()封装了前面提到的双亲委托机制的实现。loadClass()方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass()方法来查找该类。因此,为了保证类加载器都正确实现双亲委托机制,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。
类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。
参考
http://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://imtiger.net/blog/2009/11/09/java-classloader/
http://www.cnblogs.com/clownfish/p/3348173.html
http://www.cnblogs.com/realviv/articles/1906110.html
http://my.oschina.net/u/1182621/blog/146349
http://blog.csdn.net/vernonzheng/article/details/8461380
http://blog.chinaunix.net/uid-21227800-id-65887.html