可怕的线程上下文类装载器(TCCL)

在明天的 OSGi 2012 社区活动上,我将以“ 如何使你的类库在不依赖 OSGi 的情况下进行友好地 OSGi”为主题进行演讲。在演讲中我将会提及 Java 的线程上下文类加载器(TCCL),但是整个演讲只有 25 分钟,我没有更多时间对此进行深入讨论。所以我写这篇博客希望能够帮助大家了解到一些相关背景信息。
本文中的很多技术信息和研究取自于 Peter Kriens 先生写的一篇没有公开的 OSGi 需求建议书。对此我已经获得了他的许可。

历史

线程上下文类装载器(TCCL)在 Java 体系中有一段非常有趣的历史。
Java 定义了一套类装载器的层次结构,在一个典型的 Java 运行时在底部的“application”装载器负责“classpath”(通过 -classpath/-cp 提供),而启动装载器负责的是 rt.jar 包里的一系列 JRE 类( 译者注:启动装载器处于类装载器层次结构的最顶层,是所有类装载器的父装载器)。在中间还有一个名为“extension”的类装载器,它负责装载 JRE ext 目录中的类。类装载器实行双亲委派机制,也就是说应用类装载器可以看到启动类装载器所装载的所有的类,而启动类装载器却看不到应用类装载器所装载的类。
Sun 的这一机制最先是在远程方法调用(RMI)中实现 Java 序列化的时候碰到了问题。在从流中反序列化数据为 Java 对象时需要应用的类的相关信息,但是反序列化代码是由启动装载器所装载;根据双亲委派模型它也就访问不到那些类。这个问题的最终解决是通过给 JRE 添加了私有的本地代码,这些代码允许了对调用栈的检查,通过检查来找到第一个非 null 类的装载器。
但是,很多其他扩展包随后很快也遇到了同样的问题,而且他们不能够(也不应该!)都通过在 Sun JVM 中添加私有本地代码的办法来解决。大约在同一时间,J2EE 变得愈发重要。J2EE 是一个应用模型,其中的 Java 代码需要在一个严格约束的环境中才能运行。应用都运行在一个“仓库”中。每个应用运行在一个独立的类装载器中,所有线程是不能够跨应用/仓库执行的。应用当然可以使用由 VM 提供的扩展包以及由容器提供的那些包,但是应用之间还是无法使用来自对方的任何代码。此外,这一模型还遇到了由容器提供的包无法访问应用代码的问题。
为此 Java 1.2 引入了 TCCL:作为一个 tread-local 变量的和某个线程关联在一起的一个类装载器。这意味着什么?这意味着任何包都可以在任意时候通过当前调用线程访问“当前”上下文装载器。这一上下文装载器被期望予能够访问负责本次调用的特定应用类装载器,然后能够对应用的类进行访问。
Sun 自己也开始大量使用 TCCL,尽管对此还没有合适的规范。JDK 1.5 有 79 处引用到了 Thread.getContextClassLoader()。Java 1.4 之后,Java 在 JNDI、JAXP、CORBA、JMX、Xalan、Xerces、AWT、Beans、SQL、Logging、Prefs、RMI、Security、Swing 以及大部分的 XML 子系统的虚拟机实现上已经修改为使用上下文类装载器。此外大部分中间件库,比如 Hibernate、Saxon、Jakarta Commons Logging 等等也已开始使用 TCCL。Java 对 TCCL 的使用几乎没有提供任何规范或指南。这也就导致了几乎每个库都按照不同的策略对它的进行使用的后果。
关于正确使用 TCCL 的两个关键问题是:
  • 什么时候对它进行设定,由谁来设定?
  • 它应该能够访问哪些类?
基于 J2EE 的视角来看这些问题都比较容易回答,因为编程模型是受到约束的。容器掌控了所有的入口点(比如,它控制着执行 Servlet 的 HTTP 线程以及 EJB 的 RMI Socket 监听线程,禁止创建额外线程,等等),因此它能够保证 TCCL 总是在进入应用程序代码的入口处进行设置。由于应用程序是完全独立的,因此这些能够访问的类的集合也就仅仅是该应用所拥有的所有类了。
但是在一个类似于 OSGi 的运行时模块化的场景下这些问题就很难回答了。bundle 能够自由地创建它们自己的线程和切入点,并且也没有通用的办法来为一个“应用”确定为其提供类的 bundle 集合;的确,这里“应用”的概念本身就难以准确定义。我们可以限制编程模型并且要求一个应用应该作为一个没有任何依赖的独立的 bundle 进行部署,但这样做的话也就丢掉了使用 OSGi 的大部分好处。因此 OSGi 并没有试图去规定 TCCL 应该在什么时候使用。

TCCL 的替代品

在我们自己的 OSGi 规范的代码里,或者在任何显式 OSGi 支持的库里,我们无需担忧 TCCL,因为 OSGi 服务提供了一个远比任何基于类装载器途径来进行装载的途径更轻便的途径。但是(应用中)遗留的第三方库依旧是一个问题。很多这样的库试图在运行时根据名字去装载应用的类:比如,Hibernate 从 .hbm.xml 文件中读取类名然后为每个数据库记录创建这些类的实例。
当你将这样一个库放到任何模块化的场景下时 - 包括 OSGi、JBoss 模块或者 Jigsaw - 你会发现行不通,因为一个类仅通过类名不足以对其进行唯一标识。一个类的识别由其完整描述名以及定义它的类装载器构成(在 OSGi 中相当于包含它的 bundle)。因此作为类名的补充,我们还需要知道负责装载该类的类装载器。暨于由不同应用服务器创建的种类繁多的类装载环境,很多库试图使用一些探索式的方法来解决这个问题。TCCL 通常就是这样一个探索式解决办法之一,此外还有检查该库自己的类装载器,使用 JRE 扩展类装载器等等办法。
如果一个库只考虑 TCCL 的话会遇到一些阻碍:在我们该库之前就要显式地在我们的代码中设置 TCCL。幸运的是,这种情况很少发生,比如大多数这种库也将调用 Class.forName(),这意味着该库将会使用自己的类装载器来装载类。虽然这还远远不够理想,但是我们可以通过部署一个单独的片段来解决问题,而不是在我们的代码中散乱地调用 setContextClassLoader()。更好的做法当然是一个库完全避开类名并允许类对象的传递;或者至少提供一个 API 方法来设置类装载器以从中装载类名。
不幸的是很难事先对此进行预测 - 至少没有仔细检查(库)代码 - 该库采用了何种探索式解决办法,如上文所述这还是由于缺乏完整的规范和指南所造成的结果。

总结

我希望大家都能出席我明天的演讲,该演讲主要是针对想要自己的代码如何在 OSGi 中工作的更好的 Java 库的程序员们,但也不会仅局限于 OSGi。正如你从本文中能够推断出的那样,避免 TCCL 以及其他基于类装载器的怪异做法是实现友好 OSGi 的很重要的一点。此外我的演讲中还会涉及服务动态化以及一些配置问题。希望明天能看到你们!
2012 年 10 月 23 日
原文链接: The Dreaded Thread Context Class Loader
posted @ 2017-02-03 15:43  Defonds  阅读(65)  评论(0编辑  收藏  举报