2) 基础类无法调用类加载器加载用户提供的代码。
双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),但如果基础类又要调用用户的代码,例如 JNDI 服务,JNDI 现在已经是 Java 的标准服务,它的代码由启动类加载器去加载(在 JDK 1.3 时放进去的 rt.jar ),但 JNDI 的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface,例如 JDBC 驱动就是由 MySQL 等接口提供者提供的)的代码,但启动类加载器只能加载基础类,无法加载用户类。
为此 Java 引入了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread.setContextClassLoaser() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
如此,JNDI 服务使用这个线程上下文类加载器去加载所需要的 SPI 代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。
3) 用户对程序动态性的追求。
代码热替换(HotSwap)、模块热部署(Hot Deployment)等,OSGi 实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。
在 OSGi 环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索:
1)将以 java.* 开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将 Import 列表中的类委派给 Export 这个类的 Bundle 的类加载器加载。
4)否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载。
6)否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载。
7)否则,类查找失败。
上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。OSGi 的 Bundle 类加载器之间只有规则,没有固定的委派关系。
Java 默认 ClassLoader,只加载指定目录下的 class,如果需要动态加载类到内存,例如要从远程网络下来类的二进制,然后调用这个类中的方法实现我的业务逻辑,如此,就需要自定义 ClassLoader。
自定义类加载器分为两步:
继承 java.lang.ClassLoader
重写父类的 findClass() 方法
针对第 1 步,为什么要继承 ClassLoader 这个抽象类,而不继承 AppClassLoader 呢?
因为它和 ExtClassLoader 都是 Launcher 的静态内部类,其访问权限是缺省的包访问权限。
static class AppClassLoader extends URLClassLoader{...}
第 2 步,JDK 的 loadCalss() 方法在所有父类加载器无法加载的时候,会调用本身的 findClass() 方法来进行类加载,因此我们只需重写 findClass() 方法找到类的二进制数据即可。
下面我自定义了一个简单的类加载器,并加载一个简单的类。
首先是需要被加载的简单类:
// 存放于D盘根目录 public class Test { public static void main(String[] args) { System.out.println("Test类已成功加载运行!"); ClassLoader classLoader = Test.class.getClassLoader(); System.out.println("加载我的classLoader:" + classLoader); System.out.println("classLoader.parent:" + classLoader.getParent()); } }并使用 javac -encoding utf8 Test.java 编译成 Test.class 文件。