spi中sdi和miso的定义有什么差别_SPI用法及使用场景

2baf2530d3318271e77e4e3a1bdefb81.png

一、SPI的概念

    SPI的全程为Service Provider Interface,翻译为中文就是服务提供者接口。是Java提供的一种可以被第三方来实现或者扩展的接口,通过Java制定的SPI实现规范,可以很灵活地来实现框架中某些功能的扩展或者某些接口实现的替换。说简单点,就是给定一个接口,在用到接口具体实现的地方,可以不修改源代码的情况下,灵活替换接口的实现。SPI接口一般由框架的设计者提供规范,然后由框架的扩展者来扩展使用。

二、SPI的使用场景

    SPI在一般的系统中几乎不会用到,通常在一些具备扩展性的框架中使用比较多,例如:Dubbo,Spring这种框架中,在用户需要实现自己的特定功能时,不能修改框架源码,同时要把功能集成进去,这个时候就可以通过SPI扩展点来进行扩展。

三、具体示例

3.1、Java规定的SPI实现方式

  1. 定义SPI接口,也就是一个普通的interface接口
  2. 定义SPI接口的具体实现类
  3. 在classpath下创建META-INF/services文件夹
  4. 在文件夹中创建一个不带后缀,而且以接口的全限定名为文件名的文本文件
  5. 在文件中指定接口的实现类的全限定名,如果有多个实现,每行一个
  6. 通过JDK中提供的ServiceLoader去加载具体实现,即可实现SPI功能

3.2、定义接口ServiceInitializer

/**
* Description: SPI接口
* @author wangbin33
* @date 2020/12/2 12:08
*/
public interface ServiceInitializer {
    void init();
}

3.3、定义接口实现类

DownLoadInitializer实现类:

/**
* @author wangbin33
* @date 2020/12/2 12:09
*/
public class DownLoadInitializer implements ServiceInitializer {
    @Override
    public void init() {
        System.out.println("下载功能初始化!");
    }
}

UploadInitializer实现类:

/**
* @author wangbin33
* @date 2020/12/2 12:09
*/
public class UploadInitializer implements ServiceInitializer {
    @Override
    public void init() {
        System.out.println("上传功能初始化!");
    }
}

3.4、创建META-INF/services文件夹

    在resources目录下创建META-INF/services文件夹,同时创建com.wb.spring.spi.ServiceInitializer文件

64111649f8374e37d0b224a0a4b794ff.png
SPI对应的文件

文件内容为:

com.wb.spring.spi.impl.DownLoadInitializer
com.wb.spring.spi.impl.UploadInitializer
3.5、测试类TestSpiMain
/**
* Description: 测试SPI接口
* @author wangbin33
* @date 2020/12/2 12:10
*/
public class TestSpiMain {
    public static void main(String[] args) {
        ServiceLoader initializers = ServiceLoader.load(ServiceInitializer.class);for (ServiceInitializer initializer : initializers) {
            initializer.init();
        }
    }
}

输出结果:

下载功能初始化!
上传功能初始化!
3.6、Java的SPI实现原理

打开jdk的SPI实现部分源码,如下:

public final class ServiceLoader<S> implements Iterable<S> {
    // 扫描目录前缀,这就是为什么一定需要创建这个文件夹,这里默认是写死的
    private static final String PREFIX = "META-INF/services/";

    // 将要被加载的类或者接口
    private final Class service;// 用来定位,加载或者实例化接口实现实现者的类加载器private final ClassLoader loader;// 访问控制上下文对象private final AccessControlContext acc;// 按照实例化的顺序缓存已经实例化的类private LinkedHashMap providers = new LinkedHashMap<>();// 懒加载迭代器private LazyIterator lookupIterator;/**
     * 清除老的接口实现类,重新加载新的服务接口实现类
     */public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }/**
     * 构造函数私有化,外部只能通过静态方法进行实例化
     */private ServiceLoader(Class svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;// 调用reload方法加载接口实现
        reload();
    }private class LazyIterator implements Iterator<S> {
        Class service;
        ClassLoader loader;
        Enumeration configs = null;
        Iterator pending = null;
        String nextName = null;private LazyIterator(Class service, ClassLoader loader) {this.service = service;this.loader = loader;
        }private boolean hasNextService() {if (nextName != null) {return true;
            }if (configs == null) {try {// 定位具体的文件名,供下面类加载器去加载文件的内容// fullName: META-INF/services/com.wb.spring.spi.ServiceInitializer
                    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;
                }// 解析出的SPI接口实现类的全限定名
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();return true;
        }// 加载具体的SPI接口实现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) {
            }try {// 实例化
                S p = service.cast(c.newInstance());// 保存到缓存中
                providers.put(cn, p);return p;
            } catch (Throwable x) {// 省略
            }// This cannot happenthrow new Error();
        }public boolean hasNext() {// 删除了AccessController相关代码return hasNextService();
        }public S next() {// 删除了AccessController相关代码return nextService();
        }public void remove() {throw new UnsupportedOperationException();
        }
    }/**
     * 通过Class类型创建ServiceLoader对象,可以自己指定类加载器
     */public static  ServiceLoader load(Class service,
                                            ClassLoader loader)
 
{return new ServiceLoader<>(service, loader);
    }/**
     * 通过Class类型创建ServiceLoader对象
     */public static  ServiceLoader load(Class service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
    }
}
四、图解ServiceLoader的执行过程
2263981fb9ea012799c2a9e61f5fe2ef.png
SPI执行流程
五、SPI的具体使用场景

    SpringMVC框架应该都比较熟悉,都知道它是对Servlet的封装,所以他可以运行在Servlet容器中,例如平时常用的Tomcat容器,那么Tomcat容器启动的时候是如何加载及初始化SpringMVC的相关组件呢?这里面就会用到SPI内容,可以在spring源码的spring-web模块中看到如下的一个SPI接口,javax.servlet.ServiceContainerInitializer:

ddf73b7fb4cef84329fadc88b6e66e8f.png
SpringMVC的SPI实现

    这个接口是在哪如何加载的呢?其实是在Tomcat容器启动的时候,会通过SPI机制去加载这个sevlet容器初始化器,不过Tomcat中并不是直接使用Java的原生SPI机制,而是自己封装的一套类似于Java的SPI机制,在org.apache.catalina.startup.ContextConfig类中,可以看到如下的一个名称为processServletContainerInitializers的方法:

/**
* Scan JARs for ServletContainerInitializer implementations.
*
* 扫描jar包中ServletContainerInitializer的实现类
*/
protected void processServletContainerInitializers() {
    List detectedScis;try {// 创建WebappServiceLoader对象
        WebappServiceLoader loader =new WebappServiceLoader(
                        context);// 通过SPI加载ServletContainerInitializer的实现
        detectedScis = loader.load(ServletContainerInitializer.class);
    } catch (IOException e) {
        log.error(sm.getString("contextConfig.servletContainerInitializerFail",
                context.getName()),
            e);
        ok = false;return;
    }// 加载到所有ServletContainerInitializer的实现类之后,会再去检查是否标注有@HandlerTypes注解,没有的话也会直接忽略掉,不会加载for (ServletContainerInitializer sci : detectedScis) {
        initializerClassMap.put(sci, new HashSet>());
        HandlesTypes ht;try {
            ht = sci.getClass().getAnnotation(HandlesTypes.class);
        } catch (Exception e) {// 删除日志打印代码continue;
        }if (ht == null) {continue;
        }
        Class>[] types = ht.value();if (types == null) {continue;
        }for (Class> type : types) {if (type.isAnnotation()) {
                handlesTypesAnnotations = true;
            } else {
                handlesTypesNonAnnotations = true;
            }
            Set scis =
                    typeInitializerMap.get(type);// 将解析到的标注有@HandlerTypes注解而且是ServletContainerInitializer的实现放入到一个set集合中,在初始化容器的时候会使用到该集合if (scis == null) {
                scis = new HashSet();
                typeInitializerMap.put(type, scis);
            }
            scis.add(sci);
        }
    }
}

    此处自定义的WebappServiceLoader其实和Java原生的ServiceLoader特别类似,如下:

public class WebappServiceLoader<T> {
    // jar文件所在目录的前缀
    private static final String LIB = "/WEB-INF/lib/";
    // SPI文件所在目录前缀
    private static final String SERVICES = "META-INF/services/";
    private static final Charset UTF8 = Charset.forName("UTF-8");

    // 其他非核心代码省略
    /**
     * Load the providers for a service type.
     */
    public List load(Class serviceType) throws IOException {

        // 通过SPI加载META-INF/services下的ServletContainerInitializer的实现.
        String configFile = SERVICES + serviceType.getName();

        LinkedHashSet applicationServicesFound = new LinkedHashSet();
        LinkedHashSet containerServicesFound = new LinkedHashSet();
        ClassLoader loader = servletContext.getClassLoader();// 加载jar包中的SPI接口实现类
        List orderedLibs =
                (List) servletContext.getAttribute(ServletContext.ORDERED_LIBS);if (orderedLibs != null) {// handle ordered libs directly, ...for (String lib : orderedLibs) {
                URL jarUrl = servletContext.getResource(LIB + lib);if (jarUrl == null) {// should not happen, just ignorecontinue;
                }
                String base = jarUrl.toExternalForm();
                URL url;if (base.endsWith("/")) {
                    url = new URL(base + configFile);
                } else {
                    url = UriUtil.buildJarUrl(base, configFile);
                }try {
                    parseConfigFile(applicationServicesFound, url);
                } catch (FileNotFoundException e) {// no provider file found, this is OK
                }
            }// and the parent ClassLoader for all others
            loader = context.getParentClassLoader();
        }// 加载SPI配置文件的内容
        Enumeration resources;if (loader == null) {
            resources = ClassLoader.getSystemResources(configFile);
        } else {
            resources = loader.getResources(configFile);
        }// 解析SPI配置文件中的配置类while (resources.hasMoreElements()) {
            parseConfigFile(containerServicesFound, resources.nextElement());
        }// Filter the discovered container SCIs if requiredif (containerSciFilterPattern != null) {
            Iterator iter = containerServicesFound.iterator();while (iter.hasNext()) {if (containerSciFilterPattern.matcher(iter.next()).find()) {
                    iter.remove();
                }
            }
        }
        containerServicesFound.addAll(applicationServicesFound);if (containerServicesFound.isEmpty()) {return Collections.emptyList();
        }// 加载和实例化具体的实现类return loadServices(serviceType, containerServicesFound);
    }// 根据解析到的class全限定名去实例化具体的实现类private List loadServices(Class serviceType, LinkedHashSet servicesFound)throws IOException {
        ClassLoader loader = servletContext.getClassLoader();
        List services = new ArrayList(servicesFound.size());for (String serviceClass : servicesFound) {try {// 反射加载实现类
                Class> clazz = Class.forName(serviceClass, true, loader);// 初始化实现类
                services.add(serviceType.cast(clazz.newInstance()));
            } catch (Exception e) {// 省略异常
            }
        }return Collections.unmodifiableList(services);
    }
}

    通过上述SPI机制,在Tomcat容器启动的时候,会加载并初始化javax.servlet.ServiceContainerInitializer接口的实现类类,在SPI配置文件中可以看到,这个具体的实现类为:SpringServletContainerInitializer。然后调用其onStartup方法,完成SpringMVC框架的初始化过程。SpringMVC的具体启动过程,后面文章中会介绍。

六、Java原生SPI的优点和缺点
6.1、优点
  1. 不需要改动源码,就可以实现功能扩展,低耦合;
  2. 对某一个扩展的实现对原来的代码完全没有侵入性;
  3. 复合开闭原则,需要做功能扩展时,不需要修改原有实现,只需要新增实现即可;
6.2、缺点

通过上面的实例,也可以感受到这种原生SPI的如下几个弊端:

  1. 从上面的示例中可以看到,如果一个接口有多个实现,就只能遍历所有的实现,并全部实例化;
  2. 在配置文件中只是简单的罗列出了所有的扩展实现,每个实现并没有具体的名称,会导致在程序中很难去准确的引用它们,比如在实际使用过程中,如何去准确调用某一个具体的实现;
  3. 扩展如果依赖其他的扩展,做不到自动注入和装配,因为实例化过程完全是通过反射创建的;
  4. 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring中的Bean,原生的Java SPI并不支持;
七、Java原生SPI弊端如何解决

    上述主要的弊端就是多个实现无法准确定位到某个调用,那么如果可以指定一个名称是不是就可以了呢?其实在Dubbo框架中就是这样实现的,对应的SPI配置文件类似于properties文件,一个名称对应一个实现,类似于如下的内容:

840f2bebb79581ae578b2c2b811d1e07.png
Dubbo的SPI配置内容

    在使用的时候可以通过名称指定具体使用哪个实现。在SpringBoot中同样有类似的实现,通过定义spring.factories文件,里面也是类似于key value格式的内容,一个接口对应多个实现,在加载扩展类的时候,可以将自定义的扩展类加载进去,但是这种方式也没法区分具体的实现类。所以Java中原生提供的SPI只是一种规范,在具体使用的时候,如果这种规范不符合自己的使用场景,完全可以自己去扩展这种规范。

关注菜鸟封神记,定期分享技术干货!

466f1694177d8556dbd69a0b26c77e4b.png

点赞和在看是最大的支持,感谢↓↓↓

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值