深入浅出SPI机制

深入浅出SPI机制

一、SPI概念

SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现, 简单来说,它就是一种动态替换发现的机制, 举个例子来说, 有个接口,想运行时动态的给它添加实现,你只需要添加一个实现,而后,把新加的实现,描述给JDK知道就行啦(通过改一个文本文件即可)我们经常遇到的就是java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现

二、SPI与API对比

Java中API和SPI是不同的概念,API直接被应用开发人员使用,SPI被框架扩展人员使用

API Application Programming Interface
大多数情况下,都是实现方来制定接口并完成对接口的不同实现,调用方仅仅依赖却无权选择不同实现。

SPI Service Provider Interface
而如果是调用方来制定接口,实现方来针对接口来实现不同的实现。调用方来选择自己需要的实现方。

在面向接口编程中,接口位于哪个包中,我们又三种选择:

  1. 接口位于实现方所在的包中
  2. 接口位于调用方所在的包中
  3. 接口位于独立的包中
1.接口位于调用方所在的包中

对于类似这种情况下接口,我们将其称为 SPI, SPI的规则如下:

  • 概念上更依赖调用方。
  • 组织上位于调用方所在的包中。
  • 实现位于独立的包中。

常见的例子是:插件模式的插件。如:

  1. 数据库驱动 Driver
  2. 日志 Log
  3. dubbo扩展点开发
2.接口位于实现方所在的包中

对于类似这种情况下的接口,我们将其称作为API,API的规则如下:

  • 概念上更接近实现方。
  • 组织上位于实现方所在的包中。
3.接口位于独立的包中

如果一个“接口”在一个上下文是API,在另一个上下文是SPI,那么你就可以这么组织

三、SPI机制与策略模式的区别

  • 从设计思想来看:SPI机制和策略模式思想是类似的,它们遵循开闭原则,对扩展开放,对修改关闭,都是通过一定的设计隔离扩展与修改。
  • 从隔离级别来看:策略模式的隔离一般是类级别的隔离,而SPI机制一般是项目级别的隔离。
  • 从使用场景来看:SPI机制一般在框架设计时使用,策略模式则一般在业务代码中使用。

四、Java SPI

在jdk6里面引进的一个新的特性ServiceLoader,从官方的文档来说,它主要是用来装载一系列的service provider。而且ServiceLoader可以通过service provider的配置文件来装载指定的service provider。当服务的提供者,提供了服务接口的一种实现之后,我们只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

五、SPI 机制的约定

  1. 在 META-INF/services/ 目录中创建以接口全限定名命名的文件,该文件内容为API具体实现类的全限定名
  2. 使用 ServiceLoader 类动态加载 META-INF 中的实现类
  3. 如 SPI 的实现类为 Jar 则需要放在主程序 ClassPath 中
  4. API 具体实现类必须有一个不带参数的构造方法
    注意:配置文件编码采用UTF-8避免出错

六、jdk SPI案例

我们现在需要使用一个内容搜索接口,搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索。

先定义好接口

package com.cainiao.ys.spi.learn;

import java.util.List;

public interface Search {
    public List<String> searchDoc(String keyword);   
}

文件搜索实现

package com.cainiao.ys.spi.learn;

import java.util.List;

public class FileSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("文件搜索 "+keyword);
        return null;
    }
}

数据库搜索实现

package com.cainiao.ys.spi.learn;

import java.util.List;

public class DatabaseSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("数据搜索 "+keyword);
        return null;
    }
}

接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:com.cainiao.ys.spi.learn.Search,里面加上我们需要用到的实现类

com.cainiao.ys.spi.learn.FileSearch
然后写一个测试方法

package com.cainiao.ys.spi.learn;

import java.util.Iterator;
import java.util.ServiceLoader;

public class TestCase {
    public static void main(String[] args) {
        ServiceLoader<Search> s = ServiceLoader.load(Search.class);
        Iterator<Search> iterator = s.iterator();
        while (iterator.hasNext()) {
           Search search =  iterator.next();
           search.searchDoc("hello world");
        }
    }
}

可以看到输出结果:文件搜索 hello world

如果在com.cainiao.ys.spi.learn.Search文件里写上两个实现类,那最后的输出结果就是两行了。
这就是因为ServiceLoader.load(Search.class)在加载某接口时,会去META-INF/services下找接口的全限定名文件,再根据里面的内容加载相应的实现类。

这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的META-INF/services下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。

那为什么配置文件为什么要放在META-INF/services下面?
可以打开ServiceLoader的代码,里面定义了文件的PREFIX如下:

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

七、SPI的用途

数据库DriverManager、Spring、ConfigurableBeanFactory、Dubbo等都用到了SPI机制

八、Spring SPI

Spring中使用的类是SpringFactoriesLoader,在org.springframework.core.io.support包中

  1. SpringFactoriesLoader 会扫描 classpath 中的 META-INF/spring.factories文件。
  2. SpringFactoriesLoader 会加载并实例化 META-INF/spring.factories 中的制定类型
  3. META-INF/spring.factories 内容必须是 properties 的Key-Value形式,多值以逗号隔开。
8.1 使用

和 SPI 不同,由于 SpringFactoriesLoader 中的配置文件格式是 properties 文件,因此,不需要要像 SPI 中那样为每个服务都创建一个文件, 而是选择直接把所有服务都扔到 META-INF/spring.factories 文件中。

com.fsx.serviceloader.IService=com.fsx.serviceloader.HDFSService,com.fsx.serviceloader.LocalService

// 若有非常多个需要换行 可以这么写
// 前面是否顶头没关系(Spring在4.x版本修复了这个bug)
com.fsx.serviceloader.IService=\
    com.fsx.serviceloader.HDFSService,\
    com.fsx.serviceloader.LocalService
public static void main(String[] args) throws IOException {
        List<IService> services = SpringFactoriesLoader.loadFactories(IService.class, Main.class.getClassLoader());
        List<String> list = SpringFactoriesLoader.loadFactoryNames(IService.class, Main.class.getClassLoader());
        System.out.println(list); //[com.fsx.serviceloader.HDFSService, com.fsx.serviceloader.LocalService]
        System.out.println(services); //[com.fsx.serviceloader.HDFSService@794cb805, com.fsx.serviceloader.LocalService@4b5a5ed1]
    }

使用细节:

  • spring.factories内容的key不只能是接口,也可以是抽象类、具体的类。但是有个原则:=后面必须是key的实现类(子类)
  • key还可以是注解,比如SpringBoot中的的key:org.springframework.boot.autoconfigure.EnableAutoConfiguration,它就是一个注解
  • 文件的格式需要保证正确,否则会返回[](不会报错)
    =右边必须不是抽象类,必须能够实例化。且有空的构造函数~
  • loadFactories依赖方法loadFactoryNames。loadFactoryNames方法只拿全类名,loadFactories拿到全类名后会立马实例化
  • 此处特别注意:loadFactories实例化完成所有实例后,会调用AnnotationAwareOrderComparator.sort(result)排序,所以它是支持Ordered接口排序的,这个特点特别的重要。
8.2 源码剖析

因为Spring的这个配置文件和上面的不一样,它的名字是固定的spring.factories,里面的内容是key-value形式,因此一个文件里可以定义N多个键值对。它比JDK的SPI是更加灵活些的。
它主要暴露了两个方法:loadFactories和loadFactoryNames

// @since 3.2
public final class SpringFactoriesLoader {
	...
	public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
	...
	// 核心方法如下:
	private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
		MultiValueMap<String, String> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		try {
			// 读取到资源文件,遍历
			Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			result = new LinkedMultiValueMap<>();

			
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				// 此处使用的是URLResource把这个资源读进来~~~
				UrlResource resource = new UrlResource(url);
				
				// 可以看到,最终它使用的还是PropertiesLoaderUtils,只能使键值对的形式哦~~~ 当然xml也是被支持的
				
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryClassName = ((String) entry.getKey()).trim();
		
					// 使用逗号,分隔成数组,遍历   名称就出来了~~~
					for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
						result.add(factoryClassName, factoryName.trim());
					}
				}
			}
			cache.put(classLoader, result);
			return result;
		} catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
	}
}

九、Dubbo SPI

我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中,getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。该方法的逻辑比较简单,本章就不进行分析了。下面我们从 ExtensionLoader 的 getExtension 方法作为入口,对拓展类对象的获取过程进行详细的分析。

  • 对Dubbo进行扩展,不需要改动Dubbo的源码
  • 自定义的Dubbo的扩展点实现,是一个普通的Java类,Dubbo没有引入任何Dubbo特有的元素,对代码侵入性几乎为零。
  • 将扩展注册到Dubbo中,只需要在ClassPath中添加配置文件。使用简单。而且不会对现有代码造成影响。符合开闭原则。
  • Dubbo的扩展机制设计默认值:@SPI(“dubbo”) 代表默认的spi对象
  • Dubbo的扩展机制支持IoC,AoP等高级功能
  • Dubbo的扩展机制能很好的支持第三方IoC容器,默认支持Spring Bean,可自己扩展来支持其他容器,比如Google的Guice。
  • 切换扩展点的实现,只需要在配置文件中修改具体的实现,不需要改代码。使用方便。

十、参考文献

Java SPI思想梳理
Java SPI详解
Java SPI服务发现及Dubbo和Spring中SPI的应用

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值