SPI的全称是Service Provider Interface,即服务提供接口。在SpringBoot中得到了广发应用,如自动装配,实现第三方组件热插拔的核心机制就是spi思想。那么spi解决了什么问题,我们都知道,我们在开发时提倡面向接口开发,在运行期动态指定接口的具体的实现。在Java中,我们可以用反射机制来实现动态指定服务的具体提供者,从而达到解耦的目的(不在代码中写死,而是通过配置项指定使用接口的哪一个实现类,从而达到在不修改代码的前提下,实现具体实现类的选择)。但这会产生一个问题,就是当实现类是由不同厂商实现时该怎么发现实现类
比如JDK提供的数据库的JDBC接口规范,厂商Mysql和Oracle在提供实现时都实现了JDBC接口,那身为开发者的我在使用Mysql时,如何知道JDBC的接口实现类呢,在使用Oracle又该使用哪个实现类呢?当然你可以在使用的时候翻一下源码,再决定使用哪个实现类,但这太麻烦了,也不符合可插拔的理念
这时就需要用到SPI了,它主要是应用于厂商自定义组件或插件中,我们知道IDEA开发工具是用Java开发的,IDEA有许多强大的插件,这些插件实际上都是基于SPI思想开发的,使用的时候安装一下,不使用的时候卸载就行了。不会影响软件的整体运行。
SPI机制简介
SPI的全名为Service Provider Interface,简单来说就是一种服务发现机制。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
SPI具体约定
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/
目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader
。
SPI使用案例
日志组件
SPI最高并不是在SpringBoot中使用,而是在日志组件中,common-logging apache最早提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现, 发现日志提供商是通过扫描 META-INF/services/org.apache.commons.logging.LogFactory
配置文件,通过读取该文件的内容找到日志提工商实现类。只要我们的日志实现里包含了这个文件,并在文件里制定 LogFactory工厂接口的实现类即可。源码如下
public abstract class LogFactory {
public static final String HASHTABLE_IMPLEMENTATION_PROPERTY="org.apache.commons.logging.LogFactory.HashtableImpl";
private static final String WEAK_HASHTABLE_CLASSNAME = "org.apache.commons.logging.impl.WeakHashtable";
public static final String FACTORY_PROPERTIES = "commons-logging.properties";
public static final String FACTORY_PROPERTY = "org.apache.commons.logging.LogFactory";
public static final String FACTORY_DEFAULT = "org.apache.commons.logging.impl.LogFactoryImpl";
//LogFactory静态代码块:
static {
//获取LogFactory类加载器:AppClassLoader
thisClassLoader = getClassLoader(LogFactory.class);
String classLoaderName;
try {
ClassLoader classLoader = thisClassLoader;
if (thisClassLoader == null) {
classLoaderName = "BOOTLOADER";
} else {
//获取classLoader的名称:sun.misc.Launcher$AppClassLoader@150838093
classLoaderName = objectId(classLoader);
}
} catch (SecurityException e) {
classLoaderName = "UNKNOWN";
}
diagnosticPrefix = "[LogFactory from " + classLoaderName + "] ";
diagnosticsStream = initDiagnostics();
logClassLoaderEnvironment(LogFactory.class);
//创建存放日志的工厂缓存对象:实际为org.apache.commons.logging.impl.WeakHashtable
factories = createFactoryStore();
if (isDiagnosticsEnabled()) {
logDiagnostic("BOOTSTRAP COMPLETED");
}
}
//获取日志对象:
public static Log getLog(Class clazz) throws LogConfigurationException {
//得到LogFactoryImpl日志工厂后,实例化具体的日志对象:
return getFactory().getInstance(clazz);
}
//获取日志工厂
public static LogFactory getFactory() throws LogConfigurationException {
//获取当前线程的classCloader:
ClassLoader contextClassLoader = getContextClassLoaderInternal();
if (contextClassLoader == null) {
.....
}
//从缓存中获取LogFactory:此缓存就是刚才在静态代码块中创建的WeakHashtable
LogFactory factory = getCachedFactory(contextClassLoader);
//如果存在就返回:
if (factory != null) {
return factory;
}
if (isDiagnosticsEnabled()) {
......
}
//读取classpath下的commons-logging.properties文件:
Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);
ClassLoader baseClassLoader = contextClassLoader;
if (props != null) {
//如果Properties对象不为空,从中获取 TCCL_KEY 的值:
String useTCCLStr = props.getProperty(TCCL_KEY);
if (useTCCLStr != null) {
if (Boolean.valueOf(useTCCLStr).booleanValue() == false) {
baseClassLoader = thisClassLoader;
}
}
}
.....
try {
/从系统属性中获取 FACTORY_PROPERTY 的值:
String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
if (factoryClass != null) {
//如果该值不为空,则实例化日志工厂对象:
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
} else {
.....
}
} catch (SecurityException e) {
.....
}
if (factory == null) {
if (isDiagnosticsEnabled()) {
....
}
try {
//如果日志工厂对象还为null,则从 META-INF/services/org.apache.commons.logging.LogFactory 中获取:
final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
if( is != null ) {
.....
}
} catch (Exception ex) {
......
}
}
if (factory == null) {
if (props != null) {
//如果此时日志工厂为null,并props有值,则获取 FACTORY_PROPERTY 为key的值:
String factoryClass = props.getProperty(FACTORY_PROPERTY);
if (factoryClass != null) {
if (isDiagnosticsEnabled()) {
logDiagnostic(
"[LOOKUP] Properties file specifies LogFactory subclass '" + factoryClass + "'");
}
//如果我们配置了,则实例化我们配置的日志工厂对象
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
} else {
.....
}
} else {
......
}
}
if (factory == null) {
if (isDiagnosticsEnabled()) {
.....
}
//如果此时日志工厂依旧为null,则实例化默认工厂:org.apache.commons.logging.impl.LogFactoryImpl
factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);
}
if (factory != null) {
// 将日志工厂添加到缓存当中:
cacheFactory(contextClassLoader, factory);
if (props != null) {
Enumeration names = props.propertyNames();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
String value = props.getProperty(name);
factory.setAttribute(name, value);
}
}
}
return factory;
}
//创建存放日志的工厂缓存对象:
private static final Hashtable createFactoryStore() {
Hashtable result = null;
String storeImplementationClass;
try {
//从系统属性中获取 HASHTABLE_IMPLEMENTATION_PROPERTY 为key的值:
storeImplementationClass = getSystemProperty(HASHTABLE_IMPLEMENTATION_PROPERTY, null);
} catch (SecurityException ex) {
storeImplementationClass = null;
}
//如果 storeImplementationClass 为null
if (storeImplementationClass == null) {
//将 WEAK_HASHTABLE_CLASSNAME 赋值给storeImplementationClass字符串
storeImplementationClass = WEAK_HASHTABLE_CLASSNAME;
}
try {
//反射实例化缓存对象:org.apache.commons.logging.impl.WeakHashtable
Class implementationClass = Class.forName(storeImplementationClass);
result = (Hashtable) implementationClass.newInstance();
} catch (Throwable t) {
.....
}
if (result == null) {
result = new Hashtable();
}
return result;
}
}
Commons Logging 从 JDK 1.3 版本开始提供的服务发现机制,扫描类路径下的 META-INF/services/org.apache.commons.logging.LogFactory文件,如果找到,则装载其中的配置,并使用其中的配置来加载日志工厂,关键代码在上述源码中的第90行,使用了 contextClassLoader
加载实现类,它是一个线程上下文类加载器,它实际上破坏了双亲委派模型,至于为什么要破坏双亲委派,后面再说
JDBC组件
我们先来看平时是如何使用mysql获取数据库连接的:
// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类
// Class.forName("com.mysql.jdbc.Driver").newInstance();
String url = "jdbc:mysql://localhost:3306/testdb";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
这是mysql注册驱动及获取connection的过程,各位可以发现经常写的Class.forName
被注释掉了,但依然可以正常运行,这是为什么呢?这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。那到底是在哪一步自动注册了mysql driver的呢?重点就在DriverManager.getConnection()
中。我们都是知道调用类的静态方法会初始化该类,进而执行其静态代码块,DriverManager的静态代码块就是:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
继续看初始化方法loadInitialDrivers()
的代码如下:
private static void loadInitialDrivers() {
String drivers;
try {
// 先读取系统属性
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 通过SPI加载驱动类
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// 继续加载系统属性中的驱动类
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);
// 使用AppClassloader加载
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
从上面可以看出JDBC中的DriverManager
的加载Driver的步骤顺序依次是:
- 通过SPI方式,读取 META-INF/services 下文件中的类名,使用TCCL加载;
- 通过
System.getProperty("jdbc.drivers")
获取设置,然后通过系统类加载器加载。
破坏双亲委派
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(Application ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,Bootstrap Classloader无法委派Application ClassLoader来加载类。而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。
不要觉得破坏了双亲委派模型一定不好,会造成坏的影响,实际上很多开源中间件都破坏了双亲委派模型,比如Tomcat的隔离机制,OSGI模块化思想都是在破坏双亲委派模型的基础上才能实现的。JDK在1.3后提供了 java.util.ServiceLoader
来实现SPI机制
实现SPI的Demo
说了这么多,来写个小例子会更好理解。假设我现在要提供一个公共的支付模块 api-pay
。对于这个支付模块,我只提供支付的接口规范和api,至于以何种方式实现,让具体的厂商去做。然后再提供主流的支付方式模块 zfb-pay
和wx-pay
。最后写一个测试模块 test
依赖 api-pay
,然后再依赖zfb-pay
和wx-pay
中的一个,要求当无需修改代码的情况下只依赖zfb-pay
模块时自动调用支付宝的支付,当只依赖wx-pay
自动调用微信的支付。
api-pay模块
接口模块定义支付接口,和调用接口的API
PayService接口
package com.example;
/**
* @Auther: Lushunjian
* @Date: 2021/1/12 21:51
* @Description:
*/
public interface PayService {
void pay();
}
DealService提供API调用方法
package com.example;
import java.util.ServiceLoader;
/**
* @Auther: Lushunjian
* @Date: 2021/1/12 21:58
* @Description:
*/
public class DealService {
public void doDeal(){
//通过load方法新建对象
ServiceLoader<PayService> serviceLoader = ServiceLoader.load(PayService.class);
for (PayService payService : serviceLoader) {
payService.pay();
}
}
}
zfb-pay模块
ZfbPayService实现支付接口
package com.example;
/**
* @Auther: Lushunjian
* @Date: 2021/1/12 22:01
* @Description:
*/
public class ZfbPayService implements PayService{
public void pay() {
System.out.println("----zfb pay----");
}
}
在resources目录下创建 META-INF/service目录,新建 PayService 包名全路径的文件,在文件中加入它的实现类全路径
wx-pay模块
WxPayService实现支付接口
package com.example;
/**
* @Auther: Lushunjian
* @Date: 2021/1/12 22:01
* @Description:
*/
public class WxPayService implements PayService{
public void pay() {
System.out.println("----wx pay----");
}
}
在resources目录下创建 META-INF/service目录,新建 PayService 包名全路径的文件,在文件中加入它的实现类全路径
test模块
在测试模块中依三个模块,其中api-pay是必须要依赖的,因为它是提供接口API的
在测试类中调用支付的API,结果如下
因为两个支付模块的实现都依赖了,所以调用了微信支付接口,也调用了支付宝的支付接口,现在把微信的支付模块移除掉
再运行,结果如下
只打印了支付宝的调用,这样就实现了增加依赖自动发现的功能,而无需修改代码。
springboot中的类SPI扩展机制
最后到SpringBoot的自动配置原理了,通过前面的铺垫,对于SpringBoot的自动配置也不难理解了,在springboot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories文件的格式为:key=value1,value2,value3
// 从所有的jar包中找到META-INF/spring.factories文件
// 然后从文件中解析出key=factoryClass类名称的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
// 取得资源文件的URL
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
// 遍历所有的URL
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
// 组装数据,并返回
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}
在每个SpringBoot的Starter中都可以找到spring.factories文件,里面配置了自动配置需要加载的实现类,在SpringBoot启动的过程中,做的最重要的事情就是找到所有的spring.factories文件,把其中的配置类加载到IOC容器中