浅谈 java spi 如何帮助框架插件化

spi 简介

spi 的全称是Service Provider Interface,主要作用是在让服务具备运行时加载接口的指定实现类的能力,java从 1.6 开始提供此机制(其实 1.3 开始就有了,只不过一直自嗨内部使用,没暴露外部方法给大家用而已),而各种框架有时也自己实现此机制以增强一些特有的功能(e.g:dubbo自己实现的 spi,spring-boot 类似的有spring factories)。
spi通常应用在框架中,辅助框架实现功能的插件化,让用户自己按照约定写的功能也能被框架加载运行。下面就举个 java spi 的例子,从例子中感受spi 的用法以及如何帮助框架实现插件化

java spi demo

ps:以下 demo 可以自行下载

git clone git@github.com:likemoongg/blog-code-demo.git

为了说明spi 的具体用途,在这里举个例子:为了实现通用的字典查询功能可以开发一种框架,可实现对各种不同字典的查询,字典可以由用户或第三方包定制。

框架侧代码

字典类的接口Dictionary

package dictionary.spi;

public interface Dictionary {
    public String getDefinition(String word);
}

查询字典的服务DictionaryService

package dictionary;

import dictionary.spi.Dictionary;
import java.util.Iterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;

public class DictionaryService {

    private static DictionaryService service;
    private ServiceLoader<Dictionary> loader;

    private DictionaryService() {
        // 这里的 ServiceLoader 就是 java 原生 spi 的重要入口类
        // 其入参是一个接口,返回的 ServiceLoader<Dictionary> loader 可以加载出其实现类的实例
        loader = ServiceLoader.load(Dictionary.class);
    }
    
    // 实现单例
    public static synchronized DictionaryService getInstance() {
        if (service == null) {
            service = new DictionaryService();
        }
        return service;
    }
    

    // 扫描 目录META-INF/services/dictionary.spi.Dictionary下描述的所有的 Dictionary 的实现类,依次查找入参的 word
    // 其中目录层级META-INF/services是 java spi 默认约定的目录
    public String getDefinition(String word) {
        String definition = null;

        try {
            // 利用 ServiceLoader<Dictionary> loader 的迭代器遍历每一个实现类,寻找可以识别 word的字典
            Iterator<Dictionary> dictionaries = loader.iterator();
            while (definition == null && dictionaries.hasNext()) {
                Dictionary d = dictionaries.next();
                definition = d.getDefinition(word);
            }
        } catch (ServiceConfigurationError serviceError) {
            definition = null;
            serviceError.printStackTrace();
        }
        return definition;
    }
}

以上两个类就是框架提供的所有类了,我们可以看到框架还没有提供注释里所说的 META-INF/services/dictionary.spi.Dictionary 文件啊!用户运行起来肯定有问题呀!!
这个目录就是 spi 机制的关键也是我们能实现插件化的重要一步。从上面可以看到框架并没有和具体的某个实现类捆绑起来,而是通过 ServiceLoader 去寻找具体的实现类,接口对应哪些实现类就是 META-INF/services/dictionary.spi.Dictionary 文件所描述的关键信息啦。所以用户可以自己编写该文件,就相当于指定了 Dictionary 的实现类具体有哪些,也就实现了插件化!

用户侧的程序

使用框架的用户自己编写的具体Dictionary实现类有两个,一个汉语词典EnDictionary,一个汉语词典CnDictionary。
那么 META-INF/services/dictionary.spi.Dictionary 可以这么写(其实就是类的全限定名称啦~)

net.likemoon.dictionary.EnDictionary
net.likemoon.dictionary.CnDictionary

两个具体实现类如下

package net.likemoon.dictionary;

public class EnDictionary implements dictionary.spi.Dictionary{

    @Override
    public String getDefinition(String word){
        if (word.matches("[a-zA-Z]+")) {
            return "looking up English dictionary...";
        }
        return null;
    }
}
public class CnDictionary implements Dictionary {

    @Override
    public String getDefinition(String word){
        if (!word.matches("[a-zA-Z]+")) {
            return "正在查阅中文字典...";
        }
        return null;
    }
}

最后是完整的用户使用框架的的例子

package net.likemoon;

import dictionary.DictionaryService;

public class lookupDictionary {
    public static void main(String[] args) {
        DictionaryService dictionaryService = DictionaryService.getInstance();
        System.out.println(dictionaryService.getDefinition("english"));
        System.out.println(dictionaryService.getDefinition("中文"));
    }
}

输出

looking up English dictionary...
正在查阅中文字典...

总结

上面的demo 中,框架始终只面向 Dictionary 接口编写功能。具体的字典实现类由java spi 获取。而用户可以在不侵入框架代码的情况下,通过编写约定的描述文件,让框架加载用户自己编写的实现类,扩展功能实现插件化。
当然除了用户(业务程序员)自己编写以外,引入第三方编写的插件 jar 包也可以实现相同的扩展效果。此时第三方的 jar包需包含:扩展的实现类+服务描述文件

可见 spi 的核心机制是接口到具体实现类的关联由一个文件描述
总结 spi 的服务描述文件的要素。
1、文件通常要放在某个约定的目录(上面 demo 中使用的java spi 规定的就是META-INF/services)
2、文件名和文件内容要体现接口和实现类的关联(java spi 中该文件名需为接口名,文件内容为实现类全限定名)
3、文件可以编写多份,都能被相关加载类加载(这样框架可以有内置的实现类,同时用户和第三方扩展包可以加入自己写的实现类扩展功能)

具体的 spi 实现过程中还有很多细节,比如要大量使用Map 做缓存以加快非首次访问的速度,比如接口的实现类都需要提供无参数构造器方便进行实例化等等。
后续会具体讲解一下 dubbo 的 spi 的实现过程和诸多细节。

java 的 spi
dubbo的spi介绍

posted @ 2020-06-08 12:28  李将军的刀  阅读(373)  评论(0)    收藏  举报