SpringBoot自动装配原理分析

一、SPI源码分析

为什么要讲SPI呢?因为在SpringBoot的自动装配中其实有使用到SPI机制,所以掌握了这部分对于SpringBoot的学习还是很有帮助的。

SPI,全称为Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。我们先通过一个很简单的例子来看下它是怎么用的。

源码分析

关键组件解析

  1. ServiceLoader.load()方法:

    scss 复制代码
    java
    复制
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    • 核心方法,负责加载指定接口的所有实现
    • 默认使用当前线程的上下文类加载器
    • 内部维护了一个缓存机制,避免重复加载
  2. providers.iterator()​ :

    返回一个迭代器,该迭代器封装了底层的服务提供者发现和实例化逻辑。

  3. 内部工作机制:

    • ServiceLoader在类路径下查找META-INF/services/[全限定接口名]文件
    • 解析该文件中列出的实现类全限定名
    • 使用无参构造函数实例化这些实现类
    • 按顺序返回这些实例

工作流程

  1. JVM加载主类并执行main方法
  2. 调用ServiceLoader.load(BaseData.class)开始服务发现过程
  3. ServiceLoader在类路径中搜索META-INF/services/[BaseData的全限定名]文件
  4. 解析文件中列出的所有实现类
  5. 实例化每个实现类并通过迭代器提供访问
  6. 遍历迭代器,对每个服务提供者调用baseURL()方法

注意事项

  • 代码中没有处理baseURL()的返回值
  • 缺少异常处理机制
  • ServiceLoader的实例化是懒加载的,只有在迭代时才会创建实例
  • ServiceLoader使用无参构造函数实例化服务提供者,因此实现类必须有无参构造函数

这种模式是Java中实现可插拔架构的标准方式,允许第三方为应用程序提供功能扩展,而不需要修改核心代码。

自动装配源码分析

在前面的分析中,Spring Framework一直在致力于解决一个问题,就是如何让bean的管理变得更简单,如何让开发者尽可能的少关注一些基础化的bean的配置,从而实现自动装配。所以,所谓的自动装配,实际上就是如何自动将bean装载到ioc容器中来。

实际上在spring 3.x版本中,Enable模块驱动注解的出现,已经有了一定的自动装配的雏形,而真正能够实现这一机制,还是在spring 4.x版本中,conditional条件注解的出现。ok,我们来看一下spring boot的自动装配是怎么回事。

自动装配的演示

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yaml 复制代码
spring:
  redis:
    host: 127.0.0.1
    port: 6379
typescript 复制代码
@Autowired
private RedisTemplate<String,String> redisTemplate;

按照下面的顺序添加starter,然后添加配置,使用RedisTemplate就可以使用了?那大家想没想过一个问题,为什么RedisTemplate可以被直接注入?它是什么时候加入到ioc容器的呢?这就是自动装配。自动装配可以使得classpath下依赖的包相关的bean,被自动装载到Spring ioc容器中,怎么做到的呢?

ImportBeanDefinitionRegistrar 与 ImportSelector 核心区别

  • ImportSelector

    通过返回类名数组间接导入配置类,Spring 后续会自动处理这些类的加载和 Bean 注册 适用场景:批量导入配置类(如 Spring Boot 自动配置)。

  • ImportBeanDefinitionRegistrar

    直接通过代码向容器注册 Bean 定义,支持动态生成 Bean 属性甚至覆盖现有定义 适用场景:需要精细控制 Bean 定义的场景(如动态数据源注册)。

SpringBoot自动装配机制

启动类注解 @SpringBootApplication

@EnableAutoConfiguration

@Import({AutoConfigurationImportSelector.class})

Springboot @Enable*注解的工作原理:ImportSelector接口selectImports返回的数组(类的全类名)都会被加载到spring容器中。

kotlin 复制代码
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    } else {
    //获取所有候选配置类EnableAutoConfiguration
        AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
}
kotlin 复制代码
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    } else {
//获取元注解中的属性
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//使用SpringFactoriesLoader加载ClassPath路径下META-INF\spring.factories中
//key=org.springframework.boot.autoconfigure.EnableAutoConfiguration
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//去重
        configurations = this.<String>removeDuplicates(configurations);
//应用exclusion属性
        Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
        this.checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
//过滤,检查候选配置类上的注解@ConditionalOnClass,如果要求的类不存在,则这个候选类会被过滤不被加载
        configurations = this.getConfigurationClassFilter().filter(configurations);
//广播事件
        this.fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationEntry(configurations, exclusions);
    }
}

本质上来说,其实EnableAutoConfiguration会帮助SpringBoot应用把所有符合@Configuration配置都加载到当前SpringBoot创建的IoC容器,而这里面借助了Spring框架提供的一个工具类SpringFactoriesLoader的支持。以及用到了Spring提供的条件注解@Conditional,选择性的针对需要加载的bean进行条件过滤。

假设我们有一个自定义的 Redis Starter,希望 ​仅在项目中存在 RedisTemplate 类时 ​ 自动配置 Redis 客户端。此时可以通过 spring-autoconfigure-metadata.properties 定义条件元数据。


步骤 1:创建配置类
java 复制代码
java
复制
// src/main/java/com/example/RedisAutoConfig.java
@Configuration
@ConditionalOnClass(RedisTemplate.class) // 条件注解
public class RedisAutoConfig {

    @Bean
    public RedisClient redisClient() {
        return new RedisClient("localhost", 6379);
    }
}

步骤 2:生成条件元数据

编译项目时,spring-boot-autoconfigure-processor 会自动解析 @ConditionalOnClass 注解,并在生成的 spring-autoconfigure-metadata.properties 中添加:

ini 复制代码
properties
复制
# 自动生成的内容(位于编译后的 JAR 包的 META-INF 目录)
com.example.RedisAutoConfig=org.springframework.boot.autoconfigure.condition.ConditionalOnClass=org.springframework.data.redis.core.RedisTemplate

步骤 3:验证条件过滤
  • 情况 1:项目中引入了 Redis 依赖
    类路径存在 RedisTemplate → 配置类生效,RedisClient Bean 被注册。
  • 情况 2:未引入 Redis 依赖
    类路径无 RedisTemplate → 配置类被过滤,RedisClient Bean 不存在。

手动定义元数据(可选)

若需自定义条件(如不依赖注解),可直接在 spring-autoconfigure-metadata.properties 中添加:

ini 复制代码
properties
复制
com.example.RedisAutoConfig=org.springframework.boot.autoconfigure.condition.ConditionalOnClass=org.springframework.data.redis.core.RedisTemplate

关键点解析
  1. 条件表达式格式
    类名.条件类型=条件值,其中:

    • 条件类型:如 ConditionalOnClassConditionalOnProperty
    • 条件值:具体条件参数(如类名、属性键值对)。
  2. 自动与手动结合

    • 推荐通过注解自动生成元数据,减少手动维护成本。
    • 手动定义适用于复杂条件或旧代码兼容。

调试验证

在启动日志中观察以下内容,确认条件是否生效:

ini 复制代码
log
复制
DEBUG AutoConfigurationMetadataLoader - Loading metadata from [META-INF/spring-autoconfigure-metadata.properties]
DEBUG AutoConfigurationImportFilter - Condition evaluation: com.example.RedisAutoConfig (ConditionalOnClass=RedisTemplate) → true

总结

通过 spring-autoconfigure-metadata.properties,Spring Boot 实现了 ​按条件动态加载配置类​ 的核心逻辑。开发者只需关注条件本身,无需手动实现复杂的过滤代码。

相关推荐
Re27511 分钟前
我用4碗面讲清HTTP的四大请求方法:GET/POST/PUT/DELETE
后端
有梦想的攻城狮18 分钟前
spring中的ApplicationRunner接口详解
java·后端·spring·runner·application
程序视点18 分钟前
设计模式之原型模式!附Java代码示例!
java·后端·设计模式
用户21411832636021 小时前
AI 驱动开发:20 分钟搞定智能发票申请单系统
后端
G探险者1 小时前
Java 中 null 值在 JSON 输出时丢失的坑:一次 Object 参数 + Fastjson 多态的血泪教训
后端
振鹏Dong1 小时前
微服务架构及常见微服务技术栈
java·后端
程序员爱钓鱼2 小时前
Go语言实战案例:简易JSON数据返回
后端·go·trae
程序员爱钓鱼2 小时前
Go语言实战案例:用net/http构建一个RESTful API
后端·go·trae
bobz9653 小时前
firewalld 添加 nat 转发
后端
苇柠4 小时前
Spring框架基础(1)
java·后端·spring