源码级深度理解 Java SPI( 三 )


学习过 JVM 的读者 , 想必都了解过类加载器的双亲委派模型(Parents Delegation Model) 。双亲委派模型要求除了顶层的 BootstrapClassLoader 外 , 其余的类加载器都应有自己的父类加载器 。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现 。
双亲委派机制约定了:一个类加载器首先将类加载请求传送到父类加载器 , 只有当父类加载器无法完成类加载请求时才尝试加载 。
双亲委派的好处:使得 Java 类伴随着它的类加载器,天然具备一种带有优先级的层次关系 , 从而使得类加载得到统一 , 不会出现重复加载的问题:

  1. 系统类防止内存中出现多份同样的字节码
  2. 保证 Java 程序安全稳定运行
例如:java.lang.Object 存放在 rt.jar 中 , 如果编写另外一个 java.lang.Object 的类并放到 classpath 中,程序可以编译通过 。因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 classpath 中的 Object 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 classpath 中的 Object 使用的是应用程序类加载器 。正因为 rt.jar 中的 Object 优先级更高,因为程序中所有的 Object 都是这个 Object 。
双亲委派的限制:子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已经加载的 。——这就导致了双亲委派模型并不能解决所有的类加载器问题 。Java SPI 就面临着这样的问题:
  • SPI 的接口是 Java 核心库的一部分 , 是由 BootstrapClassLoader 加载的;
  • 而 SPI 实现的 Java 类一般是由 AppClassLoader 来加载的 。BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库 。它也不能代理给 AppClassLoader,因为它是最顶层的类加载器 。这也解释了本节开始的问题——为什么加载 SPI 服务时,需要指定类加载器 ClassLoader 呢?因为如果不指定 ClassLoader,则无法获取 SPI 服务 。
如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是 AppClassLoader 。在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类 。线程上下文类加载器在很多 SPI 的实现中都会用到 。
通常可以通过Thread.currentThread().getClassLoader()和 Thread.currentThread().getContextClassLoader() 获取线程上下文类加载器 。
3.4 Java SPI 的不足Java SPI 存在一些不足:
  • 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现 。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费 。
  • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获?。?不能根据某个参数来获取对应的实现类 。
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的 。
四、SPI 应用场景SPI 在 Java 开发中应用十分广泛 。首先,在 Java 的 java.util.spi package 中就约定了很多 SPI 接口 。下面,列举一些 SPI 接口:
  • TimeZoneNameProvider: 为 TimeZone 类提供本地化的时区名称 。
  • DateFormatProvider: 为指定的语言环境提供日期和时间格式 。
  • NumberFormatProvider: 为 NumberFormat 类提供货币、整数和百分比值 。
  • Driver: 从 4.0 版开始,JDBC API 支持 SPI 模式 。旧版本使用 Class.forName() 方法加载驱动程序 。
  • PersistenceProvider: 提供 JPA API 的实现 。
  • 等等
除此以外,SPI 还有很多应用,下面列举几个经典案例 。
4.1 SPI 应用案例之 JDBC DriverManager作为 Java 工程师,尤其是 CRUD 工程师,相必都非常熟悉 JDBC 。众所周知,关系型数据库有很多种,如:MySQL、Oracle、PostgreSQL 等等 。JDBC 如何识别各种数据库的驱动呢?
4.1.1 创建数据库连接我们先回顾一下,JDBC 如何创建数据库连接的呢?
在 JDBC4.0 之前,连接数据库的时候 , 通常会用 Class.forName(XXX) 方法来加载数据库相应的驱动,然后再获取数据库连接,继而进行 CRUD 等操作 。
Class.forName("com.mysql.jdbc.Driver")而 JDBC4.0 之后 , 不再需要用Class.forName(XXX) 方法来加载数据库驱动 , 直接获取连接就可以了 。显然,这种方式很方便,但是如何做到的呢?
(1)JDBC 接口:首先,Java 中内置了接口 java.sql.Driver 。
(2)JDBC 接口实现:各个数据库的驱动自行实现 java.sql.Driver 接口,用于管理数据库连接 。

推荐阅读