SpringBoot之spi思想


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的步骤顺序依次是:

  1. 通过SPI方式,读取 META-INF/services 下文件中的类名,使用TCCL加载;
  2. 通过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-paywx-pay。最后写一个测试模块 test 依赖 api-pay,然后再依赖zfb-paywx-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容器中


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
Go语言的设计哲学 Go语言的设计哲学
学习任何一门新语言都需要了解它的设计哲学,这样在写代码时才能理解为什么它要这么做,也不会因为被其他语言的语法干扰到新语言的学习。实际上很多语言的设计思想都来源于现实生活的启发,只不过它们侧重点不一样,例如Java的面向对象编程思想,Java
2021-02-01
Next 
Netty的线程模型 Netty的线程模型
Netty是一个异步、基于事件驱动的网络应用程序框架,其对 Java NIO进行了封装,大大简化了 TCP 或者 UDP 服务器的网络编程。它的设计参考了许多协议的实现,比如 FTP,SMTP,HTTP 和各种二进制和基于文本的传统协议,因
2021-01-10
  TOC