• JDK的SPI实现及使用


    一、背景介绍

      在看spring源码和dubbo源码的时候,发现两者都用采用了JDK中spi的技术,发现都有大作用,所以就来分析下JDK中的SPI的使用方式及源码实现。

    二、什么是SPI

      SPI的全称是 Service Provider Interface。 一种从特定路径下,将实现了某些特定接口的类加载到内存中的方式(为什么会如此说,请看后面分析)。提供了另外一种方式加载实现类,也降低了代码的耦合程度,提升了代码的可扩展性。

      实现SPI的地方主要有以下3处。主要的类和方法分别是:

    • JDK
      •   java.util.ServiceLoader#load
    • Spring
      •   org.springframework.core.io.support.SpringFactoriesLoader#loadFactories
    • Dubbo
      •   org.apache.dubbo.common.extension.ExtensionLoader#

    三、举例说明JDK SPI的使用方式

     1. 自定义实现类, 实现数据库驱动 Driver.class

    package com.fattyca1.driver;
    
    import java.sql.*;
    import java.util.Properties;
    import java.util.logging.Logger;
    
    /**
     * <br>自定义数据库操作</br>
     *
     * @author fattyca1
     */
    public abstract class CustomziedDriver implements Driver {
    
        public Connection connect(String url, Properties info) throws SQLException {
            return null;
        }
    
        public boolean acceptsURL(String url) throws SQLException {
            return false;
        }
    
        public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
            return new DriverPropertyInfo[0];
        }
    
        public int getMajorVersion() {
            return 0;
        }
    
        public int getMinorVersion() {
            return 0;
        }
    
        public boolean jdbcCompliant() {
            return false;
        }
    
        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return null;
        }
    
        // 自定义方法
        protected abstract void outDbName();
    }
    View Code
    /**
     * <br>自定义实现数据库驱动</br>
     *
     * @author fattyca1
     */
    public class Fattyca1Driver extends CustomziedDriver {
    
        @Override
        public void outDbName() {
            System.out.println("fattyca1 db driver init ... ");
        }
    }
    View Code

     2. 在resources下创建META-INF/services文件夹,并在目录中创建文件。文件名称为实现接口名称,文件中内容为接口实现类。

      

     3. 建立一个main程序来测试结果

    package com.lfc.demo.spi;
    
    import com.fattyca1.driver.Fattyca1Driver;
    
    import java.sql.Driver;
    import java.util.ServiceLoader;
    
    /**
     * <br>spi 测试客户端</br>
     *
     * @author fattyca1
     */
    public class SPIClient {
    
        public static void main(String[] args) {
    
            ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
    
            for (Driver driver : drivers) {
                if(driver instanceof Fattyca1Driver) {
                    ((Fattyca1Driver) driver).outDbName();
                }
            }
    
    
        }
    }
    View Code

     4. 实验结果。如下图:

      

    以上方式就是JDK中的SPI的实现方式,通过此方法,我们实现了自定义的Driver,可以使用自己定义的方式连接数据库。

    四、JDK中ServiceLoader的源码分析

      通过看上面例子, 大概了解到如果使用JDK中的SPI的实现方式,但是我们还不知道JDK是如何操作的。 接下来,我们就分析分析JDK的实现方式。

    1. 从构造函数入口,发现其构造方法是私有,无法被外部初始化,所以我们直接从提供的静态方法入手

      // ServiceLoader的构造函数
    private ServiceLoader(Class<S> svc, ClassLoader cl) { ... }

      

    2. 我们从ServiceLoader#load方法开始,一步步点进去,发现其最后调用的私有构造方法,构造中的核心方法是reload()。

        private ServiceLoader(Class<S> svc, ClassLoader cl) {
            ...
            reload(); // 主要实现方法
        }

    3. 查看reload()方法的具体实现,发现并无多余代码,主要是清除了cacheMap, 实例化了lookupIterator,。

        public void reload() {
            providers.clear(); // 清除缓存中的对象
            lookupIterator = new LazyIterator(service, loader); // 初始化迭代器,此迭代器被调用时,才会加载类。
        }

    4.分析LazyIterator. 从名字可以看出,这是懒加载的类(命名清晰的好处)。此代码实现起来比较简单。 通过给定的class名称, 读取资源文件, 然后加载文件中的实现类。通过反射,生成实现类,放入cacheMap中。

    private class LazyIterator
            implements Iterator<S>
        {
    
            Class<S> service;
            ClassLoader loader;
            Enumeration<URL> configs = null;
            Iterator<String> pending = null;
            String nextName = null;
    
            private LazyIterator(Class<S> service, ClassLoader loader) {
                this.service = service;
                this.loader = loader;
            }
    
            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;
            }
    
            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
            }
    
            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);
                }
            }
    
            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);
                }
            }
    
            public void remove() {
                throw new UnsupportedOperationException();
            }
    
        }
    View Code

    五、SPI使用场景

    1.   jdk中数据库驱动的加载
    2.       spring中各种组件的插拔
    3.       dubbo中自定义rpc协议,序列化方式,过滤器等 

    六、总结

      至此,我们分析完JDK源码中的SPI的实现(代码实现简单,没有很仔细),发现实现简单,功能d强大,大家是否学习到了呢?  我们在自己实现代码的时候,可以多考虑学习此方式,也可以给代码松耦合,提升自己的代码质量。spring和dubbo中的源码和JDK中的源码都十分相似,实现起来大同小异,大家有时间可以自己比较比较。  

  • 相关阅读:
    js get set访问器及日期扩展?
    js中加“var”和不加“var”的区别
    面试题
    ajax复习
    artTemplate使用
    Angular2组件开发—属性与事件(一)
    Angular2组件开发—为模板应用样式(三)
    Angular2组件开发—为模板应用样式(二)
    Angular2组件开发—为模板应用样式(一)
    Angular2组件开发—模板的逻辑控制(三)
  • 原文地址:https://www.cnblogs.com/lifacheng/p/12578298.html
Copyright © 2020-2023  润新知