SPI 机制,全称为 Service Provider Interface,是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在 Dubbo、JDBC 中都使用到了 SPI 机制。本文介绍了 Java SPI 机制以及在模块化和非模块话项目中的实现方式(此处的模块化指 Java9 引入的模块化)

SPI 机制介绍

SPI 全称 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI 的作用就是为这些被扩展的 API 寻找服务实现。

SPI 和 API 的区别

API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。如下图所示,其中模块 A 为接口制定方和实现方,而模块 B 为接口的使用放。

维基百科关于 API 的描述:In building applications, an API (application programming interface) simplifies programming by abstracting the underlying implementation and only exposing objects or actions the developer needs. While a graphical interface for an email client might provide a user with a button that performs all the steps for fetching and highlighting new emails, an API for file input/output might give the developer a function that copies a file from one location to another without requiring that the developer understand the file system operations occurring behind the scenes.

SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现,可用于启用框架扩展和可替换组件。 从使用人员上来说,SPI 被框架扩展人员使用。如下图所示,A 模块是接口的定义方和使用方,而 B 模块则是接口实现方。

SPI 维基百科定义:Service Provider Interface (SPI) is an API intended to be implemented or extended by a third party. It can be used to enable framework extension and replaceable components

SPI java 官方定义:A service is a well-known set of interfaces and (usually abstract) classes. A service provider(SPI) is a specific implementation of a service. The classes in a provider typically implement the interfaces and subclass the classes defined in the service itself. Service providers can be installed in an implementation of the Java platform in the form of extensions, that is, jar files placed into any of the usual extension directories. Providers can also be made available by adding them to the application's class path or by some other platform-specific means.

SPI 的优点

使用 Java SPI 机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。应用进程可以根据实际业务情况启用或替换具体组件。以 java 中的 JDBC 数据库驱动为例,java 官方在核心库制定了 java.sql.Driver 数据库驱动接口,使用该接口实现了数据库链接等逻辑,但是并没有具体实现数据库驱动接口,而是交给 MySql 等厂商去实现具体的数据库接口。

引用自博客:Java SPI 主要是应用于厂商自定义组件或插件中。在 java.util.ServiceLoader 的文档里有比较详细的介绍。简单的总结下 java SPI 机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml 解析模块、jdbc 模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似 IOC 的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

SPI 的约定

服务提供方需要通过一些约定告诉系统自己所提供的服务的位置,java9 之后一共有两种约定方式:

  1. 通过在 META-INF/services/ 目录下配置相关文件实现。
  2. 通过 java9 jigsaw 的导出语句指定服务位置。

    A service provider is a single type, usually a concrete class. An interface or abstract class is permitted because it may declare a static provider method, discussed later. The type must be public and must not be an inner class.

    A service provider and its supporting code may be developed in a module, which is then deployed on the application module path or in a modular image. Alternatively, a service provider and its supporting code may be packaged as a JAR file and deployed on the application class path. The advantage of developing a service provider in a module is that the provider can be fully encapsulated to hide all details of its implementation.

    An application that obtains a service loader for a given service is indifferent to whether providers of the service are deployed in modules or packaged as JAR files. The application instantiates service providers via the service loader's iterator, or via Provider objects in the service loader's stream, without knowledge of the service providers' locations.

模块化语句约定

模块化语句约定适用于项目已经模块化的情况,以 java.sql.Driver 为例,在模块化文件 module-info.java 中添加如下语句,就可以向应用提供指定的服务:

provides java.sql.Driver with com.wangzemin.learning.provider.TestDriverProvider;

下文以一个自定义的 java.sql.Driver 服务提供者(也可以自己随意再另外一个模块定义一个接口)为例,展示 SPI 在 java 模块化情况下约定的使用方式。

1. 示例项目目录结构:

本文提供了一个完整的例子用于测试主要包含三个文件,TestDriverProvider(自定义的 Driver 服务提供者),module-info.java(java 模块化文件),Main(用于调试以及输出结果)

2. 示例文件内容

TestDriverProvider.java

public class TestDriverProvider implements Driver {
// Override 的方法都为空。
}

模块化文件 module-info.java:

module provider {
uses java.sql.Driver;
requires java.sql;
// 指定自定义的TestDriverProvider为Driver服务的提供者
provides java.sql.Driver with com.wangzemin.learning.provider.TestDriverProvider;
}

主函数(ServiceLoader 用于查找合适的服务提供者,下文会详细介绍):

public class Main {
public static void main(String[] args) {
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
for (Driver item : loader) {
System.out.println("Get class:" + item.getClass().descriptorString());
}
}
}

3. 示例的输出

Get class:Lcom/wangzemin/learning/provider/TestDriverProvider;

由输出可以看到,ServiceLoader 可以成功定位到 TestDriverProvider。

模块化约定官方原文

A service provider that is developed in a module must be specified in a provides directive in the module declaration. The provides directive specifies both the service and the service provider; this helps to locate the provider when another module, with a uses directive for the service, obtains a service loader for the service. It is strongly recommended that the module does not export the package containing the service provider. There is no support for a module specifying, in a provides directive, a service provider in another module.

A service provider that is developed in a module has no control over when it is instantiated, since that occurs at the behest of the application, but it does have control over how it is instantiated:

If the service provider declares a provider method, then the service loader invokes that method to obtain an instance of the service provider. A provider method is a public static method named "provider" with no formal parameters and a return type that is assignable to the service's interface or class.

In this case, the service provider itself need not be assignable to the service's interface or class.

If the service provider does not declare a provider method, then the service provider is instantiated directly, via its provider constructor. A provider constructor is a public constructor with no formal parameters.

In this case, the service provider must be assignable to the service's interface or class

A service provider that is deployed as an automatic module on the application module path must have a provider constructor. There is no support for a provider method in this case.

As an example, suppose a module specifies the following directives:

 provides com.example.CodecFactory with com.example.impl.StandardCodecs;
provides com.example.CodecFactory with com.example.impl.ExtendedCodecsFactory;

where

com.example.CodecFactory is the two-method service from earlier.

com.example.impl.StandardCodecs is a public class that implements CodecFactory and has a public no-args constructor.

com.example.impl.ExtendedCodecsFactory is a public class that does not implement CodecFactory, but it declares a public static no-args method named "provider" with a return type of CodecFactory.

A service loader will instantiate StandardCodecs via its constructor, and will instantiate ExtendedCodecsFactory by invoking its provider method. The requirement that the provider constructor or provider method is public helps to document the intent that the class (that is, the service provider) will be instantiated by an entity (that is, a service loader) which is outside the class's package.

配置文件约定

配置文件约定适用于项目没有模块化的情况,需要在 classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文档,这个文档里的内容就是这个接口的具体的实现类。

下文同样以一个自定义的 java.sql.Driver 服务提供者(也可以自己随意再另外一个模块定义一个接口)为例,展示 SPI 在 java 配置文件约定下的使用方式。

1. 示例项目目录结构:

本文提供了一个完整的例子用于测试主要包含三个文件,TestDriverProvider(自定义的 Driver 服务提供者,和上文一致),Main(用于调试以及输出结果,和上文一致),META-INF/services/java.sql.Driver 文件(用于指定服务提供着的位置)

2. 示例文件内容

TestDriverProvider 和 Main 与上文中均一致,不再次详述,此处仅仅展示 META-INF/services/java.sql.Driver 文件的内容,该文件只包含一行内容:

com.wangzemin.learning.learning.provider.TestDriverProvider

3. 示例的输出

Get class:Lcom/wangzemin/learning/provider/TestDriverProvider;

由输出可以看到,ServiceLoader 可以成功定位到 TestDriverProvider。

配置文件约定官方原文

A service provider that is packaged as a JAR file for the class path is identified by placing a provider-configuration file in the resource directory META-INF/services. The name of the provider-configuration file is the fully qualified binary name of the service. The provider-configuration file contains a list of fully qualified binary names of service providers, one per line.

For example, suppose the service provider com.example.impl.StandardCodecs is packaged in a JAR file for the class path. The JAR file will contain a provider-configuration file named:

META-INF/services/com.example.CodecFactory

that contains the line:

com.example.impl.StandardCodecs # Standard codecs

The provider-configuration file must be encoded in UTF-8. Space and tab characters surrounding each service provider's name, as well as blank lines, are ignored. The comment character is '#' ('\u0023' NUMBER SIGN); on each line all characters following the first comment character are ignored. If a service provider class name is listed more than once in a provider-configuration file then the duplicate is ignored. If a service provider class is named in more than one configuration file then the duplicate is ignored.

A service provider that is mentioned in a provider-configuration file may be located in the same JAR file as the provider-configuration file or in a different JAR file. The service provider must be visible from the class loader that is initially queried to locate the provider-configuration file; this is not necessarily the class loader which ultimately locates the provider-configuration file.

SPI 原理

上文讲述了 SPI 的一些约定,那么有了这些约定之后,SPI 机制是如何定位到对应的服务提供者的类并进行加载的呢?SPI 服务的加载可以分为两部分:

  1. 类全称限定名的获取,即知道哪些类是服务提供者。
  2. 类加载,把获取到的类加载到内存中,涉及上下文类加载器。

类限定名获取

模块化情况下

可以参考jigsaw 官方文档,jigsaw 模块化语法本身就支持 SPI 服务,通过 provide xxxx with yyyy,可以为 xxxx 服务指定一个服务提供者 yyyy,这个解析过程由 jigsaw 实现。

官方文档说明:Services allow for loose coupling between service consumers modules and service providers modules.This example has a service consumer module and a service provider module:

  1. module com.socket exports an API for network sockets. The API is in package com.socket so this package is exported. The API is pluggable to allow for alternative implementations. The service type is class com.socket.spi.NetworkSocketProvider in the same module and thus package com.socket.spi is also exported.
  2. module org.fastsocket is a service provider module. It provides an implementation of com.socket.spi.NetworkSocketProvider. It does not export any packages.

配置的情况下

在指定配置的情况下,ServiceLoader.load 根据传入的接口类,遍历 META-INF/services 目录下的以该类命名的文件中的所有类,然再用类加载器加载这些服务。

类加载器加载

获取到 SPI 服务实现类的文件之后,就可以使用类加载器将对应的类加载到内存中, 问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的双亲委派模型无法解决这个问题。所以 java 采用了线程上下文类加载器。破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,从而实现 SPI 服务的加载。线程上下文类加载器的实现如下:

  1. 在 ThreaLocal 中通过 setContextClassLoader​(ClassLoader cl)存储当前线程中的类加载器,默认为 AppClassLoader。
  2. Java 核心库中的程序在需要加载 SPI 实现类的时候,会首先通过 ThreaLocal 中的 getContextClassLoader​(ClassLoader cl)方法获取上下文类加载器,然后通过该类加载器加载 SPI 的实现类。

ServiceLoader

参考官方文档。ServiceLoader 是用于加载 SPI 服务实现类的工具,可以处理 0 个、1 个或者多个服务提供商的情况。

官方说明:A facility to load implementations of a service.A service is a well-known interface or class for which zero, one, or many service providers exist. A service provider (or just provider) is a class that implements or subclasses the well-known interface or class. A ServiceLoader is an object that locates and loads service providers deployed in the run time environment at a time of an application’s choosing. Application code refers only to the service, not to service providers, and is assumed to be capable of differentiating between multiple service providers as well as handling the possibility that no service providers are located.

其主要方法为 public static ServiceLoader load​(Class service, ClassLoader loader),该方法根据需要加载的 SPI 接口和类加载器(默认情况为线程上下文类加载器),生成一个 ServiceLoader,生成的 ServiceLoader 可以通过迭代器 Iterotor 或 stream 的方式获取 SPI 的实现类。加载主要分为两部分:模块化的服务类加载和非模块化的类加载。最后会对所有加载到的实现类排序。

注意:加载的服务类如果包含网络资源,可能会出现一些异常情况。

If the class path of the class loader includes remote network URLs then those URLs may be dereferenced in the process of searching for provider-configuration files.

This activity is normal, although it may cause puzzling entries to be created in web-server logs. If a web server is not configured correctly, however, then this activity may cause the provider-loading algorithm to fail spuriously.

A web server should return an HTTP 404 (Not Found) response when a requested resource does not exist. Sometimes, however, web servers are erroneously configured to return an HTTP 200 (OK) response along with a helpful HTML error page in such cases. This will cause a ServiceConfigurationError to be thrown when this class attempts to parse the HTML page as a provider-configuration file. The best solution to this problem is to fix the misconfigured web server to return the correct response code (HTTP 404) along with the HTML error page.

上文说到,SPI 可能有很多服务提供者,但是只有其中一些是有用的,这种情况下我们就需要对 ServiceLoader 获取到的服务实现类进行过滤,比如,我们只需要 PNG 格式的 CodecFactory,那么我们就可以对对应的服务实现类添加一个自定义的@PNG 注解,然后通过下文过滤得到所需的服务提供者:

 ServiceLoader<CodecFactory> loader = ServiceLoader.load(CodecFactory.class);
Set<CodecFactory> pngFactories = loader
.stream() // Note a below
.filter(p -> p.type().isAnnotationPresent(PNG.class)) // Note b
.map(Provider::get) // Note c
.collect(Collectors.toSet());

我是御狐神,欢迎大家关注我的微信公众号

本文最先发布至微信公众号,版权所有,禁止转载!

Java基础之SPI机制的更多相关文章

  1. 结合实战和源码来聊聊Java中的SPI机制?

    写在前面 SPI机制能够非常方便的为某个接口动态指定其实现类,在某种程度上,这也是某些框架具有高度可扩展性的基础.今天,我们就从源码级别深入探讨下Java中的SPI机制. 注:文章已收录到:https ...

  2. 【Java】深入理解Java中的spi机制

    深入理解Java中的spi机制 SPI全名为Service Provider Interface是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用 ...

  3. java中的SPI机制

    1 SPI机制简介 SPI的全名为Service Provider Interface.大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的.在java.util.ServiceLoader的文档里 ...

  4. Java基础:类加载机制

    之前的<java基础:内存模型>当中,我们大体了解了在java当中,不同类型的信息,都存放于java当中哪个部位当中,那么有了对于堆.栈.方法区.的基本理解以后,今天我们来好好剖析一下,j ...

  5. Java 中的 SPI 机制是什么鬼?高级 Java 必须掌握!

    作者:sigangjun blog.csdn.net/sigangjun/article/details/79071850 SPI的全名为:Service Provider Interface,大多数 ...

  6. java中高级并发SPI机制

    Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制. 适用于:调用者根据实际使用需要,启用.扩展.或者替换框架的实现策略. 要使用Java SPI,需要遵循如下约定 ...

  7. java基础之反射机制

    一.概念 JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的信息以及动态调用对象的方法的功能称为jav ...

  8. java基础篇---反射机制

    一.JAVA是动态语言吗? 一般而言,说到动态言,都是指在程序运行时允许改变程序结构或者变量类型,从这个观点看,JAVA和C++一样,都不是动态语言. 但JAVA它却有着一个非常突出的动态相关机制:反 ...

  9. Java基础:GC机制

    上一节,简单的介绍了java当中的内存模型,那么经常和内存模型一起提到的JAVA垃圾回收机制当然也需要在这里一并的总结一下. 所谓是垃圾回收机制,用通俗的话来说,就是将那些没有被任何变量引用的实例对象 ...

随机推荐

  1. Go通关04:正确使用 array、slice 和 map!

    Array(数组) 数组存放的是固定长度.相同类型的数据. 数组声明 var <数组名> = [<长度>]<元素>{元素1,元素2} var arr = [2]in ...

  2. 判断Windows系统是32位或64位并执行不同脚本命令

    判断Windows系统是32位或64位并执行不同脚本命令 https://www.autoahk.com/?p=16549&preview=true https://www.cnblogs.c ...

  3. 几张图搞懂 NodeJS 的流

    假设我们现在要盖一座房子,我们买了一些砖块,厂家正在送货.现在我们有两个选择,一是等所有砖块都到了以后再开始动工:二是到一批砖块就开始动工,砖块到多少我们就用多少. 这两种方式哪种效率更高呢?显然是第 ...

  4. Unix 网络IO模型介绍

    带着问题阅读 1.什么是同步异步.阻塞非阻塞 2.有几种IO模型,不同模型之间有什么区别 3.不同IO模型的应用场景都是什么 同步和异步.阻塞和非阻塞 同步和异步 广义上讲同步异步描述的是事件中发送方 ...

  5. 字节跳动前技术总监开源分享《Android架构设计权威指南》,YYDS!

    架构就像是一场进化史,根据不同时期的需求,演变出不同的架构,车轮滚滚,到今天,移动端框架百花齐放,让人目不暇接.但是其中的本质是磨灭不了的,换言之根本没有磨灭而是隐藏到了人们所看不到的地方,但是依旧发 ...

  6. SpringBoot报错:Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.

    Spring Boot报错:Error starting ApplicationContext. To display the conditions report re-run your applic ...

  7. 浅谈Java迭代器

    迭代器Iterator 概述: 迭代器(Iterator):它不是一个容器,它是一种用于访问容器的方法,可用于迭代 List.Set和Map等容器. 迭代:一个一个的往外拿. 作用:帮我们遍历或者拿到 ...

  8. 【Linux】LVM 逻辑卷管理

    LVM - 逻辑卷管理 简介 LVM(Logical Volume Manager), 即逻辑卷管理,是Linux环境下对磁盘分区进行管理的一种机制. 相关名词 PV(physical volume) ...

  9. 跟我一起写 Makefile(十)

    四.foreach 函数 foreach函数和别的函数非常的不一样.因为这个函数是用来做循环用的,Makefile中的foreach函数几乎是仿照于Unix标准Shell(/bin/sh)中的for语 ...

  10. 关于shell脚本——echo、for语句、while语句、until语句

    目录 一.echo 1.1.echo命令用法 1.2.echo截取字符 二.for语句 2.1.实例 创建用户名文件 创建脚本文件 运行脚本 三.while语句 3.1.实例 创建脚本文件 运行脚本 ...