android spi 简书,Service Provider Interface详解 (SPI)

1.介绍

熟悉JDBC的同学都知道,在jdbc4.0之前,在使用DriverManager获取DB连接之前,我们总是需要显示的实例化DB驱动。比如,对mysql,典型的代码如下:

Connection conn = null;

Statement stmt = null;

try{

// 注册 JDBC driver

Class.forName("com.mysql.jdbc.Driver");

// 打开连接

conn = DriverManagger.getConnection(DB_URL,USER,PASSWD);

// 执行一条sql

stmt = conn.createStatement();

ResultSet rs = stmt.executeQuery(sql);

// 数据解包

while(ts.next()){

// 根据列名获取列值

// ...

} catch(SQLException se) {

// ...

} final {

try {

if (stmt!=null) stmt.close();

} catch(Exception e) {/*ignored*/}

try {

if (conn!=null) conn.close();

} catch(Exception e) {/*ignored*/}

}

}

JDBC的开始,总是需要通过Class.forName显式实例化驱动,否则将找不到对应DB的驱动。但是JDBC4.0开始,这个显式的初始化不再是必选项了,它存在的意义只是为了向上兼容。那么JDBC4.0之后,我们的应用是如何找到对应的驱动呢?

答案就是SPI(Service Provider Interface)。Java在语言层面为我们提供了一种方便地创建可扩展应用的途径。SPI提供了一种JVM级别的服务发现机制,我们只需要按照SPI的要求,在jar包中进行适当的配置,jvm就会在运行时通过懒加载,帮我们找到所需的服务并加载。如果我们一直不使用某个服务,那么它不会被加载,一定程度上避免了资源的浪费。

2.一个简单的例子

我们通过一个简单的例子看看如何最小化构建一个基于SPI的服务。

2.1 创建一个默认的maven项目

$ mvn archetype:generate -DgroupId=cn.jinlu.spi.demo -DartifactId=simplespi -Dversion=0.1-SNAPSHOT -DpackageName=cn.jinlu.spi.demo -DarchetypeArtifactId=maven-archetype-quickstart

...

[INFO] Using property: groupId = cn.jinlu.spi.demo

[INFO] Using property: artifactId = simplespi

[INFO] Using property: version = 0.1-SNAPSHOT

[INFO] Using property: package = cn.jinlu.spi.demo

Confirm properties configuration:

groupId: cn.jinlu.spi.demo

artifactId: simplespi

version: 0.1-SNAPSHOT

package: cn.jinlu.spi.demo

Y: :[回车]

2.2 添加一个interface或abstract class

Java SPI并没有强制必须使用interface或abstract class,完全可以将class注册为SPI注册服务,但是作为可扩展服务,使用interface或abstract class是一个好习惯。

在包 “cn.jinlu.spi.demo”中定义一个接口Animal:

package cn.jinlu.spi.demo;

public interface Animal {

void eat();

void sleep();

}

2.3 提供实现类

package cn.jinlu.spi.demo.impl;

import cn.jinlu.spi.demo.Animal;

public class Elephant implements Animal {

@Override

public void eat() {

System.out.println("Elephant is eating");

}

@Override

public void sleep() {

System.out.println("Elephant is sleeping");

}

}

2.4 服务注册

在main目录下创建目录 "resources/META-INF/services"

mkdir -p resources/META-INF/services

再在该目录下创建以接口Animal全限定名为名的配置文件,文件内容为该接口的实现类的全限定名,即

echo "cn.jinlu.spi.demo.impl.Elephant" > resources/META-INF/services/cn.jinlu.spi.demo.Animal

完成此步骤后,在当前maven项目的 src/main/resources/META-INF/services下有这么一个配置文件:"cn.jinlu.spi.demo.Animal",并且它的内容为"cn.jinlu.spi.demo.impl.Elephant"。

注意本步骤的要点:

必须放在JAR包或项目的指定路径,即 META-INF/services 下

必须以服务的全限定名命名配置文件,比如本例中,配置文件必须命名为 cn.jinlu.spi.demo.Animal,java会根据此名进行服务查找

内容必须是一个实现类的全限定名,如果要注册多个实现类,按行分割。注释以#开头。

2.5 增加单元测试:

注意,如果找不到@Test,可能是junit版本太低,在pom.xml中将其改为 4.0 或更高版本(maven-archetype-quickstart模板默认的JUNIT目前是3.8.1版本)。

package cn.jinlu.spi.demo;

import org.junit.Test;

import java.util.ServiceLoader;

public class AnimalTest {

@Test

public void animalTest() {

ServiceLoader animals = ServiceLoader.load(Animal.class);

for(Animal animal: animals) {

animal.eat();

animal.sleep();

}

}

}

2.6 执行结果:

Elephant is eating

Elephant is sleeping

可见,虽然我们没有显式使用Animal的实现类Elephant,但是java帮我们自动加载了改实现类。

3.源码分析

接下来从代码层面看看SPI都为我们做了什么。首先看看java.util.ServiceLoader的实现。在2.5节中,我们看到ServiceLoader使用非常简单,只需要调用一个静态方法load并以要加载的服务的父类(通常是一个interface或abstract class)作为参数,jvm就会帮我们构建好当前进程中所有注册到 META-INF/services/[service full qualified class name] 的服务。

3.1 创建ServiceLoader实例

下面是构造ServiceLoader实例的相关代码。ServiceLoader必须通过静态方法load(Class> service)的方式加载服务,默认会使用当前线程的上下文class loader。构造完ServiceLoader后,ServiceLoader实例并不会立刻扫描当前进程中的服务实例,而是创建一个LazyIterator懒加载迭代器,在实际使用时再扫描所有jar包找到对应的服务。懒加载迭代器被保存在一个内部成员lookupIterator中。

public final class ServiceLoader implements Iterable

{

...

/**

* 重新load指定serivice的实现。通过LazyIterator实现懒加载。

*/

public void reload() {

providers.clear();

lookupIterator = new LazyIterator(service, loader);

}

/**

* ServiceLoader构造函数,私有类型,必须通过ServiceLoader.load(Class>)静态方法来创建ServiceLoader实例

*/

private ServiceLoader(Class svc, ClassLoader cl) {

service = Objects.requireNonNull(svc, "Service interface cannot be null");

loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;

acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;

reload();

}

/**

* 构建ServiceLoader实例

*/

public static ServiceLoader load(Class service,

ClassLoader loader)

{

return new ServiceLoader<>(service, loader);

}

/**

* 通过service的class创建ServiceLoader实例,默认使用上下文classloader

*/

public static ServiceLoader load(Class service) {

ClassLoader cl = Thread.currentThread().getContextClassLoader();

return ServiceLoader.load(service, cl);

}

...

}

3.2 服务加载和遍历

上一节(3.1)的代码中,我们可以看到,在调用了ServiceLoader animals = ServiceLoader.load(Animal.class)之后,ServiceLoader会返回一个Animal.class类型的迭代器,但此时在ServiceLoader内部只是创建了一个LazyIterator,而不会真正通过classloader在classpath中寻找相关的服务实现。

相反,ServiceLoader通过实现Iterable>接口(public final class ServiceLoader implements Iterable),将对服务实现的寻址延后倒了对animals的遍历时执行。它在ServiceLoader内通过LazyIterator实现。

public final class ServiceLoader implements Iterable

{

...

// 缓存的service provider,按照初始化顺序排列。

private LinkedHashMap providers = new LinkedHashMap<>();

// 当前的LazyIterator迭代器指针,服务懒加载迭代器

private LazyIterator lookupIterator;

...

// 创建ServiceLoader迭代器,隐藏了LazyIterator的实现细节

public Iterator iterator() {

return new Iterator() {

// 创建Iterator迭代器时的ServiceLoader.providers快照,

// 因此在首次迭代时,iterator总是会通过LazyIterator进行懒加载

Iterator> knownProviders

= providers.entrySet().iterator();

public boolean hasNext() {

// 如果已经扫描过,则对providers进行迭代;

if (knownProviders.hasNext())

return true;

// 如果没有扫描过,则通过lookupIterator进行扫描和懒加载

return lookupIterator.hasNext();

}

public S next() {

// 如果已经扫描过,则对providers进行迭代;

if (knownProviders.hasNext())

return knownProviders.next().getValue();

// 如果没有扫描过,则通过lookupIterator进行扫描和懒加载

return lookupIterator.next();

}

public void remove() {

throw new UnsupportedOperationException();

}

};

}

...

}

ServiceLoader的迭代器很简单:

未进行迭代操作时,不对jar包作任何扫描

首次迭代时,因为ServiceLoader.providers中没有任何缓存,总是会通过LazyIterator进行懒加载,并将service实现的全限定名与加载的service实例作为key-value缓存到ServiceLoader.providers中。

之后再进行迭代时,总是在ServiceLoader.providers中进行。

3.3 懒加载迭代器LazyIterator

懒加载迭代器LazyIterator主要实现以下功能:

首次迭代时,通过ClassLoader.getResources(String)获得指定services文件的URL集合

如果是首次遍历懒加载器,或者对上一个URL内容解析获得的service实现类集合完成了迭代,则从configs中取下一个services文件URL进行解析,按行获得具体的service实现类集合,并进行迭代。

对当前URL中解析得到的实现类集合进行迭代,每次返回一个service实现类。

下面是LazyIterator的源码及注释:

public final class ServiceLoader implements Iterable

{

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

...

// Private inner class implementing fully-lazy provider lookup

private class LazyIterator implements Iterator

{

Class service;

ClassLoader loader;

Enumeration configs = null;

// 当前service配置文件的内容迭代器

// 即对services进行遍历,取出一个services配置文件,再对该文件按行解析,每行代表一个具体的service实现类,pending是某个services配置文件中service实现类的迭代器

Iterator pending = null;

String nextName = null;

private LazyIterator(Class service, ClassLoader loader) {

this.service = service;

this.loader = loader;

}

private boolean hasNextService() {

if (nextName != null) {

return true;

}

// 首次迭代时,configs为空,尝试通过classloader获取名为:

// "META-INF/services/[服务全限定名]"的所有配置文件

if (configs == null) {

try {

// 注意fullName的定义:"META-INF/services/[服务全限定名]"

String fullName = PREFIX + service.getName();

// 通过ClassLoader.getResources()获得资源URL集合

if (loader == null)

configs = ClassLoader.getSystemResources(fullName);

else

configs = loader.getResources(fullName);

} catch (IOException x) {

fail(service, "Error locating configuration files", x);

}

}

// 如果pending为空,或者pending已经迭代到迭代器末尾,则尝试解析下一个services配置文件

while ((pending == null) || !pending.hasNext()) {

if (!configs.hasMoreElements()) {

return false;

}

pending = parse(service, configs.nextElement());

}

// 对当前pending内容进行遍历,每一项代表services的一个实现类

nextName = pending.next();

return true;

}

}

...

}

最后,附上parse及parseLine的代码,可以发现,parseLine中会对服务实现类进行去重,所以在一个或多个services配置文件中配置多次的服务实现类只会被处理一次。

public final class ServiceLoader implements Iterable

{

...

// 按行解析给定配置文件。如果解析出的服务实现类没有被其他已解析的配置文件配置过,则通过参数nams返回给parse方法

//

private int parseLine(Class> service, URL u, BufferedReader r, int lc, List names) throws IOException, ServiceConfigurationError {

String ln = r.readLine();

if (ln == null) {

return -1;

}

int ci = ln.indexOf('#');

if (ci >= 0) ln = ln.substring(0, ci);

ln = ln.trim();

int n = ln.length();

if (n != 0) {

if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))

fail(service, u, lc, "Illegal configuration-file syntax");

int cp = ln.codePointAt(0);

if (!Character.isJavaIdentifierStart(cp))

fail(service, u, lc, "Illegal provider-class name: " + ln);

for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {

cp = ln.codePointAt(i);

if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))

fail(service, u, lc, "Illegal provider-class name: " + ln);

}

// 去重,防止重复配置服务,每个服务实现类只会被解析一次

if (!providers.containsKey(ln) && !names.contains(ln))

names.add(ln);

}

return lc + 1;

}

/**

* 解析指定的作为SPI配置文件的URL的内容

*/

private Iterator parse(Class> service, URL u) throws ServiceConfigurationError {

InputStream in = null;

BufferedReader r = null;

ArrayList names = new ArrayList<>();

try {

in = u.openStream();

r = new BufferedReader(new InputStreamReader(in, "utf-8"));

int lc = 1;

while ((lc = parseLine(service, u, r, lc, names)) >= 0);

} catch (IOException x) {

fail(service, "Error reading configuration file", x);

} finally {

try {

if (r != null) r.close();

if (in != null) in.close();

} catch (IOException y) {

fail(service, "Error closing configuration file", y);

}

}

return names.iterator();

}

...

}

4.JDBC中对SPI的使用

最后,以JDBC为例,看一个SPI的实际使用场景。在文章开始,我们提到过,JDBC4.0之前,我们总是需要在业务代码中显式地实例化DB驱动实现类:

Class.forName("com.mysql.jdbc.Driver");

为什么JDBC4.0之后不需要了呢?答案就在下面的代码中。在系统启动时,DriverManager静态初始化时会通过ServiceLoader对所有jar包中被注册为 java.sql.Driver 服务的驱动实现类进行初始化,这样就避免了上面通过Class.forName手动初始化的繁琐工作。

public class DriverManager {

// JDBC驱动注册中心,所有加载的JDBC驱动都注册在该CopyOnWriteArrayList中

private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>();

...

/* Prevent the DriverManager class from being instantiated. */

private 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() {

// 如果通过jdbc.drivers配置了驱动,则在本方法最后进行实例化

String drivers;

try {

drivers = AccessController.doPrivileged(new PrivilegedAction() {

public String run() {

return System.getProperty("jdbc.drivers");

}

});

} catch (Exception ex) {

drivers = null;

}

// If the driver is packaged as a Service Provider, load it.

// Get all the drivers through the classloader

// exposed as a java.sql.Driver.class service.

// ServiceLoader.load() replaces the sun.misc.Providers()

AccessController.doPrivileged(new PrivilegedAction() {

public Void run() {

// 通过ServiceLoader加载所有通过SPI方式注册的"java.sql.Driver"服务

ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);

Iterator driversIterator = loadedDrivers.iterator();

// 遍历ServiceLoader实例进行强制实例化,因此除了遍历不做任何其他操作

try{

while(driversIterator.hasNext()) {

driversIterator.next();

}

} catch(Throwable t) {

// Do nothing

}

return null;

}

}

);

println("DriverManager.initialize: jdbc.drivers = " + drivers);

// 强制加载"jdbc.driver"环境变量中配置的DB驱动

if (drivers == null || drivers.equals("")) {

return;

}

String[] driversList = drivers.split(":");

println("number of Drivers:" + driversList.length);

for (String aDriver : driversList) {

try {

println("DriverManager.Initialize: loading " + aDriver);

Class.forName(aDriver, true,

ClassLoader.getSystemClassLoader());

} catch (Exception ex) {

println("DriverManager.Initialize: load failed: " + ex);

}

}

}

...

}

以mySql驱动为例看看驱动实例化时做了什么:

package com.mysql.jdbc;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {

//

// Register ourselves with the DriverManager

//

static {

try {

// 向DriverManager注册自己

java.sql.DriverManager.registerDriver(new Driver());

} catch (SQLException E) {

throw new RuntimeException("Can't register driver!");

}

}

/**

* Construct a new driver and register it with DriverManager

*

* @throws SQLException

* if a database error occurs.

*/

public Driver() throws SQLException {

// Required for Class.forName().newInstance()

}

}

再看看mysql驱动jar包中对service的配置:

27c837293aeb

image

因此,只要某个驱动以这种方式被引用并被上下文class loader加载,那么该驱动就会通过SPI的方式被自动发现和加载。实际使用时,Driver.getDriver(url)会通过DB连接url获取到正确的驱动并建立与DB的连接。

备注

Android SM2、SM3、SM4 算法支持 Service Provider 及证书制作软件包 国密算法 JCAJCE Service Provider,适应版本 Android 4.2.2~7.0 支持 SM2 的 KeyFactory、KeyPairGenerator、Cipher、Signature、X.509 CertificateFactory 接口 支持 SM3 的 MessageDigest 接口、SM3withSM2 混合算法 支持 SM4 的 Cipher、KeyFactory、KeyGenerator、SecretKey 接口、相关算法 CMAC-SM4、Poly1305-SM4 增加 java.security.PublicKey 的子类 SM2PublicKey 增加 java.security.PrivateKey 的子类 SM2PrivateKey 全功能支持 SM3withSM2 算法的 X.509 证书结构体解释与密码运算 支持 BKS、PKCS#12 KeyStore 生成、解释、验算 X.509v1/v3 证书,签名算法支持 SM3withSM2、主流 RSA、DSA、ECDSA.... 生成、解释、验算 PKCS#10 证书申请,签名算法支持 SM3withSM2、主流 RSA、DSA、ECDSA.... *** 无须打包 BouncyCastle 支持库,体积小、节约内存 *** 请参阅 testSM.java、testCERT.java 文件列表: 1、AndroidSM.jar -- SM2、SM3、SM4 算法/证书支持的 JCA/JCE Service Provider 类库 2、AndroidCRT.jar -- X.509 数字证书/PKCS#10 证书申请相关类库 3、bc422.jar -- BouncyCastle 加密库,Android 4.2.2 内置版本(由真机导出dex文件转换而得,仅用于编译时选用,勿打包到apk文件中) 4、testSM.java -- SM2、SM3、SM4 算法相关类引用范例 5、testCERT.java -- X.509 数字证书/PKCS#10 证书申请相关类引用范例 6、readme.txt -- 本文 因条件及精力限制,各类、方法的实现未经严格彻底的测试,不宜用于商业用途软件的开发。 如欲将本开发包发布、上传、拷贝、共享等,务必保持其内容完整性(包括本文) 如有需要帮助或者索取源码,请联系 suntongo@qq.com, suntongo@hotmail.com
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值