Java SPI机制

引言

在软件设计模式,有个原则叫开闭原则(对拓展开放,对修改关闭),其核心思想是面向抽象编程。

举个例子,我们定义一个车的抽象,然后我实现一个抽象将其变成自行车,在实现这个抽象将其变成摩托车,那么对于车子这个抽象来说就有了两种不用的实现。

一般情况下,我们自己选择调用哪个实现,来使用相关的功能。那么问题来了,如果这两个实现变成两个模块,如果在上层接口调用势必要涉及到具体实现类,这就违反了开闭原则

Java中提供了SPI机制,就是为了解决这个问题的。

什么是SPI?

SPI是Service Provider Interface的缩写,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。

SPI整体机制图如下:

spi

我们可以看出来,SPI最大的特点是由服务调用方制定好接口规范,服务提供方来根据规范进行实现。这一点,和我们日常开发的时候有点点区别。我们日常开发的时候,都是自己定好规范,然后提供给服务调用方。

实战演示

  1. 首先创建一个Java工程,文件结构如下:

    spi-demo

    ​ |–resources

    ​ |–META-INF

    ​ |–serevices

    ​ |–src

    ​ |–com.zhu.spitest

  2. 在com.zhu.spitest下定义我们要实现的接口Phone,类全限定名是com.zhu.spitest.Phone,代码如下:

    package com.zhu.spitest;
    
    public interface Phone {
        String getSystemInfo();
    }
    
  3. 在相同路径下写下具体实现类,代码如下:

    package com.zhu.spitest;
    public class Android implements Phone{
        @Override
        public String getSystemInfo() {
            return "安卓手机";
        }
    }
    
    
    package com.zhu.spitest;
    public class Iphone implements Phone{
        @Override
        public String getSystemInfo() {
            return "苹果手机";
        }
    }
    
  4. 在resouces/META-INF/services目录下,新增一个文件,文件名是步骤2中结构的全限定名,即com.zhu.spitest.Phone

    spi2

  5. 在4中的文件中写入如下内容:

    com.zhu.spitest.Iphone
    com.zhu.spitest.Android
    
  6. 在Phone同级目录下创建一个测试类Main,具体代码如下:

    package com.zhu.spitest;
    import java.util.ServiceLoader;
    public class Main {
        public static void main(String[] args) {
            ServiceLoader<Phone> serviceLoaders = ServiceLoader.load(Phone.class);
            serviceLoaders.forEach(e->{
                String systemInfo = e.getSystemInfo();
                System.out.println(systemInfo);
            });
        }
    }
    
    //输出结果
    //苹果手机
    //安卓手机
    

实现原理解析

我们可以看到,其实最关键的代码就是ServiceLoader.load(Phone.class),我们打开源码发现有个变量:

private static final String PREFIX = "META-INF/services/";

这就是为什么我们之前要在resources目录下创建META-INF/services/文件夹的原因。其实原理也很简单,就是在这个文件夹下,根据文件名和内容去加载指定的类。

有什么用?

解耦的作用之前说了,还有一种作用其实是破环双亲委托模型!

我们看一个常用的类java.sql.Driver,相信大家都很熟悉,这是数据库驱动的顶层接口,它的包处于JAVA_HOME/lib目录下,一般是Bootstrap类加载器来加载。但是,这个东西只是一个接口,没有具体的实现。具体的实现,是有各个数据库厂商来提供的,而三方的包一般都是由app类加载器来加载的,这就是问题所在。

为了绕过这个限制,就有了spi,它会传递进来一个当前线程的类加载器来加载厂商的实现。

public static <S> ServiceLoader<S> load(Class<S> service) {
  ClassLoader cl = Thread.currentThread().getContextClassLoader();
  return ServiceLoader.load(service, cl);
}