SpringBoot源码分析(一)之SPI


文章目录

  • SpringBoot源码分析(一)之SPI
  • 前言
  • 一、SPI项目案例
  • 接口项目-Database
  • 接口实现1-Mysql
  • 接口实现2-Oracle
  • 测试demo
  • 二、源码分析



前言

在我们研究Spring Boot自动装配之前需要先了解一下SPI机制,因为Spring Boot的自动装配使用到了SPI机制来获取依赖中的@Configuration注解的类信息。
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在依赖项目的ClassPath路径下的 META-INF/services 文件夹中查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了
可能,比如在JDBC中使用到了SPI机制,JDBC在获取数据库连接的过程,就提供了一个连接接口,每个数据库厂家可以实现它的连接接口,当JDBC在创建数据库连接的时候,就可以通过各个厂家META-INF/services/下的文件创建实例Bean。

一、SPI项目案例

接口项目-Database

先定义一个接口项目SPIDatabase,创建一个公共接口(BaseData)和方法(baseToString)。该接口将被我们自己创建的Mysql、Oracle项目所实现。

package com.my.spi;

/**
 * 定义SPI的公共接口
 */
public interface BaseData {

    public void baseToString();

}

接口项目的pom.xml中的版本信息如下:

<groupId>com.my</groupId>
<artifactId>SPIDatabase</artifactId>
<version>1.0-SNAPSHOT</version>

最后别忘记了编译打包一下我们的maven项目,此刻我们的接口demo项目创建成功。接下来我们将会通过两个项目分别去实现这个接口。

接口实现1-Mysql

上面我们创建了一个SPI的接口定义项目,接下来,我们需要创建一个Mysql实现上述接口的项目-SPIMysql。
在创建SPIMysql的Maven项目之后,需要将接口项目(SPIDatabase)引入到SPIMysql项目中:

<dependency>
  <groupId>com.my</groupId>
  <artifactId>SPIDatabase</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

在项目中创建BaseData接口的实现类MySQLData,用来实现baseToString方法:

package com.MyMysql.spi.mysql;

import com.my.spi.BaseData;

/**
 * SPI:MySQL对于 baseToString 的一种实现
 */
public class MySQLData implements BaseData {

    @Override
    public void baseToString() {
        System.out.println("[baseToString实现] mysql的扩展实现...");
    }

}

然后在resources目录下创建 META-INF/services 目录,然后在目录中创建一个文件,名称必须是 定义的接口的全类路径名称。然后在文件中写上接口的实现类的全类路径名称。

使用Spring Boot的SPI机制 spring boot spi_sql

SPIMysql项目的版本信息,如下:

<groupId>com.MyMysql</groupId>
<artifactId>SPIMysql</artifactId>
<version>1.0-SNAPSHOT</version>

最后别忘记了编译打包。

接口实现2-Oracle

上面我们创建一个Mysql实现上述接口的项目-SPIMysql。接下来,我们再创建一个Oracle实现接口的项目,创建项目之后,我们也需要将接口项目(SPIDatabase)引入到SPIOracle项目中,代码跟上一步创建中一样,在此就不粘贴了。
在项目中创建BaseData接口的实现类SPIOracle,用来实现baseToString方法:

package com.MyOracle.spi.oracle;

import com.my.spi.BaseData;

/**
 * SPI:MySQL对于 baseToString 的一种实现
 */
public class OracleData implements BaseData {

    @Override
    public void baseToString() {
        System.out.println("[baseToString实现] oracle的扩展实现...");
    }

}

同样在resources目录下创建 META-INF/services 目录,然后在目录中创建一个文件,名称必须是 定义的接口的全类路径名称。然后在文件中写上接口的实现类的全类路径名称。

使用Spring Boot的SPI机制 spring boot spi_使用Spring Boot的SPI机制_02


SPIOracle项目的版本信息,如下:

<groupId>com.MyOracle</groupId>
<artifactId>SPIOracle</artifactId>
<version>1.0-SNAPSHOT</version>

测试demo

首先我们创建一个测试案例,先将SPIMysql的依赖导入。
其次,我们需要创建一个测试类,用来获取接口类BaseData:

package com.my;

import com.my.spi.BaseData;

import java.util.Iterator;
import java.util.ServiceLoader;


public class App 
{
    public static void main( String[] args ) {
        ServiceLoader<BaseData> providers = ServiceLoader.load(BaseData.class);
        Iterator<BaseData> iterator = providers.iterator();
        while(iterator.hasNext()){
            BaseData next = iterator.next();
            next.baseToString();
        }
    }
}

测试结果:

当我们引入SPIMysql的依赖时,执行baseToString方法,将输出[baseToString实现] mysql的扩展实现…

使用Spring Boot的SPI机制 spring boot spi_使用Spring Boot的SPI机制_03


当我们引入SPIOracle的依赖时,执行baseToString方法,将输出[baseToString实现] oracle的扩展实现…

使用Spring Boot的SPI机制 spring boot spi_spring boot_04

二、源码分析

通过上面的案例,我们可以知道SPI是通过load(Class<> service)来加载实现的类。
我们先来看一下ServiceLoader的类结构:

// 配置文件的路径
private static final String PREFIX = "META-INF/services/";
// 加载的服务 类或者接口
private final Class<S> service;
// 类加载器
private final ClassLoader loader;
// 访问权限的上下文对象
private final AccessControlContext acc;
// 保存已经加载的服务类
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 内部类,真正加载服务类
private LazyIterator lookupIterator;

load(Class<> service)方法创建了一些属性,重要的是实例化了内部类,LazyIterator。
其调用ServiceLoader<> load(Class<> service, ClassLoader loader)方法,从而去执行私有构造器ServiceLoader(Class<> svc, ClassLoader cl)。

private ServiceLoader(Class<S> 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();
    }
public void reload() {
	//先清空
	providers.clear();
	//实例化内部类
	LazyIterator lookupIterator = new LazyIterator(service, loader);
}

查找实现类和创建实现类的过程,都在LazyIterator完成。当我们调用iterator.hasNext和 iterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。

private class LazyIterator implements Iterator<S>{
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;
    private boolean hasNextService() {
	    //第二次调用的时候,已经解析完成了,直接返回 
	    if (nextName != null) {
	    	return true;
		}
		if (configs == null) {
			//META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件 
			//META-INF/services/com.my.spi.BaseData
			String fullName = PREFIX + service.getName(); 
			//将文件路径转成URL对象
			configs = loader.getResources(fullName);
		}
		while ((pending == null) || !pending.hasNext()) {
			//解析URL文件对象,读取内容,最后返回
	   	 	pending = parse(service, configs.nextElement());
		}
		//拿到第一个实现类的类名:com.MyOracle.spi.oracle.OracleData
		nextName = pending.next(); 
		return true;
	}
}

创建实例对象,当然,调用next方法的时候,实际调用到的是,lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。

private class LazyIterator implements Iterator<S>{
    private S nextService() {
		//全限定类名
		String cn = nextName;
		nextName = null;
		//创建类的Class对象
		Class<?> c = Class.forName(cn, false, loader); 
		//通过newInstance实例化
		S p = service.cast(c.newInstance()); //放入集合,返回实例
		providers.put(cn, p);
		return p;
	}
}