前言
最近面试的过程被问到SPI机制,一脸懵逼,虽然听说过,但是也仅限于听说,无法用自己的话将SPI这个东西讲清楚,本文就重新讲讲SPI是怎么一回事,大家都知道吗?
SPI是个什么鬼
简介
SPI全称叫做Service Provider Interface,服务提供接口。它是是Java内置的一种服务提供发现机制,可以用来提高框架的扩展性。
怎么理解呢?简单理解就是在上层模块或者核心接口层中定义一个标准的服务接口,它没有实现,那谁来实现呢?有下层模块或者各个不同的服务提供方去实现这样的标准接口,如下图所示:
区别于API的概念:
API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
应用案例
如果还是没不理解的话,我这边举个非常常见的应用例子帮助大家理解。
我们都有用过jdbc连接数据吧,连接数据库需要驱动包,不同的数据库厂商,比如mysql或者oracle是不一样的。我们总不能在jdk中定义囊括所有的厂商实现吧,如配置是mysql,调用mysql的驱动,是oracle,调用oracle驱动,还要把各个厂商的驱动都内置进去,是不是非常ugly, 这时候我们的SPI机制派上用场了。
- 定义内置服务接口
JDK中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。
- 不同产商各自实现内置服务接口
- mysql厂商实现
在mysql厂商提供的jar包mysql-connector-java-6.0.6.jar
中,可以找到META-INF/services
目录,该目录下会有一个名字为java.sql.Driver
的文件,文件内容是com.mysql.cj.jdbc.Driver
,这里面的内容就是针对Java中定义的接口的实现。
- postgresql厂商实现
在postgresql的jar包postgresql-42.0.0.jar
中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver
,这是postgresql对Java的java.sql.Driver
的实现。
等等,其他厂商类似。
- 使用实现
调用下面的方法,就会将对应的实现驱动通过SPI机制加载进来。
Connection conn = DriverManager.getConnection(url,username,password);
它是如何做到的呢?我们看下DriverManager
的源码:
public class DriverManager { ...... /** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */ static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } ..... private static void loadInitialDrivers() { String drivers; ..... AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { //使用SPI的ServiceLoader来加载接口的实现 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); } }
DriverManager
类加载会先调用静态代码块中的loadInitialDrivers()
方法。loadInitialDrivers()
方法中的ServiceLoader.load()
方法就是jdk中对spi的实现,具体源码解读查看下一个章节。
小结:
此外还有一些其他使用SPI的例子,如下:
- 日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
- Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
核心思想
通过上面的介绍和例子的讲解,我们可以知道SPI核心的思想就是解耦。SPI机制将服务的具体实现转移到了程序外,为框架的扩展和解耦提供了极大的便利。
JDK中SPI实现
使用
要使用jdk的SPI需要,需要遵循如下约定:
- 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
- 接口实现类所在的jar包放在主程序的classpath中;
- 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
- SPI的实现类必须携带一个不带参数的构造方法;
直接上例子:
- 定义一个服务接口
public interface Phone { String getSystemInfo(); }
- 定义服务接口的实现
public class Huawei implements Phone { @Override public String getSystemInfo() { return "Hong Meng"; } } public class IPhone implements Phone { @Override public String getSystemInfo() { return "iOS"; } }
- 添加配置
- 目录名必须是 META-INF/services
- 文件名是接口的全路径
- 使用
- 通过
ServiceLoader
加载实现类并调用服务
@Test public void test1() { // 调用ServiceLoader的load方法 ServiceLoader<Phone> phoneServiceLoader = ServiceLoader.load(Phone.class); Iterator<Phone> iterator = phoneServiceLoader.iterator(); while (iterator.hasNext()) { Phone phone = iterator.next(); System.out.println("phone ...."); if(phone != null) { String systemInfo = phone.getSystemInfo(); System.out.println(systemInfo); } } }
源码实现
jdk中的SPI机制是通过ServiceLoader
类实现的,通过调用load()
方法实现对服务提供接口的查找,最后遍历来逐个访问服务提供接口的实现类。
public final class ServiceLoader<S> implements Iterable<S>{ // 服务提供接口对应文件放置目录 private static final String PREFIX = "META-INF/services/"; // The class or interface representing the service being loaded private final Class<S> service; // 类加载器 private final ClassLoader loader; // The access control context taken when the ServiceLoader is created private final AccessControlContext acc; // 按照初始化顺序缓存服务提供接口实例 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // 内部类,实现了Iterator接口 private LazyIterator lookupIterator; private class LazyIterator implements Iterator<S> { ... 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; } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { // 初始化 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen } .... } }
ServiceLoader
实现了Iterable
接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNext
和next
方法。这里主要都是调用的lookupIterator
的相应hasNext
和next
方法。LazyIterator
中的hasNext
方法,静态变量PREFIX就是“META-INF/services/
”目录,这也就是为什么需要在classpath
下的META-INF/services/
目录里创建一个以服务接口命名的文件。- 最后,通过反射方法
Class.forName()
加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers
对象中,然后返回实例对象。
小结
JDK中的SPI机制思想非常好,很好的实现了解耦,但是也存在一些缺点:
- 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用
ServiceLoader
类的实例是线程不安全的。 - 实现类必须要提供无参的构造方法。
Spring中SPI实现
因为jdk中的SPI实现存在着一些不完美的地方,所有Spring借鉴了jdk的思想,自己实现了一套SPI机制。
使用
- 创建一个服务接口
public interface Phone { String getSystemInfo(); }
- 创建两个服务实现类
public class Huawei implements Phone { @Override public String getSystemInfo() { return "Hong Meng"; } } public class IPhone implements Phone { @Override public String getSystemInfo() { return "iOS"; } }
- 添加配置
- 在项目META-INF/目录下创建spring.factories文件
- 文件内容key是接口的全限定名,value是实现类
#key是接口的全限定名,value是接口的实现类 com.alvin.error.spi.Phone=com.alvin.error.spi.Huawei,com.alvin.error.spi.IPhone
- 使用
- 调用
SpringFactoriesLoader.loadFactories
方法加载
@Test public void test() { // 调用SpringFactoriesLoader.loadFactories方法加载Phone接口所有实现类的实例 List<Phone> spis = SpringFactoriesLoader.loadFactories(Phone.class, Thread.currentThread().getContextClassLoader()); // 遍历Phone接口实现类实例 for (Phone spi : spis) { System.out.println(spi.getSystemInfo());; } }
源码实现
Spring中使用SpringFactoriesLoader
类实现了对SPI机制的支持。
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; // spring.factories文件的格式为:key=value1,value2,value3 // 从所有的jar包中找到META-INF/spring.factories文件 // 然后从文件中解析出key=factoryClass类名称的所有value值 public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); // 取得资源文件的URL Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); List<String> result = new ArrayList<String>(); // 遍历所有的URL while (urls.hasMoreElements()) { URL url = urls.nextElement(); // 根据资源文件URL解析properties文件,得到对应的一组@Configuration类 Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); String factoryClassNames = properties.getProperty(factoryClassName); // 组装数据,并返回 result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); } return result; }
- 从classpath下的每个Jar包中搜寻所有
META-INF/spring.factories
配置文件 - 然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在classpath下的jar包中
- 最后将扫描得到的类通过反射实例化
总结
SPI技术将服务接口与服务实现分离以达到解耦,极大的提升程序的可扩展性。本文主要带大家理解了什么是SPI,然后分析了JDK和Spring中对SPI机制的支持,如果对大家有帮助,请留下一个赞。