• 一篇文章搞明白Java中的SPI机制



    一、什么是SPI机制?

    SPI机制是Java的一种服务发现机制,为了方便应用扩展。那什么是服务发现机制?简单来说,就是你定义了一个接口,但是不提供实现,接口实现由其他系统应用实现。你只需要提供一种可以找到其他系统提供的接口实现类的能力或者说机制。这就是SPI机制( Service Provider Interface)

    SPI机制在Java中有很广泛的运用,比如:eclipse和idea里的插件使用就是通过SPI机制实现的。开发工具提供一个扩展接口,具体的实现由插件开发者实现,开发工具提供一种服务发现机制来找到具体插件的实现,这就达到了插件的安装效果。从而可以使用插件服务。如果不需要某一插件,只需要删除某一插件的实现类,开发工具找不到具体的插件实现,这就达到了插件的卸载效果。不管是安装还是卸载都不会影响其他代码,其他服务。非常方便的实现了可插拔的效果。

    JDBC中数据库连接驱动也使用了SPI机制,来达到适配不同DB数据库的效果。

    SPI机制除了在jdk里有运用,在springboot中也用到了。springboot自动装配中"查找spring.factories 文件步骤"就是基于SPI的部分设计思想实现的。


    二、JDK中SPI机制的实现原理

    2.1 线程上下文类加载器

    先来思考一个问题:上面说了SPI是一种服务发现机制,接口提供者需要提供一种能力来找到接口实现类。有这样一种场景:Java为我们定义了用于连接数据库的Driver接口。Mysql为我们提供了Driver接口的实现类mysqlDriver用于连接mysql数据库。

    根据类加载的双亲委派原理得知,jvm在加载java.sql.Driver类时,会优先给Bootstarp类加载器去加载。但是Bootstarp类加载器只会加载jdk下的jar包和类(虚拟机按名称识别,不在虚拟机识别文件列表中的jar包不会加载)。而mysql提供的具体驱动程序实现类则是外部jar包。

    上面这种情况下请问Java是怎么加载到mysql驱动从而连接mysql数据库的呢?

    先说答案:通过线程上下文类加载器实现。

    为了解决上面说的问题,Java设计团队引入了一个不怎么优雅的设计(破坏了虚拟机类加载时的双亲委派模型):为每个线程设置一个类加载器属性。该属性默认赋值Application(应用程序)类加载器。也可以通过下面这种方式设置自定义的类加载器

            //获取当前线程
            Thread thread = Thread.currentThread();
            //获取线程上下文类加载器
            ClassLoader classLoader = thread.getContextClassLoader();
            //设置线程上下文类加载器
            thread.setContextClassLoader(null);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为默认的是Application类加载器,所以使得虚拟机在加载java.sql.Driver类时,可以通过当前线程,获取Application类加载器,然后找到第三方jar包。这样上面的问题解决了

    上面这些,就是JDK中实现SPI机制的核心依赖点。

    2.2 ServiceLoader

    ServiceLoader是JDK提供的专门用于实现SPI机制的类。位于java.util.ServiceLoader

    ServiceLoader类的构造函数被私有化了。所以构建ServiceLoader对象只能通过ServiceLoader.load()方法。该方法有两个重载

    /**
    service:需要加载的Class
    loader:加载service用的类加载器
    **/
     public static <S> ServiceLoader<S> load(Class<S> service,
                                                ClassLoader loader)
        {
         //可传入自定义类加载器
            return new ServiceLoader<>(service, loader);
        }
    
     /**
    service:需要加载的Class
    **/
     public static <S> ServiceLoader<S> load(Class<S> service) {
     //不传入类加载器,默认为当前上下文类加载器
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return ServiceLoader.load(service, cl);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    使用ServiceLoader时可选择是否用自定义类加载器来加载目标类。也可默认使用应用程序类加载器加载。


    在这里插入图片描述
    jdk通过ServiceLoader类去ClassPath下的 “META-INF/services/”(此路径约定成俗) 路径里查找相应的接口实现类。ServiceLoader类核心功能就两个点,都在ServiceLoader的内部类LazyIterator中:

    • 查找相应接口对应实现类:hasNextService()
    • 加载相应接口实现类到虚拟机内:nextService()

    查找核心逻辑如下:

    private boolean hasNextService() {
                if (nextName != null) {
                    return true;
                }
                if (configs == null) {
                    try {
                    //前缀(META-INF/services/)+相应接口全限定名
                        String fullName = PREFIX + service.getName();
    //该loader是构造ServiceLoader类时设置。可传入自定义类加载器,如未传入,则默认应用程序类加载器
                        if (loader == null)
                        //如果当前线程上下文类加载器为空,按照默认的双亲委派机制去寻找实现类资源配置。
                            configs = ClassLoader.getSystemResources(fullName);
                        else
                        //在系统中查找资源,注意查找资源的加载器是从当前线程上下文中获取。也就是默认的应用程序类加载器。所以能加载到第三方jar包下的classpath路径。
                            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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    加载

     private S nextService() {
                if (!hasNextService())
                    throw new NoSuchElementException();
                //hasNextService(查找)方法里获取到的第三方实现类全限定名。
                String cn = nextName;
                nextName = null;
                Class<?> c = null;
                try {
                //通过loader加载第三方实现类
                    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);
                    return p;
                } catch (Throwable x) {
                    fail(service,
                         "Provider " + cn + " could not be instantiated",
                         x);
                }
                throw new Error();          // This cannot happen
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    2.3 小总结

    JDK中的SPI实现,是由ServiceLoader类根据自定义传入类加载器或者应用程序类加载器在约定好的固定路径下(ClassPath:META-INF/services/)去查找和加载第三方接口实现类。

    注意:要使用JDK中的SPI机制有几个前提条件

    • 服务提供方必须实现目标接口
    • 服务提供方必须在自身ClassPath:META-INF/services/路径下建立文件,文件名为目标接口全限定名。文件内容为实现目标接口的具体实现类全限定名

    三、从JDBC的角度分析SPI机制

    SPI机制在JDBC的使用主要是在获取数据库驱动的时候。依照SPI实现,我们来看一下JDBC是如何加载Mysql驱动程序的。

    首先看下第一个条件:jdk定义Driver接口。mysq驱动程序提供接口实现类。
    在这里插入图片描述
    mysql中的Driver类,确实是实现了java.sql.Driver接口

    第二个条件:接口实现类配置文件,必须放在ClassPath:META-INF/services/路径下
    在这里插入图片描述

    条件都满足ok,下面来具体看一下真正的加载实现逻辑。

    3.1 获取驱动程序实现类列表

    String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
    Connection conn = DriverManager.getConnection(url,username,password);
    
    • 1
    • 2

    逻辑体现在上面代码中的DriverManager驱动管理器里:java.sql.DriverManager
    在这里插入图片描述

    DriverManager驱动管理器核心功能点是static代码块下的loadInitialDrivers()方法调用。它会去注册通过jdbc.properties指定的数据库驱动程序通过ServiceLoader去加载可能存在的第三方数据库驱动程序。

     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;
            }
            // If the driver is packaged as a Service Provider, load it.
            // Get all the drivers through the classloader
            // exposed as a java.sql.Driver.class service.
            // ServiceLoader.load() replaces the sun.misc.Providers()
    
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
    //通过ServiceLoader.load()方法去加载第三方jar包下的数据库驱动程序实现类。使用的是默认的应用程序类加载器
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    //由于ServiceLoader类下的内部类LazyIterator实现了Iterator迭代器接口。所以可以遍历处理获取到的一个或多个第三方驱动程序实现类
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    
                    /* Load these drivers, so that they can be instantiated.
                     * It may be the case that the driver class may not be there
                     * i.e. there may be a packaged driver with the service class
                     * as implementation of java.sql.Driver but the actual class
                     * may be missing. In that case a java.util.ServiceConfigurationError
                     * will be thrown at runtime by the VM trying to locate
                     * and load the service.
                     *
                     * Adding a try catch block to catch those runtime errors
                     * if driver not available in classpath but it's
                     * packaged as service and that service is there in classpath.
                     */
                    try{
                    //hasNext会调用hasNextService方法,用于查找第三方数据库驱动实现类
                        while(driversIterator.hasNext()) {
                        //next会调用nextService方法,用于加载第三方数据库驱动实现类
                            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);
                    //将系统设置jdbc.properties获取到的数据库驱动程序加载进虚拟机中
                    Class.forName(aDriver, true,
                            ClassLoader.getSystemClassLoader());
                } catch (Exception ex) {
                    println("DriverManager.Initialize: load failed: " + ex);
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    3.2 如何正确选择Mysql驱动程序

    由于一个应用要可以连接mysql,也同时可以连接oracle。所以DriverManager获取到的驱动程序可能有多个。那使用的时候怎么才能选择期望的数据库驱动程序呢?

    先说答案:遍历所有驱动程序,根据数据库url一个一个尝试获取数据库连接,获取成功说明驱动程序是期望的。

    验证答案:得回到这段代码上来:DriverManager.getConnection()

    String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
    Connection conn = DriverManager.getConnection(url,username,password);
    
    • 1
    • 2

    DriverManager获取数据库连接方法核心调用:

     private static Connection getConnection(
            String url, java.util.Properties info, Class<?> caller) throws SQLException {
            
            ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
            synchronized(DriverManager.class) {
               
                if (callerCL == null) {
                    callerCL = Thread.currentThread().getContextClassLoader();
                }
            }
    
            if(url == null) {
                throw new SQLException("The url cannot be null", "08001");
            }
    
            println("DriverManager.getConnection(\"" + url + "\")");
    
           
            SQLException reason = null;
    //开始遍历所有注册的数据库驱动程序
            for(DriverInfo aDriver : registeredDrivers) {
               
                if(isDriverAllowed(aDriver.driver, callerCL)) {
                    try {
                        println("    trying " + aDriver.driver.getClass().getName());
                        Connection con = aDriver.driver.connect(url, info);
                        if (con != null) {
                            // Success! 获取成功,返回连接
                            println("getConnection returning " + aDriver.driver.getClass().getName());
                            return (con);
                        }
                    } catch (SQLException ex) {
                        if (reason == null) {
                            reason = ex;
                        }
                    }
    
                } else {
                    println("    skipping: " + aDriver.getClass().getName());
                }
    
            }
    //如果获取不到任何数据库连接,抛出sql异常
            // if we got here nobody could connect.
            if (reason != null)    {
                println("getConnection failed: " + reason);
                throw reason;
            }
    
            println("getConnection: no suitable driver found for "+ url);
            throw new SQLException("No suitable driver found for "+ url, "08001");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    以上就是JDK中SPI在JDBC的运用分析。

  • 相关阅读:
    Python基础教程:XML操作详解与XPath高级查询
    【C++】模板进阶 -- 详解
    SpringBoot、基础篇
    AI歌手是否会取代流行歌手成为主流?
    lv3 嵌入式开发-10 NFS服务器搭建及使用
    WaitTimeManagerDemo
    某大型制造企业数字化转型规划方案(附下载)
    React如何命令式调用自定义的Antd-Modal组件
    【4 进程与线程】
    【HHO-KELM预测】基于哈里斯鹰算法优化核极限学习机回归预测研究(Matlab代码实现)
  • 原文地址:https://blog.csdn.net/qq_42394044/article/details/127833884