到Java模块化出现为止,双亲委派模型主要出现过3次较大规模“被破坏”的情况。
第一次“被破坏”
发生在双亲委派模型出现之前——即JDK1.2面世之前。由于双亲委派模型在JDK1.2才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不兼容这些代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法的findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。
按照loadClass()方法的逻辑,如果父类加载失败,会自动调用自己的findClass()方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。
第二次“被破坏”
是这个模型自身的缺陷造成的,双亲委派模型很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型的基础是指他们是总被用户代码继承、调用的API的存在。但程序设计往往没有绝对不变的完美规则,当有基础类型又要调回用户的代码,该怎么办?
典型例子:
- JNDI服务
是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK1.3时加入到rt.jar),属于基础类型,但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码。 - 问题
启动类加载器绝不可能认识、加载这些代码,该怎么办? - 解决方案
为解决这个困境,Java设计团队只好引入一个不太优雅的设计,线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应有程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文类加载器,JNDI使用它去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则。Java中涉及SPI的加载基本都采用这种方式来完成,例如JNDI、JDBC、JCE、JAXB和JBI等。
不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK1.6时,JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算给SPI的加载提供了一种相对合理的解决方案。
第三次“被破坏”
是由于用户程序动态性(代码热替换(Hot Swap)、模块热部署(Hot Deployment)等)的追求而导致的。打个比方,对于个人电脑来说,重启一次其实没什么大不了,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是大型系统或企业级软件开发者具有很大的吸引力。
OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
- 将以java.*开头的类,委派给父类加载器加载。
- 否则,将委派列表名单内的类,委派给父类加载器加载。
- 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
- 否则,类查找失败。
上面的查找顺序中只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行的。
总结
“被破坏”不一定是贬义,只要是明确的目的和充分的理由,突破就有原则无疑是一种创新。
正如OSGi中的类加载器的设计不符合双亲委派的类加载架构,但OSGi中对类加载器的运用是值得学习的,完全弄懂了OSGi的实现,就算是掌握了类加载器的精粹。