聊聊JAVA的SPI机制

  一、SPI简介    

SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制。SPI常用于框架的功能拓展,最典型的应用就是Spring对数据库驱动的加载、SpringBoot自动配置的实现以及Dubbo插件可拔插实现。

        SPI说到底还是接口,但是与传统意义上的接口由有所区别。传统的接口由服务提供者制定并实现接口,供消费者调用,而SPI是自己制定接口自己消费服务,而实现则交给第三方。SPI有点像发布需求招标的过程,我把需求规格、标准发布出来,大家带着实现方案来投标,谁的方案好我就用谁的。

二、原生SPI开发流程

     

三、SPI的实现案例

项目的结构如下图,是一个Nacos为注册中心的微服务Demo项目,大家测试不需要这么复杂的项目,我这里是为了探索SpringBoot自动装配SPI顺手用了以前的微服务Demo。

common模块就包含很简单一个接口,这个模块会被接口实现方和调用方引入。

package com.wen;

public interface DbType {
    String getName();
}

MysqlSPI模块仅包含DbType接口的实现

package com.wen;


public class MysqlDbType implements DbType {
    @Override
    public String getName() {
        return "Mysql";
    }
}
discovery-client模块需要引入common模块和MysqlSPI,以及配置SPI的映射文件,映射文件名放在resources/META-INF/services下,文件内容就是MysqlSPI中DbTy实现类的全限定名

测试类

import com.wen.DbType;
import com.wen.client.NacosClientApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.ServiceLoader;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = NacosClientApplication.class)
public class Example {


    @Test
    public void example01() {
        ServiceLoader<DbType> serviceLoader = ServiceLoader.load(DbType.class);
        System.out.println("JDK 原生SPI,会缓存实例到Map中");
        for (DbType search : serviceLoader) {
           System.out.println(search.getName());
     }
}

测试结果如下,我们成功拿到了,接口的实例,并执行了方法

四、SPI的实现源码探索

我们就从测试类中使用的ServiceLoader入手。ServiceLoader实现了Iterable,并且声明一个LazyIterator类型的变量lookupIterator,LazyIterator类是ServiceLoader的一个内部类,看名字就不难猜到他也实现了迭代器接口,不过它实现的是Iteratorj接口。注意图中的常量PREFIX,这就是为啥映射要配在这里的原因。下面还有的Map是为了缓存实例。

从ServiceLoader的load方法开始,不太重要的过程我们直接跳过。load走完其实啥也没干,仅仅就完成了ServiceLoader类的初始化。真正的接口实现的实例化是在迭代器遍历的过程中完成的。

我们对load的结果遍历,调用ServiceLoader的iterator方法生成一个迭代器实例,遍历过程会调用该迭代器的hasNext和Next方法,这里都是先从缓存读取,没取到就利用lookupIterator这个迭代器,临时去查找接口实现,并实例化。

LazyIterator的hasNext和next方法最后又调用了他的实例方法hasNextService和nextService

这里要重点关注了,真正干活就在这里

这个service是啥,前面忘了介绍没关系,就是他,也就是接口类的实例。

前面已经拿到了实现类的全限定名,这里最后一步,利用JAVA的反射机制进行实例化,并缓存。

到这里整个过程应该是相当清楚了。这里原生SPI的缺点也是很明显的,每定义一个接口我必须新增一个映射配置文件,另外如果一个接口有多个实现,我无法直接拿到我想要的那个,需要去循环。

五、Spring SPI实现

代码上有一点点改变,映射文件统一为spring.factories,文件内容是键值对,前面是接口全限定名,后面是接口实现的全限定名,多个实现以“,”分隔,多个接口换行符分隔。

测试代码需要调整一下,主要干活的类是SpringFactoriesLoader类

import com.wen.DbType;
import com.wen.client.NacosClientApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = NacosClientApplication.class)
public class Example {


    @Test
    public void example01() {
       List<DbType> dbTypeList= SpringFactoriesLoader.loadFactories(DbType.class, null);
        System.out.println("Spring SPI,非自动配置方式,实例不会缓存,更不会托管给Spring容器");
        for (DbType dbType:dbTypeList){
            System.out.println(dbType.getName());
        }
    }
}

看看测试结果

代码逻辑很清晰明了

我们先看下结果,缺点很明显,实例没有缓存,每次调用都要重新解析,实例化,完全没有体现出Spring应有的优势。

六、Spring SPI自动装配实现

前面我们介绍了Spring的SPI很朴素的实现方式,确实有些鸡肋。那么我们更进一步,试试自动配置。项目代码小调整一下,MysqlSPI下新加一个自动配置类

package com.wen;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;


@Configuration
public class DbTypeAutoConfiguration {
    @Bean(name = "mysql")
    @Primary
    public DbType dbType(){
        return () -> "Mysql";
    }

    @Bean(name = "mongodb")
    public DbType mongodb(){
        return () -> "Mongodb";
    }
}

映射配置文件需要调整一下,key修改成org.springframework.boot.autoconfigure.EnableAutoConfiguration

值改为刚刚修改的自动配置类的全限定名  com.wen.DbTypeAutoConfiguration

测试类变得更简单了,用例通过,说明自动装配是成功了。这才是Spring该有的样子。

七、既然到SPI的自动配置方式,SpringBoot的自动装配也简单聊下。

1、核心配置

@EnableAutoConfiguration 这是实现自动配置关键注解,通常在启动类要开启,因为启动类一般添加了@SpringBootApplication配置,而@SpringBootApplication注解实现使用了元注解@EnableAutoConfiguration,所以这两个开关并不需要我们手动打开。这两个注解只是开关,实际上干活的缺失AutoConfigurationImportSelector这个类,这个类主要负责处理条件注入、去重、过滤以及被注入类的搜索定位等前置工作,搜索定位还是依靠前面提到过SpringFactoriesLoader类

2、注入过程。我们主要关注三个问题点,入口在哪,如何定位目标,何时初始化

1)入口:依然是我们的启动类,最终到AbstractApplicationContext类的refresh方法,跟过代码就会知道,这里是Spring容器初始化组件的最核心部分。

2)、如何定位目标:定位spring.factories目标,下面是定位SPI映射文件中自动配置类途径的类和方法,过程有点长,不上图了。调用链路在下面表格顺序从上至下。

类名方法
AbstractApplicationContextpublic void refresh()
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory)
PostProcessorRegistrationDelegate
public static void invokeBeanFactoryPostProcessors(
       ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors)
ConfigurationClassPostProcessor
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) 
ConfigurationClassParser
public void parse(Set<BeanDefinitionHolder> configCandidates)
private void processDeferredImportSelectors() 
 
内部类DeferredImportSelectorGrouping
public Iterable<Group.Entry> getImports()
public void process(AnnotationMetadata annotationMetadata,
       DeferredImportSelector deferredImportSelector)
AutoConfigurationImportSelector
public void process(AnnotationMetadata annotationMetadata,
       DeferredImportSelector deferredImportSelector) 
public String[] selectImports(AnnotationMetadata annotationMetadata)
SpringFactoriesLoader
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) 

3)、何时初始化:上一步只是解析并缓存了各种BeanDefinition文件,真正实例化是AbstractApplicationContext的finishBeanFactoryInitialization方法

接着调用DefaultListableBeanFactory的preInstantiateSingletons方法,后面的过程就很简单了。

好了,篇幅有点长,可能略显啰嗦,但是本着把细节讲清楚为目标,我也就不嫌麻烦了,希望大家能有所收获。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值