浅谈SPI机制

浅谈SPI机制

前言

这段时间在研究一个开源框架,发现其中有一些以SPI命名的包,经过搜索、整理以及思考之后,将学习的笔记、心得整理出来,供日后复习使用。

SPI

SPI全称是Service Provider Interface,翻译过来是服务提供者接口,这个翻译其实不那么形象,理解起来也不是很好理解,至少不那么见名知意。

其实SPI是一种机制,一种类似于服务发现的机制,什么叫做服务发现呢,就是能够根据情况发现已有服务的机制,好像说了跟没说一样,对吧,下面我们逐个来理解。

首先是服务,英文叫做Service,服务可以理解为就是某一种或者某几种功能,比如日常生活中的医生,提供看病的服务;家政公司,提供家政服务;房产中介公司,提供,这样子的话,关于服务,应该是理清楚了。

接下来是服务的发现,英文是Service Discovery,理解了服务,那么服务的发现就应该很好理解了,用大白话讲就是具有某种能力,可以发现某些服务,比如生活中的房产中介公司(服务发现),他们就能够发现很多的拥有空闲房子并且愿意出租的人(服务)。

SPI机制的作用就是服务发现,也就是说,我们有一些服务,然后通过SPI机制,就能让这些服务被需要的人所使用,而我们这些服务被发现的过程就是SPI的任务了。

说到这里,可能你还是不太理解SPI是什么,接下来我们通过具体的例子分析来理解SPI。

在JDBC4.0之前,我们使用JDBC去连接数据库的时候,通常会经过如下的步骤

  1. 将对应数据库的驱动加到类路径中
  2. 通过Class.forName()注册所要使用的驱动,如Class.forName(com.mysql.jdbc.Driver)
  3. 使用驱动管理器DriverManager来获取连接
  4. 后面的内容我们不关心了。

这种方式有个缺点,加载驱动是由用户来操作的,这样就很容易出现加载错驱动或者更换驱动的时候,忘记更改加载的类了。

在JDBC4.0,现在我们使用的时候,上面的第二步就不需要了,并且能够正常使用,这个就是SPI的功劳了。

接下来我们先来看下为什么不需要第二步。

熟悉反射的同学应该知道,第二步其实就是将对应的驱动类加载到虚拟机中,也就是说,现在我们没有手动加载,那么对应的驱动类是如何加载到虚拟机中的呢,我们通过DriverManger的源码的了解SPI是如何实现这个功能的。

DriverManager.java

在DriverManager中,有一段静态代码(静态代码在类被加载的时候就会执行)

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;
    }

    // SPI机制加载驱动类
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            
            //  通过ServiceLoader.load进行查找,我们的重点也是这里,后面分析
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 获取迭代器,也请注意这里
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            try{
                // 遍历迭代器
                // 这里需要这么做,是因为ServiceLoader默认是延迟加载
                // 只是找到对应的class,但是不加载
                // 所以这里在调用next的时候,其实就是实例化了对应的对象了
                // 请注意这里 --------------------------------------------------------------------  1
                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)方法,该方法其实就是SPI的核心啦

接下来我们来分析这个类的代码(代码可能有点长哦,要有心理准备)

ServiceLoader.java


public final class ServiceLoader<S>
    implements Iterable<S>
{
    /**
    *  由于是调用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)
    {
        // 只是返回一哥ServiceLoader对象,调用自己的构造函数嘛
        return new ServiceLoader<>(service, loader);
    }

    /**
    *  私有构造函数
    */
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        // 目标加载类不能为null
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        // 获取类加载器,如果cl是null,则使用系统类加载器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        // 调用reload方法
        reload();
    }

    // 用于缓存加载的服务提供者
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 真正查找逻辑的实现
    private LazyIterator lookupIterator;

    /**
    *  reload方法
    */
    public void reload() {
        // 先清空内容
        providers.clear();
        // 初始化lookupIterator
        lookupIterator = new LazyIterator(service, loader);
    }
}

LazyIterator.class

LazyIterator是ServiceLoader的私有内部类

private class LazyIterator
        implements Iterator<S>
{

    Class<S> service;
    ClassLoader loader;

    /**
    *  私有构造函数,用于初始化参数
    */
    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }
}

到了上面的内容,其实ServiceLoader.load()方法就结束了,并没有实际上去查找具体的实现类,那么什么时候才去查找以及加载呢,还记得上面的Iterator<Driver> driversIterator = loadedDrivers.iterator();这一行代码吗,这一行代码用于获取一个迭代器,这里同样也没有进行加载,但是,其后面还有遍历迭代器的代码,上面标注为1的部分。

迭代器以及遍历迭代器的过程如下所示

ServiceLoader.java

public Iterator<S> iterator() {
    return new Iterator<S>() {

        // 注意这里的providers,这里就是上面提到的用于缓存
        // 已经加载的服务提供者的容器。
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        // 底层其实委托给了providers
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            // 如果没有缓存,则查找及加载
            return lookupIterator.hasNext();
        }

        // 同上
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

上面已经分析过了,ServiceLoader.load()方法执行到LazyIterator的初始化之后就结束了,真正地查找直到调用lookupIterator.hasNext()才开始。

LazyIterator.java

// 希望你还记得他
private class LazyIterator
        implements Iterator<S>
{

    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    //检查 AccessControlContext,这个我们不关系
    // 关键的核心是都调用了hasNextService()方法
    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);
        }
    }

    private boolean hasNextService() {
        // 第一次加载
        if (nextName != null) {
            return true;
        }
        // 第一次加载
        if (configs == null) {
            try {
                // 注意这里,获取了的完整名称
                // PREFIX定义在ServiceLoader中
                // private static final String PREFIX = "META-INF/services/"
                // 这里可以看到,完整的类名称就是 META-INF/services/CLASS_FULL_NAME
                // 比如这里的 Driver.class,完整的路径就是
                //                  META-INF/services/java.sql.Driver,注意这个只是文件名,不是具体的类哈
                String fullName = PREFIX + service.getName();
                // 如果类加载器为null,则使用系统类加载器进行加载
                // 类加载会加载指定路径下的所有类
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else // 使用传入的类加载器进行加载,其实就是应用类加载器
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        // 如果pending为null或者没有内容,则进行加载,一次只加载一个文件的一行
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            // 解析读取到的每个文件,高潮来了
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }

    /**
    *  解析读取到的每个文件
    */
    private Iterator<String> parse(Class<?> service, URL u)
        throws ServiceConfigurationError
    {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            // utf-8编码
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            // 一行一行地读取数据
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        // 返回迭代器
        return names.iterator();
    }

    // 解析一行行的数据
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }
        // 查找是否存在#
        // 如果存在,则剪取#前面的内容
        // 目的是防止读取到#及后面的内容
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            // 不能包含空格及制表符\t
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            // 检查第一个字符是否是Java语法规范的单词
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            // 检查每个字符
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            // 如果缓存中没有,并且当前列表中也没有,则加入列表。
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

    /**
    *  上面解析完文件之后,就开始加载文件的内容了
    */
    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
    }
}

到此,解析的步骤就完成了,在一开始的DriverManager中,我们也看到了在DriveirManager中一直在调用next方法,也就是持续地加载找到的所有的Driver的实现类了,比如MySQL的驱动类,Oracle的驱动类啦。

这个例子有点长,但我们收获还是很多,我们知道了JDBC4不用手动加载驱动类的实现原理,其实就是通过ServiceLoader去查找当前类加载器能访问到的目录下的WEB-INF/services/FULL_CLASS_NAME文件中的所有内容,而这些内容由一定的规范,如下

  • 每行只能写一个全类名
  • #作为注释
  • 只能使用utf-8及其兼容的编码
  • 每个实现类必须提供一个无参构造函数,因为是直接使用class.newInstance()来创建实例的嘛

由此我们也明白了SPI机制的工作原理,那么这个东西有什么用呢,其实JDBC就是个最好的例子啦,这样用户就不需要知道到底是要加载哪个实现类,一方面是简化了操作,另一方面避免了操作的错误,当然,这种一般是用于写框架之类的用途,用于向框架使用者提供更加便利的操作,比如上面的引导我看到SPI的例子,其实是来自一个RPC框架,通过SPI机制,让我们可以直接编写自定义的序列化方式,然后由框架来负责加载即可。

SPI实战小案例

上面学习完了SPI的例子,也学习完了JDBC是如何实现的,接下来我们来通过一个小案例,来动手实践一下SPI是如何工作的。

新建一个接口,内容随便啦

HelloServie.java

public interface HelloService {
    void sayHello();
}

然后编写其实现类

HelloServiceImpl.java

public class HelloServiceImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("hello world");
    }
}

关键点来了,既然是学习SPI,那么我们肯定不是手动new一个实现类啦,而是通过SPI的机制来加载,如果认真地看完上面的分析,那么下面的内容应该很容易看懂啦,如果没看懂,再回去看一下啦。

  1. 在实现类所在项目(这里是同个项目哈)的类路径下,如果是maven项目,则是在resources目录下

    1. 建立目录META-INF/services
    2. 建立文件cn.xuhuanfeng.spi.HelloService(接口的全限定名哈)
  2. 内容是实现类的类名:cn.xuhuanfeng.spi.impl.HelloServiceImpl(注意这里我们直接放在同个项目,不是同个项目也可以的!!!)

  3. 自定义一个加载的类,并且通过ServiceLoader.load()方法进行加载,如下所示

    public class HelloServiceFactory {
    
        public HelloService getHelloService() {
            ServiceLoader<HelloService> load = ServiceLoader.load(HelloService.class);
            return load.iterator().next();
        }
    }
    
  4. 测试一下,enjoy :)

  5. 如果你有兴趣的话,可以尝试将实现放在另一个项目中,然后打包成jar包,再放置在测试项目的classpath中,enjoy :)

总结

本小节我们主要学习了SPI,主要包括了SPI是什么,JDBC4中不需要手动加载驱动类的原理,并且详细看了DriverManager中的代码实现,最后,通过一个简单的小案例来实现我们自己的SPI服务,通过这个小节,应该说,SPI的大部分内容我们是掌握了,当然,里面管理类加载器部分我们还没有学习,这里先挖个坑,后面有时间再分析一下。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,368评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,941评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,369评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,848评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,719评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,505评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,904评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,528评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,819评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,848评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,652评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,468评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,912评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,095评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,389评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,906评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,120评论 2 339

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,523评论 18 139
  • 1.介绍 熟悉JDBC的同学都知道,在jdbc4.0之前,在使用DriverManager获取DB连接之前,我们总...
    近路阅读 6,223评论 2 3
  • SPI简介 如何使用SPI 应用举例1. 组织方制定接口2. 实现方根据SPI规范实现接口3. 组织方加载实现类 ...
    齐晋阅读 892评论 0 5
  • 本文以JDBC为例深入讲解 java spi 机制,将帮助你理解:什么是SPI,SPI实现原理,SPI的使用和SP...
    匠丶阅读 5,215评论 0 8
  • 此刻,一杯十年陈的即墨老酒,一盘香痱子,一行标题,根据要求,我得写一写2018的新目标. 在酒的作用下,内心狂野;...
    113bd6fe137e阅读 260评论 4 1