其他分享
首页 > 其他分享> > SPI 机制以及 jdbc 打破双亲委派

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

作者:互联网

文章目录


文章已收录我的仓库: 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 知道使用哪个实现类,因此官方定义了如下规则:

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

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

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

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

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

public Iterator<S> iterator() {
    return new Iterator<S>() {
        int index;
        @Override
        public boolean hasNext() {
            if (index < instantiatedProviders.size())
                return true;
            return lookupIterator1.hasNext();
        }
    };
}

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

private Iterator<Provider<S>> newLookupIterator() {
    Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();
    Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();
    return new Iterator<Provider<S>>() {
        @Override
        public boolean hasNext() {
            return (first.hasNext() || second.hasNext());
        }
        @Override
        public Provider<S> next() {
            if (first.hasNext()) {
                return first.next();
            } else if (second.hasNext()) {
                return second.next();
            } else {
                throw new NoSuchElementException();
            }
        }
    };
}

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

@Override
public boolean hasNext() {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

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

private boolean hasNextService() {
    while (nextProvider == null && nextError == null) {
        try {
            Class<?> clazz = nextProviderClass();
            if (clazz == null)
                return false;
            if (service.isAssignableFrom(clazz)) {
                Class<? extends S> type = (Class<? extends S>) clazz;
                Constructor<? extends S> ctor  = (Constructor<? extends S>)getConstructor(clazz);
                ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
                nextProvider = (ProviderImpl<T>) p;
            } 
        } catch (ServiceConfigurationError e) {
            nextError = e;
        }
    }
    return true;
}

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

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

static final String PREFIX = "META-INF/services/";
private Class<?> nextProviderClass() {
    if (configs == null) {
        // 路径名
        String fullName = PREFIX + service.getName();
        // 根据路径名取得资源,这个 loader 是线程上下文取得的
        configs = loader.getResources(fullName);
    }
    // pending 是一个 String 的迭代器
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return null;
        }
        pending = parse(configs.nextElement());
    }
    String cn = pending.next();
    try {
        return Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        return null;
    }
}

可以看到在这一步通过 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()); 方法,来看看这个方法:

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

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

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

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

写一个 demo

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

public class Test {
    public static void main(String[] args) {
        ServiceLoader<HelloPrinter> load = ServiceLoader.load(HelloPrinter.class);
        Iterator<HelloPrinter> iterator = load.iterator();
        while (iterator.hasNext()) {
            HelloPrinter helloPrinter = iterator.next();
            helloPrinter.hello();
        }
    }
}

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

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

image-20220108182310295

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

com.demo.HelloPrinterImpl1
com.demo.HelloPrinterImpl2

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

我是第一个实现者!
我是第二个实现者!

JDBC 打破双亲委派模型

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

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

image-20220108193615789

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

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

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 这个类明显不能由引导类加载器加载,而是由应用类加载器(也叫系统类加载器)加载。

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

输出是:

platform
app

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

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

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

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

ensureDriversInitialized() 这个方法:

private static void ensureDriversInitialized() {
    synchronized (lockForInitDrivers) {
        if (driversInitialized) {
            return;
        }
        String drivers;
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                while (driversIterator.hasNext()) {
                    driversIterator.next();
                }
                return null;
            }
        });
        driversInitialized = true;
    }
}

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

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
    
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

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

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

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

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

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

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

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

下文加载器,这个线程上下文类加载器在上一篇文章已经详细说了,就不再赘述了。

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

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

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

标签:jdbc,return,public,SPI,双亲,hasNext,ServiceLoader,加载
来源: https://blog.csdn.net/m0_51380306/article/details/122386121