9.5 线程上下文类加载器
线程上下文类加载器(context class loader)是在J2SE 1.2中引入的概念。Java中表示线程的java.lang.Thread类中有两个方法用来获取和设置当前线程的上下文类加载器。这两个方法分别是getContextClassLoader和setContextClassLoader,其中setContextClassLoader方法接受一个ClassLoader类的对象作为参数。当前线程中运行的代码可以使用该线程的上下文类加载器来加载Java类和资源文件。如果一个线程在创建之后没有显式地设置其上下文类加载器的值,则使用其父线程的上下文类加载器对象作为自身的上下文类加载器对象。程序启动时的第一个线程的上下文类加载器默认是Java平台的系统类加载器对象。因此,在默认情况下,通过当前线程的getContextClassLoader方法获取的类加载器对象和使用当前类的getClassLoader方法得到的类加载器对象是相同的,二者均为系统类加载器。
线程上下文类加载器提供了一种直接的方式在程序的各部分之间共享ClassLoader类的对象。当在程序中需要使用类加载器来加载类或资源时,有几种做法可以获取需要使用的ClassLoader类的对象。第一种做法是创建新的ClassLoader类的对象,这种做法使用的场合比较少。第二种做法是使用加载当前Java类的ClassLoader类的对象。第三种做法是使用Java平台提供的系统类加载器或扩展类加载器。这三种做法都无法简单地满足某些场景的需要。比如,存在两个互相关联的Java类A和B,这两个类必须由同一个类加载器对象来加载,否则会出现内部错误。满足这个需求的做法是用加载类A的ClassLoader类的对象去加载类B。如果使用前面提到的做法,就需要提供额外的方式在加载类A和类B的代码之间传递ClassLoader类的对象。这会带来附加的复杂性。只要加载类A和类B的代码在同一个线程中运行,使用线程上下文类加载器是最简单的做法。在加载类A时,获取当前使用的ClassLoader类的对象,调用当前线程对象的setContextClassLoader方法把线程上下文类加载器设置为该ClassLoader类的对象。在加载类B的时候,通过当前线程对象的getContextClassLoader方法来得到之前保存的ClassLoader类的对象,再进行加载即可。
线程上下文类加载器的重要作用是解决Java平台的服务提供者接口(service provider interface, SPI)带来的类加载相关的问题。Java平台上的很多规范都是以SPI的形式出现的,比如数据库访问规范JDBC和XML处理规范JAXP等。这些SPI的特点是Java平台只提供接口和部分辅助Java类,接口的具体实现由规范的实现者提供。以JDBC规范为例来说,相关的接口声明在java.sql和javax.sql包中,例如java.sql.Connection接口表示一个数据库连接。而Connection接口的具体实现类由数据库驱动来提供。SQL Server、MySQL和Apache Derby等数据库都有自己对应的JDBC接口的实现类。SPI接口通常会提供一些工厂方法来创建接口的具体实现对象。在第2章介绍Java的脚本语言支持API时提到的javax.script.ScriptEngineManager类就是一个典型的例子。ScriptEngineManager类用来管理当前程序中可用的脚本执行引擎。脚本语言开发者可以实现javax.script.ScriptEngine接口,使开发人员可以通过脚本语言支持API来使用这种脚本语言。当开发人员下载了该脚本语言对应的ScriptEngine接口的实现,并将其添加到程序的类路径(CLASSPATH)中之后,新的脚本执行引擎对ScriptEngineManager类来说必须是可见的,从而允许开发人员通过工厂方法得到对应的ScriptEngine接口的实现对象。脚本引擎在注册时只提供了类名。ScriptEngineManager类的对象为了能够提供ScriptEngine接口的具体实现对象,需要加载对应的Java类并创建出新的对象。
这里存在的问题是使用代理模式无法完成SPI实现类的加载。SPI接口相关的类本身作为Java标准库的一部分,是由启动类加载器来加载的。也就是说,加载ScriptEngineManager类的是启动类加载器。在ScriptEngineManager类的实现中需要查找程序的CLASSPATH来找到SPI的实现类,并创建出相应的对象。程序的CLASSPATH中的类一般是由系统类加载器负责加载的,启动类加载器无法完成相关的加载工作。而启动类加载器又无法把加载的工作代理给系统类加载器来完成,因为启动类加载器是系统类加载器的祖先。在理论上,可以在启动类加载器内部保存一个系统类加载器对象的引用,在加载SPI实现类时代理给系统类加载器来完成。但是在某些情况下,SPI实现类可能并不出现在CLASSPATH中,而是需要由自定义的类加载器对象来完成加载。启动类加载器是无法处理这种情况的。
为了解决这个问题,ScriptEngineManager类的对象在加载SPI实现类时,使用的是线程上下文类加载器。在默认情况下,程序运行时的线程上下文类加载器是系统类加载器,这样可以加载CLASSPATH中出现的SPI实现类。如果需要使用自定义类加载器来加载SPI实现类,可以把当前线程的上下文类加载器设置成能够加载到SPI实现类的类加载器对象。如果在程序运行中改变了线程上下文类加载器的值,可能会造成SPI的实现类无法加载。