zoukankan      html  css  js  c++  java
  • SPI机制剖析——基于DriverManager+ServiceLoader的源码分析

    我的上一篇博客类加载器与双亲委派中提到,SPI机制是一种上级类加载器调用下级类加载器的情形,因此会打破类加载的双亲委派模型。为了深入理解其中的细节,本博客详细剖析一下SPI机制,并以JDBC为例,基于源码来进行分析。

    SPI

    原理介绍

    SPI(Service Provider Interface),是JDK内置的服务提供发现机制。即JDK内部定义规范的接口,不同厂商基于标准服务接口实现具体的实现类和方法。SPI一般被用来做框架扩展的开发。
    下面这张图,很简明扼要地阐释了SPI的机理。
    SPI机制
    与SPI相对应的,是我们耳熟能详的API。API不需要上图中“标准服务接口”这一环节,而是调用方直接调用服务提供方。按照上一篇博客的分析,“标准服务接口”位于Java核心类库中,使用boot类加载器进行加载,而boot类加载器是无法获取“第三方实现类”的位置的。所以,相较于API而言,SPI需要打破双亲委派模型。

    优缺点

    好处

    但是,我陷入思考,SPI这样的模式有什么好处吗,或者说API有什么缺点吗?

    想象一下,如果程序直接调用第三方类库,当第三方类库发生改动时,应用程序代码很可能需要随之改动。但如果在JDK内部定义标准服务接口,要求第三方厂商实现这些接口,那无论实现类如何改动,只要标准接口不变,都不会影响到应用程序。所以我认为SPI机制的根本目的是为了“解耦”。这也就是面向对象中所谓的“接口编程”,把装配的控制权移到程序之外。

    许多著名的第三方类库都采纳了SPI机制,JDBC就是其中之一。数据库厂商会基于标准接口来开发相应的连接库。如MySQL何PostgreSql的驱动都实现了标准接口:java.sql.Driver。对于应用程序而言,无需关心是MySQL还是PostgreSql,只需要与标准服务接口打交道即可。SPI正是基于这种模式完成了解耦合。

    不足

    当然,即便如此,SPI依旧是存在缺点和不足的,如下:

    1. 不能按需加载。需要遍历所有的实现,并且进行实例化,某些实现的实例化可能很耗时,这样会造成浪费;
    2. 获取实现类的方式不够灵活,只能通过Iterator获取,不能根据某个参数来获取实现类;
    3. ServiceLoader类的实例线程不安全。

    JDBC的SPI机制

    首先来看一段使用JDBC的简单代码:

    @Test
    public void testJDBC() throws SQLException, ClassNotFoundException {
        String url = "jdbc:mysql://localhost:3307/mls";
        String userName = "root";
        String password = "123456";
        // Class.forName("com.mysql.cj.jdbc.Driver");
        Connection con = DriverManager.getConnection(url, userName, password);
        Statement statement = con.createStatement();
        String sql = "select * from mlsdb where id=1";
        ResultSet rs = statement.executeQuery(sql);
        while (rs.next()) {
            System.out.println(rs.getString("province"));
        }
    }
    

    注意到中间有一行注释的代码Class.forName("com.mysql.cj.jdbc.Driver");,其实这一行可写可不写。

    我的倒数第二篇博客类加载时机与过程里提到,Class.forName方法会触发“初始化”,即触发类加载的进行。因此如果写上这行代码,此处则是使用APP类加载器加载mysql的jdbc驱动类。

    然而,这一句Class.forName不用写,代码也能正常运行。因为加载DriverManager类时,会将MySQL的Driver对象注册进DriverManager中。具体流程后文会细说。其实这就是SPI思想的一个典型的实现。得益于SPI思想,应用程序中无需指定类似"com.mysql.cj.jdbc.Driver"这种全类名,尽可能地将第三方驱动从应用程序中解耦出来。

    下面,通过源码来分析驱动加载以及服务发现的过程,主要涉及到DriverManager和ServiceLoader两个类

    源码分析

    DriverManager是用于管理Jdbc驱动的基础服务类,位于Java.sql包中,因此是由boot类加载器来进行加载。加载该类时,会执行如下代码块:

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    

    上述静态代码块会执行loadInitialDrivers()方法,该方法用于加载各个数据库驱动。代码如下:

    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
    
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
    
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);//实例化ServiceLoader对象,并注入线程上下文类加载器和Driver.class
                Iterator<Driver> driversIterator = loadedDrivers.iterator();//获得迭代器
    
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();//进行类加载
             `       }
             `   } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
    
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
    
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
    
    • ServiceLoader.load(Driver.class)此方法会把实例化一个ServiceLoader对象,并且向其注入线程上下文类加载器和Driver.class;
    • loadedDrivers.iterator():获得ServiceLoader对象的迭代器;
    • driversIterator.hasNext():查找Driver类;
    • driversIterator.next():在实现的“next()”方法中进行类加载,使用上面的线程上下文类加载器。

    ServiceLoader.load(Driver.class);的代码及相关调用方法如下:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();// 获得线程上下文类加载器
        return ServiceLoader.load(service, cl);
    }
    public static <S> ServiceLoader<S> load(Class<S> service,
                                                ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
    
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
    
    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();
    }
    

    经过上述过程,用成员变量private final ClassLoader loader;引用传入的类加载器,用service接收Driver.class。同时,上述过程中实例化了一个LazyIterator对象,并用成员变量lookupIterator来引用。
    执行ServiceLoader的“hasNext()”方法时最终会调用lookupIterator迭代器的“hasNext()”方法(此处暂且省略调用过程),如下:

    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
                    configs = loader.getResources(fullName);//进行服务查找
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }
    public boolean hasNext() {
        if (acc == null) {
            return hasNextService();
        } else {
            PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                public Boolean run() { return hasNextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
    

    上述过程通过configs = loader.getResources(fullName)来查找实现Driver接口的类。

    同样,ServiceLoader的迭代器的“next()”方法最终会调用lookupIterator迭代器的“next()”方法,如下:

    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            c = Class.forName(cn, false, loader);//使用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);
            return p;
        } catch (Throwable x) {
            fail(service,
                 "Provider " + cn + " could not be instantiated",
                 x);
        }
        throw new Error();          // This cannot happen
    }
    
    public S next() {
        if (acc == null) {
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
    

    可以看到,next()会最终调用到nextService()方法,并在此方法中通过c = Class.forName(cn, false, loader);执行类加载。此处的loader也是由ServiceLoader中的loader传入的,即为前文提到的线程上下文类加载器。

    经历了上述ServiceLoader类中一系列操作之后(包括服务发现和类加载),位于mysql驱动包中的Driver类会被初始化。该类如下所示

    package com.mysql.cj.jdbc;
    
    import java.sql.DriverManager;
    import java.sql.SQLException;
    
    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!");
            }
        }
    }
    

    上述Driver类加载时,会执行静态代码块,即执行DriverManager.registerDriver(new Driver());方法向DriverManager中注册一个Driver实例。
    我们再回到DriverManager类中,看看registerDriver方法:

    public static synchronized void registerDriver(java.sql.Driver driver,
                DriverAction da)
            throws SQLException {
    
        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }
    
        println("registerDriver: " + driver);
    
    }
    

    会将该MySQL驱动添加到成员变量registeredDrivers中,该成员变量存放已注册的jdbc驱动列表,如下:

    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    

    如此一来,服务发现、类加载、驱动注册便到此结束。接下来,应用程序执行数据库连接操作时,会调用“getConnection”方法,遍历registeredDrivers,获取驱动,建立数据库连接。

    总结

    以上便是JDBC的SPI机制总结,最核心的地方在于,ServiceLoader中使用低级别的加载器发现Driver类,并进行类加载。这些工作是BootStrap类加载器所办不到的。由于DriverManager和ServiceLoader都位于Java核心类库中,使用BootStrap类加载器来加载,所以需要通过线程上下文类加载器向ServiceLoader对象中传入一个低级别的类加载器,如系统类加载器,从而来打破双亲委派机制。

  • 相关阅读:
    Suricata, to 10Gbps and beyond(X86架构)
    golang语法学习(一):变量,常量以及数据类型
    Linux相关问题-CentOS6.5 x64版本号下Tomcat无法自启动的解决的方法
    软考之路--你存在我深深的脑海里
    Apache虚拟主机-解惑篇
    Apache 性能调优-参考篇
    Jmeter脚本录制
    整理LVS架构压力测试工作
    关于jboss的线程问题+java.lang.outofmemoryError
    CUDA入门
  • 原文地址:https://www.cnblogs.com/buptleida/p/14162051.html
Copyright © 2011-2022 走看看