一文搞懂Spring的SPI机制(详解与运用实战)

4,536 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情

Java SPI

SPI 全称 Service Provider Interface,是 Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API 寻找服务实现。本质是通过基于接口的 编程+策略模式+配置文件 实现动态加载。可以实现 解耦 (接口和实现分离),提高框架的 可拓展性(第三方可以自己实现,达到插拔式的效果)。

我们来开发SPI看一下。

首先定义一个接口。

public interface HelloSpi {

    String getName();

    void handle();

}

定义不同的实现类。

public class OneHelloSpiImpl implements HelloSpi {
    @Override
    public String getName() {
        return "One";
    }

    @Override
    public void handle() {
        System.out.println(getName() + "执行");
    }
}

public class TwoHelloSpiImpl implements HelloSpi {
    @Override
    public String getName() {
        return "Two";
    }

    @Override
    public void handle() {
        System.out.println(getName() + "执行");
    }
}

在指定目录(META-INF.services)下创建文件,(为什么是这个目录接下来会讲到)。

image.png

文件名是接口的全类名,文件内容是实现类的全类名。

com.shuaijie.impl.OneHelloSpiImpl
com.shuaijie.impl.TwoHelloSpiImpl

编写测试单元测试。是通过ServiceLoader这个类实现的功能,接下来会详细讲解这个类的实现原理。

@Test
public void testSpi() {
    ServiceLoader<HelloSpi> load = ServiceLoader.load(HelloSpi.class);
    Iterator<HelloSpi> iterator = load.iterator();
    while (iterator.hasNext()) {
        HelloSpi next = iterator.next();
        System.out.println(next.getName() + " 准备执行");
        next.handle();
    }
    System.out.println("执行结束");
}

执行结果为

One 准备执行
One执行
Two 准备执行
Two执行
执行结束

通过执行结果我们可以看出,HelloSpi接口的所有实现类都得到了调用,我们可以通过这种机制根据不同的业务场景实现拓展的效果。示例是通过ServiceLoader实现的,我们来看一下这个类。

ServiceLoader

ServiceLoader是一个简单的服务提供者加载工具。是JDK6引进的一个特性。

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

load方法是通过获取当前线程的 线程上下文类加载器 实例来加载的。Java应用运行的初始线程的上下文类加载器默认是系统类加载器。这里其实 破坏了双亲委派模型,因为Java应用收到类加载的请求时,按照双亲委派模型会向上请求父类加载器完成,这里并没有这么做(有些面试官会问到破坏双亲委派模型相关问题,简单了解)。

iterator.hasNext()主要是通过 hasNextService()来实现的,我们来看一下主要代码。

private static final String PREFIX = "META-INF/services/";
private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

该方法会去加载 PREFIX 变量路径下的配置,PREFIX 是一个固定路径,这也就是我们为什么要在META-INF/services/下创建文件的原因。并根据 PREFIX 加上全类名获取到实现类所在的全路径。

image.png

Java中有许多我们常见的框架使用SPI机制的地方,JDBC,Dubbo,Logback等,Spring中也有使用。

Spring SPI

Spring SPI对 Java SPI 进行了封装增强。我们只需要在 META-INF/spring.factories 中配置接口实现类名,即可通过服务发现机制,在运行时加载接口的实现类。

我们将讲解 Java SPI 的例子通过Spring SPI来实现一下。

image.png

测试代码如下。

@Test
public void testSpringSpi() {
    List<HelloSpi> helloSpiList = SpringFactoriesLoader.loadFactories(HelloSpi.class,this.getClass().getClassLoader());
    Iterator<HelloSpi> iterator = helloSpiList.iterator();
    while (iterator.hasNext()) {
        HelloSpi next = iterator.next();
        System.out.println(next.getName() + " 准备执行");
        next.handle();
    }
    System.out.println("执行结束");
}

执行结果,与Java SPI执行结果一致。

One 准备执行
One执行
Two 准备执行
Two执行
执行结束

Springboot利用Spring SPI开发starter

刚接触 Springboot 的时候引入一个个starter依赖,当时十分好奇这个东西,这一个个的starter就是通过Spring SPI来实现的。

我们也可以自己编写一个starter提供一些功能,传入公司的maven仓库,需要用到的项目就可以直接引入,减少项目对某些模块的代码硬侵入。

先来编写接口和实现。

package com.shuaijie.service;
public interface SpringbootStarterService {
    void handle();
}


package com.shuaijie.service.impl;
public class SpringbootStarterServiceImpl implements SpringbootStarterService {
    @Override
    public void handle() {
        System.out.println("SpringbootStarterServiceImpl执行");
    }
}

spring.factories中内容。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.shuaijie.service.impl.SpringbootStarterServiceImpl

pom.xml指定groupId,artifactId,version。

<groupId>com.shuaijie</groupId>
<artifactId>spring-boot-starter-shuaijie</artifactId>
<version>1.0.0</version>

然后通过maven打包传入maven仓库。其他的Springboot项目就可以直接使用了。

@Autowired
private SpringbootStarterService springbootStarterService;

@Test
public void testSpringbootStarter() {
    springbootStarterService.handle();
}

这种方式是不是很实用呢?