ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

什么是SPI

2022-04-14 14:31:24  阅读:255  来源: 互联网

标签:service 实现 什么 接口 SPI ServiceLoader 加载


什么是SPI

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。

  • API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
  • SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。

整体机制图如下:

 

 Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。

使用场景

概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略

比较常见的例子:

  • 数据库驱动加载接口实现类的加载,JDBC加载不同类型数据库的驱动
  • 日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
  • Spring,Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo,Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

使用介绍

要使用Java SPI,需要遵循如下约定:

  • 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
  • 接口实现类所在的jar包放在主程序的classpath中;
  • 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
  • SPI的实现类必须携带一个不带参数的构造方法;

示例代码

step1. 定义接口

package org.ray.spi;

public interface Human {

    public void speak();
}

step2. 定义实现类

package org.ray.spi;

public class Chinese implements Human{

    @Override
    public void speak() {
        System.out.println("哈喽 我的");
    }
}
package org.ray.spi;

public class English implements Human{

    @Override
    public void speak() {
        System.out.println("hello world");
    }
}

step3. 定义配置文件

在classpath(src/main/resources)下创建META-INF/resources目录,创建以接口名字org.ray.spi.Human命名的文件,内容写入接口实现类的全限定类名,如果有多个需换行

org.ray.spi.English
org.ray.spi.Chinese

step4. 执行ServiceLoader

    public static void main(String[] args) throws IOException {

        ServiceLoader<Human> load = ServiceLoader.load(Human.class);
        for (Human human : load) {
            human.speak();
        }
    }

step5. 查看输出

hello world
哈喽 我的

源码分析

ServiceLoader在这里没有核心操作,主要负责对外提供load()方法用于获取SPI接口和实例化懒加载迭代器LazyIterator

public final class ServiceLoader<S> implements Iterable<S>{
    
    #SPI规则固定加载文件地址前缀
    private static final String PREFIX = "META-INF/services/";
       #SPI的接口
    private final Class<S> service;
    #类加载器,使用的是当前线程的类加载器(Thread.currentThread().getContextClassLoader())
    private final ClassLoader loader;
    #默认是null, 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;
    #缓存加载成功的类
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    #当前的迭代器,默认初始化为LazyIterator,注意这里是懒加载的,只有使用的时候才去迭代加载SPI文件
    private LazyIterator lookupIterator;
    
    #SPI执行使用方法,不指定ClassLoader
       public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    #SPI执行使用方法,指定ClassLoader
    public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
        return new ServiceLoader<>(service, loader);
    }
    #构造方法中保存SPI接口,初始化懒加载迭代器
    private ServiceLoader(Class<S> 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();
    }
    #初始化懒加载迭代器
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
}

LazyIterator是懒加载,实例化后什么也不干,只保存了SPI接口

    private class LazyIterator implements Iterator<S> {
        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }
    }

ServiceLoader的iterator()方法被调用,开始执行核心逻辑LazyIterator懒加载SPI文件

  • hasNextService():用于加载META-INF/services/下SPI文件
  • nextService():用于根据SPI文件中指定的实现类的全限定类名通过反射实例化对象,放入缓存
    private class LazyIterator implements Iterator<S> {
        
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    #拼装SPI文件完整地址META-INF/services+SPI全限定类名
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        #加载SPI文件
                        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;
        }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                #根据全限定类名获取Class描述文件
                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 {
                #根据Class文件使用反射创建对象
                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
        }
    }

总结

优点:
使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

相比使用提供接口jar包,供第三方服务模块实现接口的方式,SPI的方式使得源框架不必关心接口的实现类的路径,可以不用通过下面的方式获取接口实现类:

代码硬编码import 导入实现类

指定类全路径反射获取:例如在JDBC4.0之前,JDBC中获取数据库驱动类需要通过Class.forName(“com.mysql.jdbc.Driver”),类似语句先动态加载数据库相关的驱动,然后再进行获取连接等的操作

第三方服务模块把接口实现类实例注册到指定地方,源框架从该处访问实例

通过SPI的方式,第三方服务模块实现接口后,在第三方的项目代码的META-INF/services目录下的配置文件指定实现类的全路径名,源码框架即可找到实现类

缺点:

虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。

多个并发多线程使用ServiceLoader类的实例是不安全的。

转载:https://blog.csdn.net/dcr782195101/article/details/122004685

 

 

TRANSLATE with x English
Arabic Hebrew Polish
Bulgarian Hindi Portuguese
Catalan Hmong Daw Romanian
Chinese Simplified Hungarian Russian
Chinese Traditional Indonesian Slovak
Czech Italian Slovenian
Danish Japanese Spanish
Dutch Klingon Swedish
English Korean Thai
Estonian Latvian Turkish
Finnish Lithuanian Ukrainian
French Malay Urdu
German Maltese Vietnamese
Greek Norwegian Welsh
Haitian Creole Persian  
  TRANSLATE with COPY THE URL BELOW Back EMBED THE SNIPPET BELOW IN YOUR SITE Enable collaborative features and customize widget: Bing Webmaster Portal Back

标签:service,实现,什么,接口,SPI,ServiceLoader,加载
来源: https://www.cnblogs.com/cainiao-Shun666/p/16144368.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有