[toc]

文章已收录我的仓库:Java学习笔记

SPI 机制以及 JDBC 打破双亲委派

本文基于 jdk 11

SPI 机制

简介

何为 SPI 机制?

SPI 在Java中的全称为 Service Provider Interface,是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

例如本文的中心 jdbc,我们可以使用统一的接口 Connection 去操控各种数据库,但你有没有想过,难道 jdk 真的内置了所有的数据库驱动吗?

显然这是不可能的,我们平时使用需要我们自己导入对应 jar 包去使用不同的数据库,例如 MySQL、SqlServer 数据库等。可是,我们使用的Connection conn = DriverManager.getConnection(url,user,pass)又是确确实实是 jdk 内置的一个接口,其实这就是一种 SPI 机制,即官方定义好一个接口,由不同的第三方服务者去实现这个接口。

那么问题来了,SPI 机制是如何实现的呢?

答案很简单,遵守官方的约定

jdk SPI 原理

SPI 可以有不同实现,但无论怎样核心思想都是遵守官方规则。官方不一定要是 jdk,例如 SpringBoot 就定义了一套规则,但我们这里仍然讲 jdk 的实现原理。

实现 SPI 机制的关键点在于要知道接口的具体实现类是哪一个,这些实现类是第三方服务提供的,必须得有一种方法让 JVM 知道使用哪个实现类,因此官方定义了如下规则:

  • 服务提供者的类必须要实现官方提供的接口(或继承一个类)。
  • 将该具体实现类的全限定名放置在资源文件下 META-INF/services/${interfaceClassName} 文件中,其中 ${interfaceClassName} 是接口的全限定名。
  • 如果扫描到多个具体实现类,jdk 会初始化所有的这些类。

这就是 jdk 的约定,既然要求服务者提供的类名放在 META-INF/services/${interfaceClassName} 文件下,那么官方肯定要去扫描这个文件,官方提供了一个实现:ServiceLoader.load 方法。

获取具体类实例的伪代码如下:

  1. ServiceLoader load = ServiceLoader.load(XXX.class);
  2. for (XXX x : load) {
  3. // o 就是我们要获取的实例,XXX 是一个官方定义的接口接口
  4. System.out.println(x);
  5. }

ServiceLoader.load 方法返回一个 ServiceLoader 实例,我们需要通过遍历其迭代器去获取所有可能的实例,核心代码就在迭代器中了。

我们来看看这个迭代器的源码,我们主要看 hasNext 方法,因为 next 方法本身依赖于 hasNext 方法:

  1. public Iterator<S> iterator() {
  2. return new Iterator<S>() {
  3. int index;
  4. @Override
  5. public boolean hasNext() {
  6. if (index < instantiatedProviders.size())
  7. return true;
  8. return lookupIterator1.hasNext();
  9. }
  10. };
  11. }

跳到了 lookupIterator1.hasNext() 方法中,lookupIterator1 是调用 newLookupIterator() 方法返回的,来看看这个方法:

  1. private Iterator<Provider<S>> newLookupIterator() {
  2. Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();
  3. Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();
  4. return new Iterator<Provider<S>>() {
  5. @Override
  6. public boolean hasNext() {
  7. return (first.hasNext() || second.hasNext());
  8. }
  9. @Override
  10. public Provider<S> next() {
  11. if (first.hasNext()) {
  12. return first.next();
  13. } else if (second.hasNext()) {
  14. return second.next();
  15. } else {
  16. throw new NoSuchElementException();
  17. }
  18. }
  19. };
  20. }

可以发现主要有两种加载方式,一种是模块化的加载,另一种是普通的懒加载方式,我们应该会进到 second.hasNext() 方法:

  1. @Override
  2. public boolean hasNext() {
  3. if (acc == null) {
  4. return hasNextService();
  5. } else {
  6. PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
  7. public Boolean run() { return hasNextService(); }
  8. };
  9. return AccessController.doPrivileged(action, acc);
  10. }
  11. }

second.hasNext() 方法又调用了 hasNextService() 方法,来看看这个方法的逻辑:

  1. private boolean hasNextService() {
  2. while (nextProvider == null && nextError == null) {
  3. try {
  4. Class<?> clazz = nextProviderClass();
  5. if (clazz == null)
  6. return false;
  7. if (service.isAssignableFrom(clazz)) {
  8. Class<? extends S> type = (Class<? extends S>) clazz;
  9. Constructor<? extends S> ctor = (Constructor<? extends S>)getConstructor(clazz);
  10. ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
  11. nextProvider = (ProviderImpl<T>) p;
  12. }
  13. } catch (ServiceConfigurationError e) {
  14. nextError = e;
  15. }
  16. }
  17. return true;
  18. }

这个方法首先判断 nextProvider 是不是空,如果不是的话说明已经有资源,直接返回;我们是初次加载,肯定为空,因此进入循环,可以看到主要的方法就是 nextProviderClass(),通过这个方法返回一个构造器,然后进行实包装,并设置 nextProvider 等于包装后的 ProviderImpl,有了 ProviderImpl,自然可以通过反射获取实例!

这里核心方法应该是 nextProviderClass() :

  1. static final String PREFIX = "META-INF/services/";
  2. private Class<?> nextProviderClass() {
  3. if (configs == null) {
  4. // 路径名
  5. String fullName = PREFIX + service.getName();
  6. // 根据路径名取得资源,这个 loader 是线程上下文取得的
  7. configs = loader.getResources(fullName);
  8. }
  9. // pending 是一个 String 的迭代器
  10. while ((pending == null) || !pending.hasNext()) {
  11. if (!configs.hasMoreElements()) {
  12. return null;
  13. }
  14. pending = parse(configs.nextElement());
  15. }
  16. String cn = pending.next();
  17. try {
  18. return Class.forName(cn, false, loader);
  19. } catch (ClassNotFoundException x) {
  20. return null;
  21. }
  22. }

可以看到在这一步通过 PREFIX + service.getName() 获取文件名,这个就是我们说的META-INF/services/${interfaceClassName} 约定的文件,然后取得对应资源,pending 是一个 String 的迭代器,其内就是具体实现类的全限定名,如果这个迭代器存在,说明已经解析过了,则直接返回 Class.forName(pending.next(), false, loader)

如果 pending 迭代器到底了,那么会再一次进入循环,并调用 configs.nextElement() 再次读取,这也就是说明如果有多个配置文件,那么所有的类都会被加载!直到真的什么都没有了,那么抛出异常,返回 null,这里返回 null 的话,那么hashNext 方法就会返回 false。

如果是第一次解析,那么会进入到 pending = parse(configs.nextElement()); 方法,来看看这个方法:

  1. private Iterator<String> parse(URL u) {
  2. Set<String> names = new LinkedHashSet<>(); // preserve insertion order
  3. URLConnection uc = u.openConnection();
  4. uc.setUseCaches(false);
  5. try (InputStream in = uc.getInputStream();
  6. BufferedReader r = new BufferedReader(new InputStreamReader(in, UTF_8.INSTANCE))) {
  7. int lc = 1;
  8. while ((lc = parseLine(u, r, lc, names)) >= 0);
  9. }
  10. return names.iterator();
  11. }

这个方法很简单,就是读取配置中的每一行,添加到 Set 中,然后返回其迭代器。所以我们将返回一个配置文件中所有的全限定名!

这就是 jdk 提供的 SPI 机制的具体实现原理!

注:上述源码经过了些许简化

写一个 demo

假如我们有一个 HelloPrinter 的接口,这个接口需要第三方来提供,则可以这样编写来获取具体实现类:

  1. public class Test {
  2. public static void main(String[] args) {
  3. ServiceLoader<HelloPrinter> load = ServiceLoader.load(HelloPrinter.class);
  4. Iterator<HelloPrinter> iterator = load.iterator();
  5. while (iterator.hasNext()) {
  6. HelloPrinter helloPrinter = iterator.next();
  7. helloPrinter.hello();
  8. }
  9. }
  10. }

试问,这里有没有出现任何关于具体实现类的信息?没有!具体实现类对客户来说是完全透明的,客户只知道 HelloPrinter 这个接口!现在什么都没做,这个代码什么也不会输出。

然后我们启动另一个项目写两个实现类 HelloPrinterImpl1 和 HelloPrinterImpl2,按照约定建立如下目录:

SPI 机制以及 jdbc 打破双亲委派 - 图1

在这个文件中填写实现者的全限定名:

  1. com.demo.HelloPrinterImpl1
  2. com.demo.HelloPrinterImpl2

然后 maven 打包,让客户端引入这个 jar 包,重新运行,现在客户端的输出是:

  1. 我是第一个实现者!
  2. 我是第二个实现者!

JDBC 打破双亲委派模型

本节前置知识:类加载机制

JDBC 其实也是一种 SPI 机制,例如当我们引入 MYSQL 驱动的 jar 包时:

SPI 机制以及 jdbc 打破双亲委派 - 图2

可以发现这与我们上面讲的一模一样,由此可见我们使用的驱动应该是 “com.mysql.cj.jdbc.Driver” 这个类。

当引入了 jar 包之后,则可以通过代码:

  1. Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/xxx?serverTimezone=GMT", "root", "123456");

来获取数据库连接,Connection 是官方的接口,无论什么数据库都可以一样操作,非常方便。

但我们为什么说 jdbc 打破了双亲委派模型呢?

原因在于 DriverManager.getConnection 方法其实是加载了 com.mysql.cj.jdbc.Driver 这个类,然后这个驱动去创建连接。

问题是 DriverManager 是属于 jdk 的官方类,应当是由引导类加载器(jdk8 以前叫启动类加载器)加载的,而 com.mysql.cj.jdbc.Driver 这个类明显不能由引导类加载器加载,而是由应用类加载器(也叫系统类加载器)加载。

  1. public class Test {
  2. public static void main(String[] args) throws Exception {
  3. System.out.println(DriverManager.class.getClassLoader().getName());
  4. Class c = Class.forName("com.mysql.cj.jdbc.Driver");
  5. System.out.println(c.getClassLoader().getName());
  6. }
  7. }

输出是:

  1. platform
  2. app

可见确实是由平台类加载器和应用类加载器去加载这两个类,问题是 DriverManager 要如何加载 com.mysql.cj.jdbc.Driver 驱动,直接使用 Class.forName 可行吗?

不可行!因为 Class.forName 默认会采用调用者自己的类加载器去加载,DriverManager 的类加载器是平台类加载器,显然加载不到驱动类。

所以 DriveManager 必须要使用次一级的类加载器去加载,这里是使用了 SPI 机制,在 getConnection 方法中,我们调用了一行代码:

  1. private static Connection getConnection(
  2. String url, java.util.Properties info, Class<?> caller) throws SQLException {
  3. // 省略
  4. ensureDriversInitialized();
  5. // 省略
  6. return driver.connect(url, info);
  7. }

ensureDriversInitialized() 这个方法:

  1. private static void ensureDriversInitialized() {
  2. synchronized (lockForInitDrivers) {
  3. if (driversInitialized) {
  4. return;
  5. }
  6. String drivers;
  7. AccessController.doPrivileged(new PrivilegedAction<Void>() {
  8. public Void run() {
  9. ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  10. Iterator<Driver> driversIterator = loadedDrivers.iterator();
  11. while (driversIterator.hasNext()) {
  12. driversIterator.next();
  13. }
  14. return null;
  15. }
  16. });
  17. driversInitialized = true;
  18. }
  19. }

可以看到在这个方法内调用了 loadedDrivers = ServiceLoader.load(Driver.class) 方法,然后遍历迭代器,driversIterator.next() 相当于实例化了 Drive,别看它好像啥也没做,但事实上在实例化的过程中,触发了 Drive 类的初始化语句块的调用:

  1. public class Driver extends NonRegisteringDriver implements java.sql.Driver {
  2. public Driver() throws SQLException {
  3. }
  4. static {
  5. try {
  6. DriverManager.registerDriver(new Driver());
  7. } catch (SQLException var1) {
  8. throw new RuntimeException("Can't register driver!");
  9. }
  10. }
  11. }

它向 DriverManager 注册了自己!因此 DriverManager 可以保存这个 Driver!

ServiceLoader 加载实现原理上面已经说了,但我故意没讲 ServiceLoader.load 方法:

  1. @CallerSensitive
  2. public static <S> ServiceLoader<S> load(Class<S> service) {
  3. ClassLoader cl = Thread.currentThread().getContextClassLoader();
  4. return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
  5. }

load 方法内,设置默认的加载器为线程上下文加载器,这个线程上下文类加载器在上一篇文章已经详细说了,就不再赘述了。

由于我们在主线程调用的 DriverManager.getConnection 方法,现在已经很明显了,ServiceLoader 使用了主线程的类加载器去加载,这当然是应用加载器!

现在再来总结一下为什么说 jdbc 打破了双亲委派,我认为有两点:

  1. DriverManager 属于 jdk 官方类,使用平台加载器,然而却使用了应用加载器去加载 Driver 类,这相当于是高层次的类使用了低层次的类加载器,这算是逆向打通了双亲委派模型。
  2. 另外就是 ServiceLoader 使用线程上下文加载器直接获得类加载器,而没有一层一层向上调用,这肯定也是违反了双亲委派模型的。