JVM系列——破坏双亲委派模型的场景和应用

上文提到过双亲委派模型并不是强制性的,而是Java设计者推荐的类加载器实现方式。
在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java 模块化出现为止,双亲委派模型出现过几次(3次?)较大规模“被破坏”的情况。

兼容1.2之前版本

双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2面世以前的“远 古”时代。

由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类 java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代 码,Java设计者们引入双亲委派模型时不得不做出一些妥协。

为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的 protected方法findClass()。并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass()中编写代码。
上节我们已经分析过loadClass()方法,双亲委派的具体逻辑就实现在这里面

按照loadClass()方法的逻辑,如果父类加载失败,会自动调用自己的findClass()方法来完成加载,这样 既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。

基础类型回调用户代码

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的。
双亲委派很好地解决了各个类 加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被 称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在。
如果有基础类型又要调用回用户的代码,那该怎么办呢(因为按照双亲委派模型,子加载器可以看到父加载器的内容,但反之不行)

JNDI

这并非是不可能出现的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务, 它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型 了。

但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码。
现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办

线程上下文类加载器

为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)
这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置。
如果创建线程时还未设置,它将会从父线程中继承一个。
如果在应用程序的全局范围内都没有设置过的话,那这个类加载器
默认就是应用程序类加载器

SPI场景

有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为。
这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,违背了双亲委派模型的一般性原则,但也是无可奈何的事情。

Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、JDBC、JCE、JAXB和JBI等。
不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK 6时,JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。

动态性:热部署/热替换

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。
对于个人电脑来说,重启一次其实没有什么问题,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是大型系统或企业级软件开发者具有很大的吸引力。

现在我们先来简单看一看OSGi是如何通过类加载器实现热部署的。
OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现。
每一个程序模块(OSGi中称为 Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实 现代码的热替换。
在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更 加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:

  1. 将以java.*开头的类,委派给父类加载器加载。
  2. 否则,将委派列表名单内的类,委派给父类加载器加载。
  3. 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
  5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器

加载。

  1. 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
  2. 否则,类查找失败。

上面的查找顺序中只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加
载器中进行的,关于OSGi的其他内容,这里就不再展开了。

更多破坏双亲委派源码示例

jdbc等,未完待续…
参考

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值