SpringBoot(二):springboot自动装配之SPI机制

简介: 上篇文章我们介绍了springboot启动过程中涉及的核心类及其功能,我们知道springboot相较于spring的一大特性就是**自动装配**,那么自动装配是怎么具体实现的呢?其实在实现自动装配上springboot采用了多种方案结合的,比如基于spring的扩展点的自动属性注入等,还有提供了一套SPI机制让程序自动可插拔的装配。本文我带大家重点 了解一下**SPI机制**的实现原理。

SpringBoot(二):springboot自动装配之SPI机制

上篇文章我们介绍了springboot启动过程中涉及的核心类及其功能,我们知道springboot相较于spring的一大特性就是自动装配,那么自动装配是怎么具体实现的呢?
其实在实现自动装配上springboot采用了多种方案结合的,比如基于spring的扩展点的自动属性注入等,还有提供了一套SPI机制让程序自动可插拔的装配。
本文我带大家重点 了解一下SPI机制的实现原理。

1 什么是SPI?

SPI(Service Provider Interface)机制是一种服务发现和加载机制。它允许开发者编写一个服务接口,然后通过在项目中使用服务提供者实现该接口的方式,实现对应的服务功能。

1.1 JDK中的SPI

JDK的SPI机制通过在Classpath中的META-INF/services目录下,创建以服务接口全限定名命名的文件,
文件的内容为实现该接口的具体实现类。当应用程序需要使用该服务时,JDK会自动加载并实例化配置文件中列出的实现类,并提供给应用程序使用。

1.2 Springboot中的SPI

在Spring Boot中,SPI机制允许开发者通过定义接口和实现类的方式,实现对应的功能扩展。通过在META-INF/spring.factories配置文件中列出实现类,
Spring Boot能够自动加载并使用这些扩展点,提供了灵活的定制和扩展能力。

Tip:我本人在多年的开发经验中也积累了很多通用的starter,比如说通过引入我的starter修改配置文件就可以快速搭建一个web资源认证授权服务器/客户端,
当然这个也是在spring-security+oauth2上进一步的封装,需要了解的可以去Git自己获取

// Git代码
https://gitee.com/yeeevip/yeee-memo/tree/master/memo-parent/memo-common/common-auth/common-platform-auth-server
https://github.com/yeeevip/yeee-memo/tree/master/memo-parent/memo-common/common-auth/common-platform-auth-server

2 Springboot的SPI机制是怎么实现的?

2.1 简短概括

  1. 程序启动,注册配置类处理器
  2. spring刷新上下文,执行配置类处理器
  3. 扫描spring.factories将得到的BeanDefinition注册到容器
  4. spring实例化/初始化这些BeanDefinition

2.2 源码跟踪

  1. 启动SpringBoot程序,创建应用上下文ApplicationContext

我们会调用SpringApplication.run启动程序,通常情况下我们会在程序的主启动类上加一个 @SpringBootApplication 注解,追踪这个注解会发现它是一个复杂的聚合注解,
其中内部包含了@Configuration、 @EnableAutoConfiguration ,而 @EnableAutoConfiguration 有集成了 @Import 注解,这个注解对于springboot非常重要!

// 用户定义的程序主启动类,启动程序
@SpringBootApplication
public class SpringbootExampleApplication {
   
    public static void main(String[] args) {
   
        SpringApplication.run(SpringbootExampleApplication.class, args);
    }
}
// Springboot的主启动类,创建spring上下文
public class SpringApplication {
   
    public ConfigurableApplicationContext run(String... args) {
   
        ConfigurableApplicationContext context = null;
        try {
   
            ...
            context = createApplicationContext();
            ...
        } catch (Throwable ex) {
   
            ...
        }
    }
}
  1. 注册BeanFactory后置处理器用于处理配置类上的注解

springboot启动创建应用上下文后,会注册一系列的内置的后置处理器,如ConfigurationClassPostProcessor、AutowiredAnnotationBeanPostProcessor、
CommonAnnotationBeanPostProcessor,其中 ConfigurationClassPostProcessor 实现了BeanFactoryPostProcessor是一个BeanFactory级别的处理器用于处理配置类

注册PostProcessor代码如下:

public class AnnotatedBeanDefinitionReader {
   
    public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
   
        ...
        // 注册各种后置处理器
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    } 
}
// 注册各种后置处理器
public abstract class AnnotationConfigUtils {
   
    public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(BeanDefinitionRegistry registry, @Nullable Object source) {
   
        ...
        if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
   
            // 注册<配置类>处理器
            RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
            def.setSource(source);
            beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
        }
        ...
        return beanDefs;
    }
}
  1. 刷新应用上下文,执行注册的后置处理器

springboot执行refreshContext刷新上下文,本质还是spring上下文的refresh方法,这个方法是spring生命周期的关键核心代码,
spring生命周期的大部分扩展点都是在这里执行的。其中在执行容器中未实例化初始化的bean定义之前会执行内置的、用户自定义的所有注册的beanFactory后置处理器
,这里会执行到ConfigurationClassPostProcessor这个配置类处理器

Tip:关于Spring的扩展点大概有13个,其中包括一些后置处理器,具体怎么使用需要了解的可以去Git自己获取,这里不再细说

// Git代码
https://gitee.com/yeeevip/yeee-memo/tree/master/learn-example/spring-boot-example/src/main/java/vip/yeee/memo/demo/springboot/extpoint
https://github.com/yeeevip/yeee-memo/tree/master/learn-example/spring-boot-example/src/main/java/vip/yeee/memo/demo/springboot/extpoint

刷新上下文代码如下:

// Springboot调用refreshContext来执行父类的refresh方法
public class SpringApplication {
   
    public ConfigurableApplicationContext run(String... args) {
   
        ...
        try {
   
            ...
            // 刷新上下文
            refreshContext(context);
            ...
        } catch (Throwable ex) {
   
            throw new IllegalStateException(ex);
        }
        ...
        return context;
    }
}
// 调用spring的AbstractApplicationContext的refresh()方法刷新
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
   
    public void refresh() throws BeansException, IllegalStateException {
   
        ...
        try {
   
            ...
            // 执行所有注册的BeanFactory后置处理器
            invokeBeanFactoryPostProcessors(beanFactory);
            ...
            // 实例化、初始化所有的单例Bean
            finishBeanFactoryInitialization(beanFactory);
        } catch (BeansException ex) {
   
            ...
        }
    }
}
  1. 执行配置类后置处理器后置处理器(SPI核心逻辑

spring在执行到BeanFactory处理器的生命周期时会执行到ConfigurationClassPostProcessor这个处理器,
执行具体处理逻辑,如配置类上的@PropertySource、@ComponentScan、@Import、@ImportResource、@Bean等注解都会有相应的处理,目的就是将定义的BeanDefinition注册到BeanFactory容器中以便于最终的初始化。

由于用@SpringBootApplication这个注解标记的类即SpringbootExampleApplication.class本身也是个配置类,
这里的话就会处理这个主启动类。

处理配置类的代码:

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor {
   
    public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
   
        // 这里会有我们程序的主启动配置类,也就是SpringbootExampleApplication.class
        List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
        ...
        // 转换每个配置类
        ConfigurationClassParser parser = new ConfigurationClassParser();
        ...
        do {
   
            ...
            // 处理配置类
            parser.parse(candidates);
            ...
        } while (!candidates.isEmpty());
    }
}
// 调用parse方法后会执行到doProcessConfigurationClass
class ConfigurationClassParser {
   
    protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) {
   
        // 处理配置类的@PropertySource注解
        ...
        processPropertySource(propertySource);
        ...
        // 处理配置类的@ComponentScan注解
        ...
        Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
        ...
        // 处理配置类的@Import注解,这里就是SPI机制核心实现逻辑了
        processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
        // 处理配置类的@ImportResource注解
        ...
        String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
        configClass.addImportedResource(resolvedResource, readerClass);
        ...
        // 处理配置类的@Bean注解
        ...
        Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
        ...
    }
}

processImports这个方法会专门处理 @Import 这个注解,
进入方法发现会执行getImports方法获得@Import导入的类AutoConfigurationImportSelector.class,由于它实现了DeferredImportSelector,所以程序会执行
deferredImportSelectorHandler.handle去处理这个Selector;其实这里也还没有真正开始处理,最终的处理是在调用deferredImportSelectorHandler.process()发起的。

看代码:

class ConfigurationClassParser {
   
    private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates,...) {
   
        ...
        for (SourceClass candidate : importCandidates) {
   
            if (candidate.isAssignable(ImportSelector.class)) {
   
                ...
                // 判断当前的导入的类是不是DeferredImportSelector的子类
                if (selector instanceof DeferredImportSelector) {
   
                    // 是DeferredImportSelector的子类的话由deferredImportSelectorHandler去处理
                    this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
                } 
                ...
            }
            ...
        }
    }
    public void parse(Set<BeanDefinitionHolder> configCandidates) {
   
        ...
        // 这里发起真正处理Selector的逻辑
        this.deferredImportSelectorHandler.process();
    }
}
  • 处理AutoConfigurationImportSelector的具体实现步骤如下:

4.1 读取项目路径下所有依赖中的spring.factories文件,加载需要自动配置的类

跟踪代码发现,执行process()后会执行handler.processGroupImports(),进而会执行grouping.getImports(),这个方法会用类加载器扫码程序classpath下所有jar中的spring.factories
文件,然后会得到所有用EnableAutoConfiguration属性标记的自动配置类

class ConfigurationClassParser {
   
    public Iterable<Group.Entry> getImports() {
   
        for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
   
            // 会扫描jar下的spring.factories中的自动配置类
            this.group.process(deferredImport.getConfigurationClass().getMetadata(),
                    deferredImport.getImportSelector());
        }
        return this.group.selectImports();
    }
}
// 具体扫描spring.factories的实现在AutoConfigurationImportSelector.process
public class AutoConfigurationImportSelector implements DeferredImportSelector... {
   
    @Override
    public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
   
        // getAutoConfigurationEntry这个方法执行
        AutoConfigurationEntry autoConfigurationEntry = (deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);
        ...
    }
    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
   
        ...
        // 这里会调用SpringFactoriesLoader.loadFactoryNames读取spring.factories文件
        List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
        ...
    }
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
   
        // 读取key为EnableAutoConfiguration.class的所有自动配置类
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());
    }
    protected Class<?> getSpringFactoriesLoaderFactoryClass() {
   
        return EnableAutoConfiguration.class;
    }
}

4.2 针对所有扫描到的自动配置类再次执行配置类处理逻辑

上面已经从spring.factories中扫描到了所有配置类,紧接着程序同样会执行配置类处理逻辑,具体处理逻辑同第4节开始,同样也是处理这个配置类中的各种注解/方法,
目的就是为了将这些BeanDefinition加入到spring容器中以便于最终初始化。

4.3 将所有通过配置类处理过符合条件的BeanDefinition注册到spring容器中

执行完针对配置类的处理转换后,也就是执行完parse方法后,会再调用reader.loadBeanDefinitions用来把配置类处理后符合条件的BeanDefinition注册到spring容器

看代码:

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor {
   
    public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
   
        ...
        do {
   
            ...
            // 处理配置类
            parser.parse(candidates);
            ...
            // parse转换处理完配置类后,然后会把所有处理过符合条件的注册到spring容器
            this.reader.loadBeanDefinitions(configClasses);
            ...
        } while (!candidates.isEmpty());
    }
}
  1. 最后就是spring将所有单例BeanDefinition进行实例化、初始化了

这里spring会把通过各种方式注册的单例BeanDefinition,如通过注解@Component、@Service标记的Bean,以及我们本文探讨的重点通过SPI机制的导入的Bean进行最终的实例化/初始化。
具体的处理过程也是spring的核心之一,有兴趣的小伙伴可以看一下源码,这里不详细介绍了。

public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
   
    public void refresh() throws BeansException, IllegalStateException {
   
        ...
        try {
   
            ...
            // 执行所有注册的BeanFactory后置处理器
            invokeBeanFactoryPostProcessors(beanFactory);
            ...
            // 实例化、初始化所有的单例Bean
            finishBeanFactoryInitialization(beanFactory);
        } catch (BeansException ex) {
   
            ...
        }
    }
}

2. 总结

好啦,今天的内容就到这里了,通过以上代码分析,带大家了解了springboot的SPI机制实现的原理,
当然本文分析的可能还是比较片面,毕竟springboot在实现自动装配上还不止是SPI一个方案,它还涉及了很多复杂的处理。

文章中如果有问题疑问欢迎小伙伴们提问或者给我指点一下哦,大家一起探讨一下!

下面这个是我多年经验积累的一套脚手架, 里面集成了各种通用的自定义starter,同时也提供了互联网项目中各种中间件/框架的使用demo,
有兴趣的小伙伴可以git看看哦,并且欢迎大佬们指点!!!

// Git代码
https://gitee.com/yeeevip/yeee-memo
https://github.com/yeeevip/yeee-memo
目录
相关文章
|
1月前
|
架构师 Java 开发者
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
在40岁老架构师尼恩的读者交流群中,近期多位读者成功获得了知名互联网企业的面试机会,如得物、阿里、滴滴等。然而,面对“Spring Boot自动装配机制”等核心面试题,部分读者因准备不足而未能顺利通过。为此,尼恩团队将系统化梳理和总结这一主题,帮助大家全面提升技术水平,让面试官“爱到不能自已”。
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
|
1月前
|
Java 数据库连接 mybatis
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
该文档详细介绍了如何在Springboot Web项目中整合Mybatis,包括添加依赖、使用`@MapperScan`注解配置包扫描路径等步骤。若未使用`@MapperScan`,系统会自动扫描加了`@Mapper`注解的接口;若使用了`@MapperScan`,则按指定路径扫描。文档还深入分析了相关源码,解释了不同情况下的扫描逻辑与优先级,帮助理解Mybatis在Springboot项目中的自动配置机制。
136 0
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
|
3月前
|
Java 数据库 开发者
深入剖析 SpringBoot 的 SPI 机制
【8月更文挑战第10天】在软件开发中,SPI(Service Provider Interface)机制是一种重要的服务发现和加载机制,尤其在构建模块化、可扩展的系统时尤为重要。SpringBoot作为Spring家族的一员,其内置的SPI机制不仅继承了Java SPI的设计思想,还进行了优化和扩展,以适应Spring Boot特有的需求。本文将深入剖析SpringBoot中的SPI机制,揭示其背后的原理与应用。
91 7
|
3月前
|
Java 开发者 Spring
"揭秘SpringBoot魔法SPI机制:一键解锁服务扩展新姿势,让你的应用灵活飞天!"
【8月更文挑战第11天】SPI(Service Provider Interface)是Java的服务提供发现机制,用于运行时动态查找和加载服务实现。SpringBoot在其基础上进行了封装和优化,通过`spring.factories`文件提供更集中的配置方式,便于框架扩展和组件替换。本文通过定义接口`HelloService`及其实现类`HelloServiceImpl`,并在`spring.factories`中配置,结合`SpringFactoriesLoader`加载服务,展示了SpringBoot SPI机制的工作流程和优势。
58 5
|
3月前
|
安全 Java UED
掌握SpringBoot单点登录精髓,单点登录是一种身份认证机制
【8月更文挑战第31天】单点登录(Single Sign-On,简称SSO)是一种身份认证机制,它允许用户只需在多个相互信任的应用系统中登录一次,即可访问所有系统,而无需重复输入用户名和密码。在微服务架构日益盛行的今天,SSO成为提升用户体验和系统安全性的重要手段。本文将详细介绍如何在SpringBoot中实现SSO,并附上示例代码。
80 0
|
3月前
|
消息中间件 Java Kafka
深入SpringBoot的心脏地带:掌握其核心机制的全方位指南
【8月更文挑战第29天】这段内容介绍了在分布式系统中起到异步通信与解耦作用的消息队列,并详细探讨了三种流行的消息队列产品:RabbitMQ、RocketMQ 和 Kafka。RabbitMQ 是一个基于 AMQP 协议的开源消息队列系统,支持多种消息模型,具有高可靠性及稳定性;RocketMQ 则是由阿里巴巴开源的高性能分布式消息队列,支持事务消息等多种特性;而 Kafka 是 LinkedIn 开源的分布式流处理平台,以其高吞吐量和良好的可扩展性著称。文中还提供了使用这三种消息队列产品的示例代码。总之,这三款产品各有优势,适用于不同场景。
17 0
|
3月前
|
消息中间件 Java Kafka
SpringBoot大揭秘:如何轻松掌握其核心机制?
【8月更文挑战第29天】这段内容介绍了在分布式系统中起到异步通信与解耦作用的消息队列,并详细探讨了三种流行的消息队列产品:RabbitMQ、RocketMQ 和 Kafka。RabbitMQ 是一个基于 AMQP 协议的开源消息队列系统,支持多种消息模型,具有高可靠性及稳定性;RocketMQ 则是由阿里巴巴开源的高性能分布式消息队列,支持事务消息等多种特性;而 Kafka 是 LinkedIn 开源的分布式流处理平台,以其高吞吐量和良好的可扩展性著称。文中还提供了使用这三种消息队列产品的示例代码。
16 0
WXM
|
3月前
|
存储 缓存 Java
|
3月前
|
消息中间件 Java Kafka
SpringBoot Kafka SSL接入点PLAIN机制收发消息
SpringBoot Kafka SSL接入点PLAIN机制收发消息
38 0
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
158 1

热门文章

最新文章

下一篇
无影云桌面