JDBC获取连接中的java知识(2):SPI、中台

1,055

本文承接上文《JDBC获取连接中的java知识(1):类的加载》,通过学习本文可以习得以下内容

  • SPI模式是什么,怎么用;
  • ServiceLoader源码分析
  • 一种对插件和中台友好的架构设计;

提出问题

上次讲到了JDBC中的DriverManager的类初始化时会loadInitialDrivers,参考其源码可以发现loaderDrivers的核心流程是ServiceLoader相关的操作,创建serviceLoader并不断调用interator.next。执行之后registeredDrivers就有了可以用来提供服务的driver实例。但这中间是如何实现并产出driver实例的?

// 1
   ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
   Iterator<Driver> driversIterator = loadedDrivers.iterator();
   try{
// 2
        while(driversIterator.hasNext()) {
            driversIterator.next();
            }
        } catch(Throwable t) {
            // Do nothing
            }
        return null;
    }

SPI的含义

接口角度

SPI全称是service provider interface和API(application programming interface)的语意非常接近,两者都是对类、接口和方法的描述。

但不同的是,如果说API是提供信息给调用方使用,API告诉调用者一个类、接口和方法如何使用来达到调用者的目的;SPI则是提出要求让服务提供方实现,SPI告诉实现者要满足什么要求才能提供服务。

API和SPI可能是不同的class/interface,也可以有双重身份。比如本文涉及的JDBC的Connection具有双重身份:因为jdbc直接把connection提供给用户使用,所以connection是jdbc提给应用方的api;同时connection要由驱动实现方实现,对db的driver它就是一个SPI了。Driver其实更像一个单纯的SPI,因为jdbc主要通过DriverManager而非Driver本身提供服务给应用方。

模型角度

SPI同时是一种编程架构和类动态加载的实现机制,这也就是下文要讲的内容。

SPI中的设计思想

在上一节classloading中讲到loading中的二进制字节流可以来自于任意地方,那么是不是可以在使用时按照一种规范统一加载、初始化、实例化一些自己想要的类呢? 具体到一个interface可以有多个接口实现方,接口实现方是不是按照约定的规范实现自己的implementation之后就能自动提供服务呢呢?

顺着这个思路就捋顺了SPI的设计。

在SPI模型中这个供大家来实现的interface称为ServiceProviderInterface也就是SPI,各个实现这个接口的具体类称为ServiceProvider,负责统一动态加载providers然后进行实例化的类就是ServiceLoader,约定的规范就是在META-INF/services路径下的配置文件

JDBC中SPI的核心组成

通过上图SPI模型的核心组成已经很清晰了:

  1. ServiceProviderInterface:这个类完全由应用方定义。在JDBC中,Driver.class是Java系统希望所有DBMS实现的标准服务接口,所以这里java.sql.Driver就是ServiceProviderInterface;
  2. ServiceProvider:这个就是完整实现(1)的具体类,比如mysql的connector-J中实现Driver的com.mysql.cj.jdbc.Driver;
  3. ServiceLoader:java.util.ServiceLoader实现了发现providers,并进行加载实例化的全过程;ServiceLoader是整个SPI模型的核心,来自于java核心库,下一节将展开分析它的实现;
  4. META-INF/services/SPI-name下的配置文件:这个配置文件是ServiceLoader进行扫描的目标,并按照其中信息进行类的加载。比如在mysql-connector中的文件如下图所示,以SPI命名并以ServiceProvider的fullName为内容;

ServiceLoader

下面重点分析一下ServiceLoader类的设计。

serviceLoader fields

  // 约定好的配置路径
    private static final String PREFIX = "META-INF/services/";

  // load的对象
  // The class or interface representing the service being loaded
    private final Class<S> service;

 // 需要用到的类加载器
    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

// 资源权限控制
    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

// 用于存储加载后的serviceProvider实例,注意不是static的;
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 核心设计,懒加载的实现类
    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;

构造器需要的信息:为谁代言,怎么加载

load服务具体实现时,首先要明确实现的interface是哪个,并且用哪个ClassLoader加载; 于是构造器的两个入参就是Class<S>和Classloader。

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
  • 当传入的Classloader是null时,ServiceLoader会直接去读SystemClassloader,一般情况下scl就是上一节中讲到的Laucher中的appClassloader;
  • 另一个值得注意的是AccessControlContext acc这个字段来帮助判断获得资源的权限验证;
  • 最后一个关键点就是reload方法,reload方法首先清空了之前实例并初始化了用于加载的关键的lazyIterator
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

ServiceLoader的构造器是私有的,开放了三个静态方法来获得SL实例。

JDBC中只传入了class作为入参,可以发现Serviceloader直接使用当前threadContext中的classloader,这是一种非常简便的方法来确认classloader。如果用户没有设置过threadContextClassloader,那么在上一节中讲过,launcher函数会把thread中的cl设置为appClassloader。

// jdbc中加载方法
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

// ServiceLoader中该方法定义
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

iterator的组成

这里先看一下JDBC是如何加载所有drivers的,JDBC获得了iterator之后不断遍历,然后就结束了。 这里可能会有个疑问,JDBC没有使用任何遍历后的结果,也没有特殊的操作,为什么这个遍历之后就可以有可以用的数据库驱动能直接获得connection了呢

这个问题的答案有两部分:1)iterator在遍历过程中做了什么;2)JDBC的Driver是如何设计的;

// JDBC对serviceLoader的使用全过程
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
                
                
 //  driversIterator.next的源码实现              
            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

serviceloader返回的iterator其实内部分为两部分:一个是之前讲过的实例化缓存好的providers的iterator命名为knownIterator,一个上文初始化的LazyIterator命名为lookupIterator;在hasNext和next调用时,总是优先返回已知的provider,如果为null或者false再去调用lookupIterator。

而在这个serviceloader第一次使用时providers总是空的,因为ServiceLoader整体的加载策略是一个lazy的策略:实例化serviceloader并不会触发eager的类的加载与实例化,而是在用时一个个加载,这也是为什么执行lookup的类称为lazy的缘故。

第一次providers都是空的,那么jdbc调用的next实际都是在执行lookupIterator的next,也就是LazyIterator的next。而类的加载和实例化的核心就在Lazy这个类里。

LazyLookup by iterator

常常觉得这个类有点歧义,它实际上是在用遍历的方法懒惰的寻找serviceProvider,可能叫做LazyLookup更合适。 LazyInterator的核心实现是hasNextService和nextService,也就是判断是否还有provider,有则加载实例化返回。

hasNextService

hasNextService的核心就是使用classloader,在所负责的类的路径中,按约定好的路径META-INF/services/SPI-name来寻找provider,如果能找到就在每次调用时解析出一个provider的名字,存在nextName里。

注意这里的实现是lazy的,并不会把所有的configs的内容全部解析完,而是调用一次解析一次。

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                // --- 约定路径
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                    // --- classloader 委派模型来查找资源
                    // --- 查找结果存在configs里,只搜索一次,搜索是一次性的
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                // ---解析文件中serviceProvider名字的过程,由于一个文件中可能有多个provider,所以pending实际也是一个iterator;
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

nextService

在hasNextService解析名字之后,nextService直接根据name进行类的加载、校验和实例化,并把实例化的结果放到providers里。

            Class<?> c = null;
            try {
            // 加载类,但并不进行实例化,false字段关闭initialization
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            // 校验
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
            // 实例化并缓存
                S p = service.cast(c.newInstance());
                providers.put(cn, p);

自动注册的实例

讲到这里serviceLoader如何工作的核心机制就讲完了,但是刚刚有个尾巴,就是jdbc代码不断调用next并没有对返回的实例做了什么特殊处理?当时说答案有两部分:1)iterator做了什么,根据刚刚的讲解,当jdbc代码中不断调用next时serviceLoader把所有支持的drvier都加载并实例化了;2)但是显然JDBC并没有使用返回的实例,这是因为JDBC使用的是另一种机制,也就是实例的自动注册。

JDBC借用SPI模式下的两个特点(1)发现所有providers(2)初始化类。当类初始化时,Driver中的static代码就可以将Driver实例自动注册到manager中去提供服务了

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
        // 类初始化的自动注册过程
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

整个JDBC获得连接中隐藏的设计可以总结为下图。这套设计及其中使用的服务发现、iterator实现懒加载、实例自动注册、classLoader进行资源扫描等思路方法都非常值得学习。

插件式管理,中台的应用

这一部分应用场景实在是抛砖引玉。

插件式的产品

上图提供了一种插件式的管理方案,providers就是实现了产品基本接口的个性化插件,可以通过serviceLoader自动加载并实例化,providers可以自动注册到产品的插件管理中心。

业务中台

这种架构模式在业务开发中常常用于中台。举个例子来说,当一个业务的主要业务是A,但是业务方BCD的逻辑实现都可能都略有不同。这个时候中台开发者开发者只需要实现核心逻辑,把个性化的业务逻辑定义成一个ServiceProvider,业务方ABCD自己实现provider让中台项目能通过classLoader(可能是自定义的)加载这些类就可以了。