Java SPI机制解析

Java SPI机制解析

什么是SPI

SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制。

整体机制如图
[

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

例如jdbc编程,往往由社区制定规范,然后各大厂商根据规范制定对应的功能,在JDBC中就出现了mysql、oracle等,通过SPI可实现可拔插的实现机制

使用场景

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

Java常见场景

  • 数据库驱动加载接口实现类的加载 JDBC加载不同类型数据库的驱动
  • 日志接口SLF4J加载不同提供商的日志实现类
  • Dubbo框架中大量使用如DubboFilter、LoadBalance等SPI实现

功能开发步骤

  1. 制定统一的接口
  2. 服务提供者根据统一的接口,做出具体实现
  3. 服务提供者暴露服务(JAVA SPI 通过在Resouce下META-INF/services目录下创建一个以接口全限定类名为命名的文件,文件内容为具体实现接口全限定类名,可多个)
  4. 调用方根据需要引用特定的服务提供者jar包(主程序将通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件,找到需装载的类)

代码示例

以下是代码示例,视频播放的SPI,功能如下,我们按着上述一步步来实现这个播放器管理吧

制定统一的接口
  1. 根据对应的URL,播放器驱动获取对应的播放器。(类比,java.sql.Driver)
/**
 * Description 
 *
 * @author Created by 叶半仙 on 2019/7/3.
 */
public interface VideoPlayerDriver {

    /**
     * 获取对应的播放器
     * @param url
     * @return
     * @throws IllegalAccessException
     */
    VideoPlayer getPlayer(String url) throws IllegalAccessException;

    /**
     * 是否支持对应的url
     * @param url
     * @return
     */
    boolean support(String url);
}
  1. 播放器接口,暂定以下接口,播放接口,获取时长接口,播放器名称,播放器版本等接口,实现如下(类比,java.sql.Connection)
/**
 * Description 播放器接口方法
 *
 * @author Created by 叶半仙 on 2019/7/3.
 */
public interface VideoPlayer {
    /**
     * 播放
     * @param url
     * @return
     */
    String play(String url);

    /**
     * 获取对应时长
     */
    long duration(String url);

    /**
     * 播放器名称
     * @return
     */
    String playerName();

    /**
     * 播放器版本
     */
    String playerVersion();
}

以上,我们完成了我们开发步骤的第一步,定义好了我们所需要的接口,但是,大家想想,我们在使用jdbc驱动的时候,我们是不是还有一个DriverManager去管理所有的驱动?所以我们这里也需要一个VideoPlayerDriverManager

DriverManager的作用
  • 服务发现
  • 服务注册
  • 服务调用适配

针对以上三点,我对VideoPlayerDriverManager的实现大致有了以下三个方法

  • loadInitialPlayers() 服务发现
  • registered(VideoPlayerDriver videoPlayerDriver) 服务注册
  • VideoPlayer player(String url) 根据对应的URL,自动适配选用合适的播放器

以上三个方法已可在java.sql.DriverManager找到对比

  • loadInitialDrivers();
  • registerDriver(Driver driver);
  • Connection getConnection(String url,String user, String password)

其中,一般服务发现,是在DriverManager加载的时候就会去做,所有代码实现中,会把该方法放在静态代码块内

static {
    loadInitialPlayers();
    logger.debug("加载驱动完成");
}

其中loadInitialPlayers() 是我们的重中之重,java.util.ServiceLoder去自动扫描所有我们需要动态装载的实现类

    private static void loadInitialPlayers(){
        ServiceLoader<VideoPlayerDriver> serviceLoader = ServiceLoader.load(VideoPlayerDriver.class);
        // 获取所有在META-INF/services下注册的VideoPlayerDriver接口实现
        Iterator<VideoPlayerDriver> iterator = serviceLoader.iterator();
        while (iterator.hasNext()){
            //驱动类加载
            iterator.next();
        }
    }

其实上面几个接口也对应了 Service Provider Framework 的四个概念:

  • Service Interface 服务接口,这里对应 VideoPlayer 接口。
  • Provider Registration API 用户注册接口,这里对应 VideoPlayerDriverManager.registerDriver() 方法。
  • Service Access API 获取服务实例方法,这里对应 VideoPlayerDriverManager.player(url) 方法。
  • Service Provider Interface 创建服务实现的接口,这里对应 VideoPlayerDriver 接口。
    所有借助 Java SPI 机制实现的框架,除了Service Interface 服务接口不是必须的之外,其他三个都是必须要有的。
    VideoPlayerDriverManager源码
/**
 * Description 播放器驱动管理类
 *
 * @author Created by 叶半仙 on 2019/7/3.
 */
public class VideoPlayerManager {
    /**
     * 处理器集合
     */
    private final static CopyOnWriteArrayList<VideoPlayerDriver> registeredDrivers = new CopyOnWriteArrayList<>();


    private static final Logger logger = LoggerFactory.getLogger( VideoPlayerManager.class );
    static {
        loadInitialPlayers();
        logger.debug("加载驱动完成");
    }

    /**
     * 初始化
     */
    private static void loadInitialPlayers(){
        ServiceLoader<VideoPlayerDriver> serviceLoader = ServiceLoader.load(VideoPlayerDriver.class);
        // 获取所有在META-INF/services下注册的VideoPlayerDriver接口实现
        Iterator<VideoPlayerDriver> iterator = serviceLoader.iterator();
        while (iterator.hasNext()){
            //驱动类加载
            iterator.next();
        }
    }

    /**
     * 获取对应的播放器
     * @param url
     * @return
     */
    public static VideoPlayer player(String url){
        VideoPlayer videoPlayer = null;
        for (VideoPlayerDriver  driver:registeredDrivers){
            try {
                    videoPlayer = driver.getPlayer(url);
                    break;
            }catch (Exception e){
                logger.debug("驱动器:{},处理发生异常",driver.getClass(),e);
            }
        }
        return videoPlayer;
    }

    /**
     * 获取所有的驱动器
     * @return
     */
    public static  List<VideoPlayerDriver> getDrivers(){
        List<VideoPlayerDriver> videoPlayerDrivers = new LinkedList<>();
        registeredDrivers.forEach(e->videoPlayerDrivers.add(e));
        return videoPlayerDrivers;
    }

    /**
     * 注册驱动
     * @param videoPlayerDriver
     */
    public static synchronized void registered(VideoPlayerDriver videoPlayerDriver){
        logger.info("注册驱动:{}",videoPlayerDriver.getClass());
        registeredDrivers.addIfAbsent(videoPlayerDriver);
    }
    public static void main(String[] args) {
       logger.info("测试驱动加载");
    }
}
服务提供者根据统一的接口,做出具体实现

我这里新建了个tencent 腾讯视频播放器的module,并针对VideoPlayerVideoPlayerDriver 做出对应版本的TencentVideoPlayerTencentVideoPlayerDriver

/**
 * Description 腾讯视频播放驱动
 *
 * @author Created by 叶半仙 on 2019/7/3.
 */
public class TencentVideoPlayerDriver implements VideoPlayerDriver {
    /**
     * 如果url的格式是http://www.vip.qq.com/的格式,代表需要使用腾讯视频的播放器
     */
    public static final String  REGEX = "^http(s)?://www\\.vip\\.qq\\.com/[A-Za-z0-9\\.]*";
    public static final Pattern PATTERN = Pattern.compile(REGEX);
    @Override
    public VideoPlayer getPlayer(String url) {
        if (support(url)){
            // 此处可扩展成工厂或单例模式,直接new也是不太符合规范的
            return new TencentVideoPlayer();
        }
        throw new RuntimeException("该url不支持");
    }

    @Override
    public boolean support(String url) {
        return PATTERN.matcher(url).matches();
    }


    static{
        // 向VideoPlayerDriver注册驱动
        VideoPlayerManager.registered(new TencentVideoPlayerDriver());
    }
}

这样,一个新的播放器驱动我们就写好了,其中。千万不要忘记加个静态块,向我们的DriverManager做驱动注册哟。

/**
 * Description 腾讯播放器
 *
 * @author Created by 叶半仙 on 2019/7/3.
 */
public class TencentVideoPlayer implements VideoPlayer {
    /**
     * 播放
     *
     * @param url
     * @return
     */
    @Override
    public String play(String url) {
        return playerName()+": "+url;
    }

    /**
     * 获取对应时长
     *
     * @param url
     */
    @Override
    public long duration(String url) {
        return url.length();
    }

    /**
     * 播放器名称
     *
     * @return
     */
    @Override
    public String playerName() {
        return "腾讯视频";
    }

    /**
     * 播放器版本
     */
    @Override
    public String playerVersion() {
        return "1.0.0-SNAPSHOT";
    }

}

我这边还写了一个youku的播放器,驱动方面仅支持 http://www.youku.com 的url

/**
 * Description 优酷播放器驱动
 *
 * @author Created by 叶半仙 on 2019/7/3.
 */
public class YoukuVideoPlayerDriver implements VideoPlayerDriver {
    /**
     * 如果url的格式是http://www.youku.com/的格式,代表需要使用优酷视频的播放器
     */
    public static final String  REGEX = "^http(s)?://www\\.youku\\.com/[A-Za-z0-9\\.]*";
    public static final Pattern PATTERN = Pattern.compile(REGEX);
    @Override
    public VideoPlayer getPlayer(String url) {
        if (support(url)){
            return new YoukuVideoPlayer();
        }
        throw new RuntimeException("该url不支持");
    }

    @Override
    public boolean support(String url) {
        return PATTERN.matcher(url).matches();
    }

    static{
        VideoPlayerManager.registered(new YoukuVideoPlayerDriver());
    }

youku的player我就不贴了,可能在优酷播放器里面就主要是打印的文字不太一样,主要是为了吐槽优酷视频的广告太长。哈哈哈

服务提供者暴露服务

这里我们需要严格按照SPI要求的格式

  • 在resources目录下新建META-INF/service目录
  • 该目录下新建以 接口全限定类名 文件,以VideoPlayerDriver为例,

com.zhongkk.spi.video.driver.VideoPlayerDriver

  • 在上面文件中写上实现的接口全限定类名

com.zhongkk.spi.video.tencent.driver.TencentVideoPlayerDriver
[

调用方根据需要引用特定的服务提供者jar包

在maven/gradle项目中引用对应jar包

    //驱动接口包,管理类包
    compile project(':manager')
    // 腾讯视频驱动包
    compile project(':spi_video_tencent_player')
    // 优酷视频驱动包
    compile project(':spi_video_youku_player')

以上类似于,我们如果需要使用mysql,我们就引用

compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.16'

如果我们要使用oracle,我们就引入

compile group: 'com.oracle', name: 'ojdbc14', version: '10.2.0.4.0'

好了,以上我们就完成了我们需要做的前置工作。现在,我们开始测试看看效果吧

测试

测试代码如下

/**
 * Description 播放器驱动测试类
 *
 * @author Created by 叶半仙 on 2019/7/3.
 */
public class Main {
    public static void main(String[] args) {
        testTencent();
        testYouku();
    }

    public static void testTencent() {
        String url = "http://www.vip.qq.com/zhongkk.mp4";
        VideoPlayer player = VideoPlayerManager.player(url);
        System.out.println("=====  "+player.playerName()+"播放器欢迎您  =====");
        System.out.println(player.play(url));
        System.out.println("时长: "+player.duration(url)+"分钟");
    }
    public static void testYouku() {
        String url = "http://www.youku.com/zhongkk.mp4";
        VideoPlayer player = VideoPlayerManager.player(url);
        System.out.println("=====  "+player.playerName()+"播放器欢迎您  =====");
        System.out.println(player.play(url));
        System.out.println("时长: "+player.duration(url)+"分钟");
    }
}


我们在驱动管理器初始化的服务发现中,发现tencent和youku的驱动都会自动发现到,调用next方法,试对应驱动加载,调用对应的静态块,完成驱动注册。

注册后的驱动如下

看看两个打印下的数据

完美 perfect !

源码分析

首先看ServiceLoader类的签名类的成员变量:

public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";

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

    // 用于定位,加载和实例化providers的类加载器
    private final ClassLoader loader;

    // 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;

    // 缓存providers,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 懒查找迭代器
    private LazyIterator lookupIterator;
  
    ......
}

其中 load方法作用为,新建ServiceLoader,并实例化该类中的成员变量

  • 读取变量PREFIX = "META-INF/services/" 目录和对应的service.getName()类名称(注意:ServiceLoader可以跨越jar包获取META-INF下的配置文件)
  • 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
  • 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象

总结

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

缺点:

  • 延迟加载,但会被所有的实现类加载一遍,某些不需要使用的类也被加载,导致浪费。不够灵活
  • 多个并发多线程使用ServiceLoader类的实例是不安全的

参考

理解的Java中SPI机制

Java中SPI机制深入及源码解析

项目地址

视频播放spi测试项目

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值