关于java:Java基础之SPI机制

67次阅读

共计 11916 个字符,预计需要花费 30 分钟才能阅读完成。

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

<!–more–>

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());

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

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

正文完
 0