SPI(Service Provider Interface)机制

简介: JAVA SPI 约定如下:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/ 目录中同时创建一个以服务接口命名的文件,该文件中的内容就是实现该服务接口的具体实现类。

JAVA SPI

约定如下:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/ 目录中同时创建一个以服务接口命名的文件,该文件中的内容就是实现该服务接口的具体实现类。

Java中提供了一个用于服务实现查找的工具类:java.util.ServiceLoader。

//将服务声明的文件名称定义为: example.spi.service.IService,与接口名称一致,其中的内容包括:
//example.spi.service.PrintServiceImpl  
//example.spi.service.EchoServiceImpl  

public static void main(String[] args) {  
    //实例化具体类时需要注意对应类有无参构造函数
    ServiceLoader<Service> serviceLoader = ServiceLoader.load(IService.class);  
   
    for (IService service : serviceLoader) {  
        service.printInfo();  
    }  
}  

ServiceLoader的源码分析

重要属性:

// 加载的接口
private Class<S> service;

// 用于缓存已经加载的接口实现类,其中key为实现类的完整类名
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 用于延迟加载接口的实现类
private LazyIterator lookupIterator;

第一步:获取一个ServiceLoader<Service> serviceLoader = ServiceLoader.load(IService.class);实例,此时还没有进行任何接口实现类的加载操作,属于延迟加载类型的。只是创建了LazyIterator lookupIterator对象而已。

第二步:ServiceLoader实现了Iterable接口,即实现了该接口的iterator()方法,实现内容如下:

    // for循环遍历ServiceLoader的过程其实就是调用上述hasNext()和next()方法的过程
    public Iterator<S> iterator() {
        return new Iterator<S>() {

            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                // 第一次循环遍历会使用lookupIterator去查找,之后就缓存到providers中。
          // LazyIterator会去加载类路径下/META-INF/services/接口全称 文件的url地址,文件加载并解析完成之后,得到一系列的接口实现类的完整类名。
          // 调用next()方法时才回去真正执行接口实现类的加载操作,并根据无参构造器创建出一个实例,存到providers中。
return lookupIterator.hasNext(); } public S next() { //再次遍历ServiceLoader,就直接遍历providers中的数据 if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } }; }

ServiceLoader缺点

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

Dubbo SPI

 dubbo的扩展机制与JAVA SPI比较类似,但额外增加了其他功能:

  1. 可以根据接口名称来获取服务,dubbo spi 可以通过getExtension(String key)的方法方便的获取某一个想要的扩展实现,java的SPI机制需要加载全部的实现类。
  2. 服务声明文件支持A=B的方式,此时A为名称B为实现类。 文件名:com.alibaba.dubbo.rpc.Filter
    • echo=com.alibaba.dubbo.rpc.filter.EchoFilter

    • generic=com.alibaba.dubbo.rpc.filter.GenericFilter

    • genericimpl=com.alibaba.dubbo.rpc.filter.GenericImplFilter

  3. 支持扩展IOC依赖注入功能,可以为Service之间的依赖关系注入相关的服务并保证单例。
    • 举例来说:接口A,实现者A1、A2。接口B,实现者B1、B2。

      现在实现者A1含有setB()方法,会自动注入一个接口B的实现者,此时注入B1还是B2呢?

      都不是,而是注入一个动态生成的接口B的实现者B$Adpative,该实现能够根据参数的不同,自动引用B1或者B2来完成相应的功能。

      Protocol$Adpative是根据URL参数中protocol属性的值来选择具体的实现类的。

      如值为dubbo,则从ExtensionLoader<Protocol>中获取dubbo对应的实例,即DubboProtocol实例

      如值为hessian,则从ExtensionLoader<Protocol>中获取hessian对应的实例,即HessianProtocol实例

      也就是说Protocol$Adpative能够根据url中的protocol属性值动态的采用对应的实现。

  4. 对扩展采用装饰器模式进行功能增强,类似AOP实现的功能

    • 接口A的另一个实现者AWrapper1。在获取某一个接口A的实现者A1的时候,已经自动被AWrapper1包装了。
      private A a;
      AWrapper1(A a){
          this.a=a;
      }

ExtensionLoader源码分析 

ExtensionLoader<Protocol> protocolLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol protocol = protocolLoader.getAdaptiveExtension();

@Extension("dubbo")
public interface Protocol {

    int getDefaultPort();
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
    void destroy();
}

第一步:根据要加载的接口创建出一个ExtensionLoader实例

重要属性: ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();

用于缓存所有的扩展加载实例,这里加载Protocol.class,就以Protocol.class为key,创建的ExtensionLoader为value存储到上述EXTENSION_LOADERS中。

第二步:ExtensionLoader实例是加载Protocol的实现类

  1. 先解析Protocol上的Extension注解的name,存至String cachedDefaultName属性中,作为默认的实现
  2. 到类路径下的加载 META-INF/services/com.alibaba.dubbo.rpc.Protocol文件,然后就是读取每一行内容,加载对应的class。(扩展配置文件: /META-INF/dubbo/internal,/META-INF/dubbo/,META-INF/services)
  3. 上述class分成三种情况来处理,对于一个接口的实现者,ExtensionLoader分三种情况来分别存储对应的实现者,属性分别如下:Class<?> cachedAdaptiveClass;Set<Class<?>> cachedWrapperClasses;Reference<Map<String, Class<?>>> cachedClasses;
    • 情况1: 如果这个class含有Adaptive注解,则将这个class设置为Class<?> cachedAdaptiveClass。
    • 情况2: 尝试获取对应接口参数的构造器,如果能够获取到,则说明这个class是一个装饰类即需要存到Set<Class<?>> cachedWrapperClasses中
    • 情况3: 如果没有上述构造器。则获取class上的Extension注解,根据该注解的定义的name作为key,存至Reference<Map<String, Class<?>>> cachedClasses结构中 

 

目录
相关文章
|
机器学习/深度学习 人工智能 数据可视化
文心千帆大模型测评分享,效果超出预期
文心千帆大模型测评分享,效果超出预期
337 1
|
11月前
|
存储 安全 Java
Java 集合框架中的老炮与新秀:HashTable 和 HashMap 谁更胜一筹?
嗨,大家好,我是技术伙伴小米。今天通过讲故事的方式,详细介绍 Java 中 HashMap 和 HashTable 的区别。从版本、线程安全、null 值支持、性能及迭代器行为等方面对比,帮助你轻松应对面试中的经典问题。HashMap 更高效灵活,适合单线程或需手动处理线程安全的场景;HashTable 较古老,线程安全但性能不佳。现代项目推荐使用 ConcurrentHashMap。关注我的公众号“软件求生”,获取更多技术干货!
201 3
|
缓存 负载均衡 算法
深入探索Linux内核的调度机制
本文旨在揭示Linux操作系统核心的心脏——进程调度机制。我们将从Linux内核的架构出发,深入剖析其调度策略、算法以及它们如何共同作用于系统性能优化和资源管理。不同于常规摘要提供文章概览的方式,本摘要将直接带领读者进入Linux调度机制的世界,通过对其工作原理的解析,展现这一复杂系统的精妙设计与实现。
593 8
|
算法 安全 Java
性能工具之 JMeter 自定义 Java Sampler 支持国密 SM2 算法
【4月更文挑战第28天】性能工具之 JMeter 自定义 Java Sampler 支持国密 SM2 算法
921 1
性能工具之 JMeter 自定义 Java Sampler 支持国密 SM2 算法
|
安全 前端开发 JavaScript
Node框架的优缺点
Node 框架的优点使其在现代 Web 开发中具有重要地位,但同时也需要开发者在使用过程中注意其缺点,合理选择和应用,以充分发挥其优势,避免潜在问题的出现。随着技术的不断发展和完善,Node 框架也在不断改进和优化,以更好地适应各种应用需求。
382 57
|
存储 移动开发 编解码
一文读懂Web Codecs API:浏览器背后的媒体魔术师
一文读懂Web Codecs API:浏览器背后的媒体魔术师
329 0
|
前端开发 API Android开发
Flutter最强大的图表库fl_chart的使用
Flutter最强大的图表库fl_chart的使用
1534 1
|
小程序 JavaScript API
微信小程序开发学习之页面导航(声明式导航和编程式导航)
这篇文章介绍了微信小程序中页面导航的两种方式:声明式导航和编程式导航,包括如何导航到tabBar页面、非tabBar页面、后退导航,以及如何在导航过程中传递参数和获取传递的参数。
微信小程序开发学习之页面导航(声明式导航和编程式导航)
|
JSON 搜索推荐 API
深入了解亚马逊商品详情API:功能、作用与实例
亚马逊商品详情API接口由官方提供,允许开发者通过程序调用获取商品详细信息,如标题、价格等,适用于电商数据分析、搜索及个性化推荐等场景。接口名称包括ItemLookup、GetMatchingProductForId等,支持HTTP POST/GET请求,需提供商品ID、API密钥及其他可选参数。返回数据格式通常为JSON或XML,涵盖商品详情、分类、品牌、价格、图片URL及用户评价等。该接口对数据收集、实时推荐、营销活动及数据分析至关重要,有助于提升电商平台的数据处理能力、用户体验及商家运营效率。使用时需注册亚马逊开发者账号并申请API访问权限,获取API密钥后按文档构建请求并处理响应数据。
|
机器学习/深度学习 运维 算法
【机器学习】可以利用K-means算法找到数据中的离群值吗?
【5月更文挑战第14天】【机器学习】可以利用K-means算法找到数据中的离群值吗?