李锋镝的博客

  • 首页
  • 时间轴
  • 留言
  • 插件
  • 左邻右舍
  • 关于我
    • 关于我
    • 另一个网站
  • 知识库
  • 赞助
Destiny
自是人生长恨水长东
  1. 首页
  2. 转载
  3. 技术
  4. 正文

Java SPI详解

2021年4月16日 17797点热度 0人点赞 1条评论

1.什么是SPI

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

2.SPI和API的使用场景

    API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。

    SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。  从使用人员上来说,SPI 被框架扩展人员使用。

3.SPI的简单实现

    下面我们来简单实现一个jdk的SPI的简单实现。

    首先第一步,定义一组接口:

public interface UploadCDN {
    void upload(String url);
}

   这个接口分别有两个实现:

public class QiyiCDN implements UploadCDN {  //上传爱奇艺cdn
    @Override
    public void upload(String url) {
        System.out.println("upload to qiyi cdn");
    }
}

public class ChinaNetCDN implements UploadCDN {//上传网宿cdn
    @Override
    public void upload(String url) {
        System.out.println("upload to chinaNet cdn");
    }
}

    然后需要在resources目录下新建META-INF/services目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件,在这个文件中写入接口的实现类的全限定名:

自动草稿

自动草稿

    这时,通过serviceLoader加载实现类并调用:

public static void main(String[] args) {
  ServiceLoader<UploadCDN> uploadCDN = ServiceLoader.load(UploadCDN.class);
  for (UploadCDN u : uploadCDN) {
    u.upload("filePath");
  }
}

    输出如下:
自动草稿

     这样一个简单的spi的demo就完成了。可以看到其中最为核心的就是通过ServiceLoader这个类来加载具体的实现类的。

4. SPI原理解析

     通过上面简单的demo,可以看到最关键的实现就是ServiceLoader这个类,可以看下这个类的源码,如下:

 
public final class ServiceLoader<S> implements Iterable<S> {

    //扫描目录前缀
    private static final String PREFIX = "META-INF/services/";

    // 被加载的类或接口
    private final Class<S> service;

    // 用于定位、加载和实例化实现方实现的类的类加载器
    private final ClassLoader loader;

    // 上下文对象
    private final AccessControlContext acc;

    // 按照实例化的顺序缓存已经实例化的类
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();

    // 懒查找迭代器
    private java.util.ServiceLoader.LazyIterator lookupIterator;

    // 私有内部类,提供对所有的service的类的加载与实例化
    private class LazyIterator implements Iterator<S> {
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        String nextName = null;

        //...
        private boolean hasNextService() {
            if (configs == null) {
                try {
                    //获取目录下所有的类
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    //...
                }
                //....
            }
        }

        private S nextService() {
            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) {
                //..
            }
            //..
        }
    }
}

 

     上面的代码只贴出了部分关键的实现,有兴趣的读者可以自己去研究,下面贴出比较直观的spi加载的主要流程供参考:

 

自动草稿

5.dubbo SPI

    dubbo作为一个高度可扩展的rpc框架,也依赖于java的spi,并且dubbo对java原生的spi机制作出了一定的扩展,使得其功能更加强大。

首先,从上面的java spi的原理中可以了解到,java的spi机制有着如下的弊端:

  • 只能遍历所有的实现,并全部实例化。
  • 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
  • 扩展如果依赖其他的扩展,做不到自动注入和装配。
  • 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持。

    dubbo的spi有如下几个概念:

    (1)扩展点:一个接口。

    (2)扩展:扩展(接口)的实现。

    (3)扩展自适应实例:其实就是一个Extension的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。dubbo会根据接口中的参数,自动地决定选择哪个实现。

    (4)@SPI:该注解作用于扩展点的接口上,表明该接口是一个扩展点。

    (5)@Adaptive:@Adaptive注解用在扩展接口的方法上。表示该方法是一个自适应方法。Dubbo在为扩展点生成自适应实例时,如果方法有@Adaptive注解,会为该方法生成对应的代码。

    dubbo的spi也会从某些固定的路径下去加载配置文件,并且配置的格式与java原生的不一样,类似于property文件的格式:

自动草稿

     下面将基于dubbo去实现一个简单的扩展实现。首先,要实现LoadBalance这个接口,当然这个接口是被注解标注的可以扩展的:

@SPI("random")
public interface LoadBalance {
    @Adaptive({"loadbalance"})
    <T> Invoker<T> select(List<Invoker<T>> var1, URL var2, Invocation var3) throws RpcException;
}

public class DemoLoadBalance implements LoadBalance {

    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        System.out.println("my demo loadBalance is used, hahahahh");
        return invokers.get(0);//选择第一个
    }
}

 

 
 

     然后,需要在duboo SPI的扫描目录下,添加配置文件,注意配置文件的名称要和扩展点的接口名称对应起来:

自动草稿

     还需要在dubbo的spring配置中显式的声明,使用上面自己实现的负载均衡策略:

<dubbo:reference id="helloService" interface="com.dubbo.spi.demo.api.IHelloService" loadbalance="demo" />

    然后,启动dubbo,调用service,就可以发现确实是使用了自定义的负载策略:

自动草稿

     至此,dubbo的spi的demo也完成了。

    dubbo spi的原理和jdk的实现稍有不同,大概流程如下图,具体的实现读者可以自己了解下源码。

自动草稿

6.总结

    关于spi的详解到此就结束了,总结下spi能带来的好处:

  • 不需要改动源码就可以实现扩展,解耦。
  • 实现扩展对原来的代码几乎没有侵入性。
  • 只需要添加配置就可以实现扩展,符合开闭原则。
除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/archives/transport/3305

相关文章

  • ThreadLocal如何解决内存泄漏问题
  • SpringBoot定时任务 - 经典定时任务设计:时间轮(Timing Wheel)案例和原理
  • @Resource 和 @Autowired 的区别
  • Spring Boot 2.x使用PostgreSQL数据库
  • 带颜色的JVM:三色标记详解
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JAVA SPI
最后更新:2021年4月16日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >

文章评论

  • 啊啊啊啊啊啊

    :huaji20:

    2021年4月16日
    回复
  • 1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
    取消回复

    愁云淡淡雨潇潇。暮暮复朝朝。别来应是,眉峰翠减,腕玉香销。
    小轩独坐相思处,情绪好无聊。一丛萱草,几竿修竹,数叶芭蕉。

    最新 热点 随机
    最新 热点 随机
    SpringBoot框架自动配置之spring.factories和AutoConfiguration.imports 应用型负载均衡(ALB)和网络型负载均衡(NLB)区别 什么是Helm? TransmittableThreadLocal介绍与使用 ReentrantLock深度解析 RedisTemplate和Redisson的区别
    玩博客的人是不是越来越少了?准备入手个亚太的ECS,友友们有什么建议吗?什么是Helm?2024年11月1号 农历十月初一别再背线程池的七大参数了,现在面试官都这么问URL地址末尾加不加“/”有什么区别
    超级文艺的句子 Linux开启FTP Hibernate、MyBatis的简介、区别以及优化 沁园春·长沙 哇塞~这个小姐姐实在太惊艳了…… C# 11 的这个新特性,我愿称之最强!
    标签聚合
    SQL Redis JAVA IDEA 面试 文学 docker Spring 多线程 数据库 日常 设计模式 K8s 架构 ElasticSearch 分布式 SpringBoot MySQL JVM 教程
    友情链接
    • i架构
    • 临窗旋墨
    • 博友圈
    • 博客录
    • 博客星球
    • 哥斯拉
    • 志文工作室
    • 搬砖日记
    • 旋律的博客
    • 旧时繁华
    • 林羽凡
    • 知向前端
    • 蜗牛工作室
    • 集博栈
    • 韩小韩博客
    • 風の声音

    COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

    Theme Kratos Made By Dylan

    津ICP备2024022503号-3