Java基础之SPI机制

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 为接口的使用放。

[图片上传失败...(image-528fa0-1649063971650)]

维基百科关于 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 模块则是接口实现方。

[图片上传失败...(image-51f54b-1649063971650)]

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(用于调试以及输出结果)

[图片上传失败...(image-5446be-1649063971649)]

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 模块化语法本身就支持 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 目录下的以该类命名的文件中的所有类,然再用类加载器加载这些服务。

[图片上传失败...(image-f5afbb-1649063971648)]

类加载器加载

获取到 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());

在此我向大家推荐一个架构学习交流圈。交流学习伪鑫:539413949(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码

)">
< <上一篇
下一篇>>