简析Java SPI机制

一. 什么是SPI?

搞清楚这个概念相对不难,SPI全称是:Service provider interface ,翻译成中文就是:服务提供发现接口。这里的服务发现和我们常听到的微服务中的服务发现并不相同。
Java SPI提供这样了这样一个机制:为某个接口寻找服务实现的机制。这有点类似IOC的思想,将装配的控制权移到了程序之外。
说了这么多,我只有一个问题,SPI到底是什么?
SPI其实是一种思想,一种面向接口编程的思想;
放一张图
分析一下这张图:

  • 先看看接口属于实现方的情况,这个很容易理解,实现方提供了接口和实现,我们可以引用接口来达到调用某实现类的功能,这就是我们经常说的api,它具有以下特征:
  1. 概念上更接近实现方;
  2. 组织上位于实现方所在的包中;
  3. 实现和接口在一个包中;
  • 当接口属于调用方时,我们就将其称为spi,全称为:service provider interface,它具有以下特征:
  1. 概念上更依赖调用方;
  2. 组织上位于调用方所在的包中;
  3. 实现位于独立的包中(也可认为在提供方中);

如图所示:
在这里插入图片描述

二. SPI怎么实现?

先放一张SPI实现的脑图
在这里插入图片描述

  1. 创建一个接口和它的实现类:

    /*
     * bq.com
     * Copyright (C) 2018-2020 All Rights Reserved.
     */
    package com.example.spitest;
    
    /**
     * @author liuyuan
     * @version SpiTestService.java, v 0.1 2020-08-14 17:07
     */
    public interface SpiTestService {
    
        void sayHello();
    }
    
    
    /*
     * bq.com
     * Copyright (C) 2018-2020 All Rights Reserved.
     */
    package com.example.spitest.impl;
    
    import com.example.spitest.SpiTestService;
    
    /**
     * @author liuyuan
     * @version SpiTestServiceImpl.java, v 0.1 2020-08-14 17:07
     */
    public class SpiTest01ServiceImpl implements SpiTestService {
    
        @Override
        public void sayHello() {
            System.out.println("hello spi SpiTest01ServiceImpl >>> 华为初始化...");
        }
    }
    
    
    /*
     * bq.com
     * Copyright (C) 2018-2020 All Rights Reserved.
     */
    package com.example.spitest.impl;
    
    import com.example.spitest.SpiTestService;
    
    /**
     * @author liuyuan
     * @version SpiTest02ServiceImpl.java, v 0.1 2020-08-14 17:08
     */
    public class SpiTest02ServiceImpl implements SpiTestService {
    
        @Override
        public void sayHello() {
            System.out.println("hello spi SpiTest02ServiceImpl >>> 小米初始化...");
        }
    }
    
  2. 在特定位置创建一个文件夹,文件夹下创建一个以接口全路径类名命名的文件:

    - src
    	-main
        	-resources
            	- META-INF
                	- services
                    	- com.example.spitest.SpiTestService
    
  3. 配置文件中指定实现类:

    com.example.spitest.impl.SpiTest01ServiceImpl
    com.example.spitest.impl.SpiTest02ServiceImpl
    
  4. 加载配置文件中指定的实现;

    /*
     * bq.com
     * Copyright (C) 2018-2020 All Rights Reserved.
     */
    package com.example.spitest;
    
    import com.example.spitest.impl.SpiTest02ServiceImpl;
    import org.springframework.stereotype.Service;
    
    import java.util.Optional;
    import java.util.ServiceLoader;
    import java.util.stream.StreamSupport;
    
    /**
     * @author liuyuan
     * @version SpiService.java, v 0.1 2020-08-14 17:16
     */
    @Service
    public class SpiService {
    
        public SpiTestService test() {
    
            // 使用 ServiceLoader 来加载配置文件中指定的实现(核心)
            ServiceLoader<SpiTestService> spiTestServices = ServiceLoader.load(SpiTestService.class);
    
            // 取第一个实现
            final Optional<SpiTestService> spiTestService = StreamSupport.stream(spiTestServices.spliterator(), false)
                    .findFirst();
    
            // 如果实现累不存在,手动创建一个
            return spiTestService.orElse(new SpiTest02ServiceImpl());
        }
    } 	
    
  5. 测试:

    package com.example.spitest;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class SpiTestApplicationTests {
    
        @Autowired
        private SpiService spiService;
    
        @Test
        void spiTest() {
            SpiTestService test = spiService.test();
            // 结果:hello spi SpiTest01ServiceImpl >>> 华为初始化...
            test.sayHello();
        }
    }
    

三. SPI的使用场景

  • 我们先看看上面自定义的一个SPI:
  1. 我们定义了一个SpiService接口,比如说这个接口是根据用户需要初始化不同厂商的插件;
  2. 现在我们有两个开源的插件(华为和小米),用户如果想要用小米,只需要依赖小米的依赖即可,不做任何其它操作;
  3. 那么小米和华为这两个厂商的jar需要做什么呢?他们只需要去实现SpiService接口,实现自己的业务逻辑,并且重复一下上面2、3、4步的内容,就可以自定义一个插件;

这里也就用到了SPI的思想:使用方提供规则,提供方根据规则把自己加载到使用方中

  • DriverManager spi案例
  1. 针对于一个数据库,市面上有很多种数据库驱动,对于用户来说,在使用某一个驱动时,不希望修改代码,只需要更换依赖和指定的配置即可;

  2. 打开mysql-connector-java的jar包,在META-INF/services发现了接口路径,打开里面的内容,可以看到是com.mysql.jdbc.Driver,那么我们是不是可以猜测,不同的驱动都需要去实现Driver接口;

    在这里插入图片描述

    在这里插入图片描述

  3. 查看DriverManager的源码,可以看到其内部的静态代码块中有一个loadInitialDrivers方法,在注释中我们看到用到了上文提到的spi工具类ServiceLoader;
    在这里插入图片描述
    点开这个方法,可以看到如下代码:
    在这里插入图片描述

  4. 走到这里,发现DriverManager和我们自定义SPI的2、3、4步大致一致,已经可以确定,DriverManager初始化时也运用了spi的思想,使用ServiceLoader把写到配置文件里的Driver都加载了进来。

其实用到SPI思想的框架还有很多,比如常用的Dubbo、插件体系、Spring等等…只要是能满足用户按照系统规则来自定义,并且可以注册到系统中的功能点,都带有着spi的思想。

四.补充

  1. 配置文件为什么要放在META-INF/services下面?
    我们打开ServiceLoader类,查看源码,可以看到里面定义了一个PREFIX:
    在这里插入图片描述

  2. 参考文档:https://zhuanlan.zhihu.com/p/28909673

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值