zoukankan      html  css  js  c++  java
  • JDK SPI

    最近学习了JDK SPI

     

    JDK SPI是什么

    最近工作中听几个同事说了好几次SPI这个名词,虽然和我没关系,但是心里默默想还是学习一下,不然下次和我说到SPI,连是什么都不知道那就尴尬了。

    所以SPI是什么呢?SPI全称Service Provider Interface,在Java中还是一个比较重要的概念,是Java提供的一套用来被第三方实现或者扩展的API,或者换句话说,SPI是一种服务发现机制

    JDK SPI使用说明及示例

    要使用SPI比较简单,只需要按照以下几个步骤操作即可:

    • 在jar包的META-INF/services目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名
    • 接口实现类所在的jar包在classpath下
    • 主程序通过java.util.ServiceLoader动态状态实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM
    • SPI的实现类必须带一个无参构造方法

    接着我们看一下具体例子,首先定义一个SpiService,它是一个接口:

    复制代码
    package org.xrq.test.spi;
    
    public interface SpiService {
    
        public void hello();
        
    }
    复制代码

    两个实现类,分别为SpiServiceA与SpiServiceB:

    复制代码
    package org.xrq.test.spi;
    
    public class SpiServiceA implements SpiService {
    
        public void hello() {
            System.out.println("SpiServiceA.Hello");
        }
        
    }
    复制代码
    复制代码
    package org.xrq.test.spi;
    
    public class SpiServiceB implements SpiService {
    
        @Override
        public void hello() {
            System.out.println("SpiServiceB.hello");
        }
        
    }
    复制代码

    接着我们建一个META-INF/services的文件夹,里面建一个file,file的名字是接口的全限定名org.xrq.test.spi.SpiService:

    文件的内容是SpiService实现类SpiServiceA、SpiServiceB的全限定名:

    org.xrq.test.spi.SpiServiceA
    org.xrq.test.spi.SpiServiceB

    这样就大功告成了!然后我们写个测试类自动加载一下这两个类:

    复制代码
    public class SpiTest {
    
        @Test
        public void testSpi() {
            ServiceLoader<SpiService> serviceLoader = ServiceLoader.load(SpiService.class);
            
            Iterator<SpiService> iterator = serviceLoader.iterator();
            while (iterator.hasNext()) {
                SpiService spiService = iterator.next();
                
                spiService.hello();
            }
        }
        
    }
    复制代码

    结果一目了然,调用了hello()方法:

    SpiServiceA.Hello
    SpiServiceB.hello

    这就是SPI的使用示例,接着我们看一下SPI在实际场景中的应用。

    SPI在JDBC中的应用

    回看快四年前的文章https://www.cnblogs.com/xrq730/p/4851944.html,这篇名为《JDBC学习2:为什么要写Class.forName("XXX")?》的文章里面当时技术真的是稚嫩,为什么不写Class.forName("XXX")的解释现在看来真的是弱爆了,最后一楼网友的回复"不用写的原因是,新版本JDBC使用了SPI",所以学了一下SPI马上就想起这个例子来了,因此就由JDBC讲讲SPI的实际应用。

    在老版本的JDBC中,假设我们使用的是MySql,初始化JDBC的时候是需要显式调用Class.forName("com.mysql.jdbc.Driver")这一句的,但是在某个版本之后就不需要做这一步操作了,如上所说这是通过SPI实现的,怎么理解呢。Class.forName其实没有实际意义,其实既不会new对象也不会反射生成对象,它只是为了调用com.mysql.jdbc.Driver的static方法块而已:

    复制代码
    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!");
            }
        }
    
        /**
         * Construct a new driver and register it with DriverManager
         * 
         * @throws SQLException
         *             if a database error occurs.
         */
        public Driver() throws SQLException {
            // Required for Class.forName().newInstance()
        }
    }
    复制代码

    方法块的作用只有一个,通过jdk自带的DriverManager注册Driver,registerDrivers方法没什么套路,把Driver放到CopyOnArrayList里面而已:

    复制代码
    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);
    
    }
    复制代码

    从某个JDK版本,具体也不知道哪个版本,废弃了这个操作,看下新版的DriverManager,我的是JDK1.8的:

    复制代码
    /**
     * 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这个方法的核心部分:

    复制代码
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
    
            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;
        }
    });
    复制代码

    看到使用SPI的方式从META-INF/services下去找java.sql.Driver这个文件,并找到里面的Driver实现类逐一注入。最后我们看一下Iterator的next()方法做了什么就完全懂了,通过next()方法调用了:

    复制代码
    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            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
    }
                
    复制代码

    看到Class.forName了吧,虽然都是Class.forName,但是通过SPI的方式把用户手动做的动作变成框架做。

    对SPI的理解

    最后谈一谈我对SPI的理解,学习了怎么用SPI、SPI在实际应用中的示例之后,深刻理解SPI机制才能在以后工作中真正将SPI为我所用。

    首先大家可以注意到,标题是JDK SPI,也就是说SPI并不是JDK专属的。是的,我理解的SPI其实是一种可插拔技术的总称,最简单的例子就是USB,厂商提供了USB的标准,厂家根据USB的标准制造自己的外设,例如鼠标、键盘、游戏手柄等等,但是USB标准具体在电脑中是怎么用的,厂家就不需要管了。

    回到我们的代码中也是一样的道理。当我们开发一个框架的时候,除了保证基本的功能外,最重要的一个点是什么?我认为最重要的应该是松耦合,即扩展开放、对修改关闭,保证框架实现对于使用者来说是黑盒。

    框架不可能做好所有的事情,只能把共性的部分抽离出来进行流程化,松耦合实现的核心就是定义好足够松散的接口,或者可以理解是扩展点,具体的扩展点让使用者去实现,这样不同的扩展就不需要修改源代码或者对框架进行定制,这就是面向接口编程的好处。

    回到我们框架的部分来说:

    • JDK对于SPI的实现是通过META-INF/services这个目录 + ServiceLoader
    • Spring实现SPI的方式是留了N多的接口,例如BeanPostProcessor、InitializingBean、DisposableBean,我们只需要实现这些接口然后注入即可

    对已有框架而言,我们可以通过框架给我们提供的扩展点扩展框架功能。对自己写框架而言,记得SPI这个事情,留好足够的扩展点,这将大大加强你写的框架的扩展性。

    ================================================================================== 

    我不能保证写的每个地方都是对的,但是至少能保证不复制、不黏贴,保证每一句话、每一行代码都经过了认真的推敲、仔细的斟酌。每一篇文章的背后,希望都能看到自己对于技术、对于生活的态度。

    我相信乔布斯说的,只有那些疯狂到认为自己可以改变世界的人才能真正地改变世界。面对压力,我可以挑灯夜战、不眠不休;面对困难,我愿意迎难而上、永不退缩。

    其实我想说的是,我只是一个程序员,这就是我现在纯粹人生的全部。
     
    https://www.cnblogs.com/xrq730/p/11440174.html
  • 相关阅读:
    LeetCode 226. Invert Binary Tree
    LeetCode 221. Maximal Square
    LeetCode 217. Contains Duplicate
    LeetCode 206. Reverse Linked List
    LeetCode 213. House Robber II
    LeetCode 198. House Robber
    LeetCode 188. Best Time to Buy and Sell Stock IV (stock problem)
    LeetCode 171. Excel Sheet Column Number
    LeetCode 169. Majority Element
    运维工程师常见面试题
  • 原文地址:https://www.cnblogs.com/think90/p/11443161.html
Copyright © 2011-2022 走看看