不遵循双亲委派的类加载器 -- 线程上下文类加载器

2018-05-21 22:48:44   最后更新: 2018-05-22 14:20:00   访问数量:358




上一篇日志我们介绍了 java 类加载器与他们实际的类实现:

java 类加载器详解 -- 双亲委派模式及实现

其中,我们介绍了类加载器的双亲委派原则,以及很重要的一个原则:

  • 类 A 引用到类 B,则由类 A 的加载器去加载类 B,保证引用到的类被一同载入到系统

但是,如果一个接口在 rt.jar 中由 BootstrapClassLoader 加载,可是他引用的具体实现类却在用户空间中,根据可见性原则,这个实现类不能由 BootstrapClassLoader 来加载,这样就违背了上面提到的原则,解决办法只能是让 rt.jar 中的对应实现类被一个其他的类加载器来加载,因此就必须有一个打破双亲委派原则的 ClassLoader 出现,于是就诞生了 ThreadContextClassLoader 来解决这个问题

那么,什么场景下会出现上面的这个问题呢?那就是在下面即将介绍的 SPI 机制中

 

上面提到了 SPI,我们首先来了解一下 java 中的 SPI 究竟是一个什么机制

SPI (Service Provider Interface)是一个针对厂商或者插件提供者的机制

我们系统中抽象的各个模块,往往有很多不同的实现方式,比如日志的模块方案就有 xml 解析模块、jdbc 模块的方案等,因此,为了实现可插拔原则,在模块装配时是不能在程序里静态指明的,而是需要通过接口的方式,在实际使用时才加载具体的实现类

 

SPI 的约定

java spi的具体约定是:当服务的提供者,提供了服务接口的一种实现之后,在 jar 包的 META-INF/services/ 目录里同时创建一个以服务接口命名的文件

该文件里就是实现该服务接口的具体实现类,而当外部程序装配这个模块的时候,就能通过该 jar 包 META-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入

例如,数据库操作的 spi 约定,驱动提供商的类位于 META-INF/services/java.sql.Driver 中,实际的实现者例如 JDBC 就会通过在该文件中创建具体的实现类来将接口暴露给驱动提供者

这是一个基于类加载机制的 IOC 设计模式的实现,也就是说,数据库操作的驱动的相关接口均在 rt.jar 中,而实现类在 META-INF/services/java.sql.Driver 中,这样就违背了类加载机制的加载原则

 

实现示例

我们来看一个简单的例子来更加清晰的说明上面的情况:

public class DriverManager { //省略不必要的代码 static { loadInitialDrivers();//执行该方法 println("JDBC DriverManager initialized"); } //loadInitialDrivers方法 private static void loadInitialDrivers() { sun.misc.Providers() AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { //加载外部的Driver的实现类 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); //省略不必要的代码 } }); } }

 

我们看到,在代码中通过 ServiceLoader.load(Driver.class) 方法加载了 java.sql.Driver 类,可是这个类并不在 rt.jar 中,所以 DriverManager 如果被 BootstrapClassLoader 加载,那么他是一定无法加载到 Driver.class 从而抛出 ClassNotFoundException 的,也就是说我们需要另一个加载器去加载这个 Driver.class

 

 

 

所谓的 ThreadContextClassLoader 其实就是 Thread 类中的 contextClassLoader 对象,他是一个 ClassLoader 类型的对象,正如我们上篇博客中看到的,他默认是 AppClassLoader 对象

而 contextClassLoader 对象的 getter 和 setter 是公开的,这就让我们可以去为 Thread 设置独特的 contextClassLoader 了

 

ServiceLoader 实现了对 Driver.class 的加载

public static <S> ServiceLoader<S> load(Class<S> service) { //通过线程上下文类加载器加载 ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }

 

 

上述的例子中,由于 contextClassLoader 实际上就是默认的 AppClassLoader,也就是说,BootstrapClassLoader 实际上反向委托给了 AppClassLoader 去加载用户空间中的具体实现类

可是为什么不直接指定其他的 ClassLoader 呢?ThreadContextClassLoader 存在的价值是什么?

 

为什么不直接指定 AppClassLoader

AppClassLoader 作为 ThreadContextClassLoader 的默认实现,这里确实可以直接使用,但是一旦强制指定 AppClassLoader 就意味着我们永远无法使用自定义的 ClassLoader 来加载我们想要的类了,极大的丧失了灵活性

 

为什么不使用 getSystemClassLoader 方法获取系统默认类加载器

默认情况下,通过 getSystemClassLoader 方法获取的类加载器也是 AppClassLoader,但是随着代码部署到不同的环境,如 java web 服务等,都会更改系统默认类加载器的实现,而如果我们设置了系统默认的类加载器去加载 spi 的实现类,就会导致整个服务无法执行正确的预先写好的类加载器的方法,从而出现无法预料的结果

 

因此,通过每个线程独有的 contextClassLoader 来进行定制化的操作是最为灵活和安全的

 

java中的SPI机制 -- https://blog.csdn.net/sigangjun/article/details/79071850

Java 类加载体系与ContextClassLoader -- https://blog.csdn.net/v1v1wang/article/details/6864573

 






技术帖      技术分享      线程            class      thread      java      context      classloader      双亲委派      类加载器     


京ICP备15018585号