SpringBoot自动装配原理

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: SpringBoot自动装配原理

前言

我相信,只要你用过Spring Boot,就会对这样一个现象非常的好奇:

引入一个组件依赖,加个配置,这个组件就生效了。

举个例子来说,比如我们常用的Redis, 在Spring Boot中的使用方式是这样的:

1.引入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.编写配置

spring:
  redis:
    database: 0
    timeout: 5000ms
    host: 127.0.0.1
    port: 6379
    password: 123456

好了,接下来只需要使用时注入RedisTemplate就能使用了,像这样:

@Autowired
private RedisTemplate redisTemplate;

这期间,我们做了什么嘛?我们什么也没有做,那么,这个RedisTemplate对象是怎么注入到Spring容器中的呢?

接下来,就让我们带着这样的疑问逐步剖析其中的原理,这个原理就叫做自动装配。

SPI

先不着急,在这之前,我们先来了解了解上古大法:SPI机制。

SPI ,全称为 Service Provider Interface(服务提供者接口),是一种服务发现机制。它通过在classpath路径下的META-INF/services文件夹查找文件,自动加载文件中所定义的类。

栗子

建一个工程,结构如下

provider 为服务提供方,可以理解为我们的框架

zoo 为使用方,因为我的服务提供接口叫Animal,所以所有实现都是动物~

pom.xml里面啥都没有

1. 定义一个接口

在provider模块中定义接口Animal

package cn.zijiancode.spi.provider;
/**
 * 服务提供者 动物
 */
public interface Animal {
    // 叫
    void call();
}

2. 使用该接口

在zoo模块中引入provider

<dependency>
  <groupId>cn.zijiancode</groupId>
  <artifactId>provider</artifactId>
  <version>1.0.0</version>
</dependency>

写一个小猫咪实现Animal接口

public class Cat implements Animal {
    @Override
    public void call() {
        System.out.println("喵喵喵~~");
    }
}

写一个狗子也实现Animal接口

public class Dog implements Animal {
    @Override
    public void call() {
        System.out.println("汪汪汪!!!");
    }
}

3. 编写配置文件

新建文件夹META-INF/services

在文件夹下新建文件cn.zijiancode.spi.provider.Animal

对,你没看错,接口的全限定类名就是文件名

编辑文件

cn.zijiancode.spi.zoo.Dog
cn.zijiancode.spi.zoo.Cat

里面放实现类的全限定类名

3. 测试

package cn.zijiancode.spi.zoo.test;
import cn.zijiancode.spi.provider.Animal;
import java.util.ServiceLoader;
public class SpiTest {
    public static void main(String[] args) {
        // 使用Java的ServiceLoader进行加载
        ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
        load.forEach(Animal::call);
    }
}

测试结果:

汪汪汪!!!
喵喵喵~~

整个项目结构如下:

借助SPI理解自动装配

回顾一下我们做了什么,我们在resources下创建了一个文件,里面放了些实现类,然后通过ServiceLoader这个类加载器就把它们加载出来了。

假设有人已经把编写配置之类的前置步骤完成了,那么我们是不是只需要使用下面的这部分代码,就能将Animal有关的所有实现类调度出来。

// 使用Java的ServiceLoader进行加载
ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
load.forEach(Animal::call);

再进一步讲,如果再有人把上面这部分代码也给写了,然后把这些实现类全部注入到Spring容器里,那会发生什么?

哇塞,那我他喵的不是就能直接注入然后汪汪汪了吗?!

相信到这里大家心里都已经有个谱了

找找Spring Boot中的配置文件

在SPI机制中,是通过在组件下放入一个配置文件完成的,那么Spring Boot是不是也这样的呢?我们就来找一找吧。

打开redis的组件

咦,这里面却并没有看到有关自动装配的文件,难道我们的猜想是错的嘛?

别急,其实所有spring-boot-starter-x的组件配置都是放在spring-boot-autoconfigura的组件中的

这里有个spring.factories的文件,翻译一下就是spring的工厂,咦,有点像了,打开看看

其他的我们先不用管,可以很明显的看到最下面有个自动配置的注释,key还是个EnableAutoConfiguration,开启自动配置!噢噢噢噢噢!找到了找到了!

往下翻一下,看看有没有Redis相关的。

再打开这个RedisAutoConfiguration类,看看里面是些什么代码

OMG! 破案了破案了!

现在,配置文件我们也找到了:spring.factories,也实锤了就是通过这个配置文件进行的自动配置。

那么,我们来尝试还原一下案情经过:通过某种方式读取spring.factories文件,紧接着把里面所有的自动配置类加载到Spring容器中,然后就可以通过Spring的机制将配置类的@Bean注入到容器中了。

接下来,我们就来学习一下这个某种方式究竟是什么吧~

Spring中的一些注入方式

阿鉴先透露一下,这个某种方式,其实就是某一种注入方式,我们先来看看Spring中有哪些注入方式

聊起Spring,我可是老手了,有兴趣的小伙伴可以看看我的Spring源码分析系列:https://zijiancode.cn/categories/source-framework

关于注入方式,相信小伙伴肯定也是:就这?

类似于@Component,@Bean这些,阿鉴就不说了,大家肯定见过一种这样的注解:EnableXxxxx

比如:EnableAsync开启异步,EnableTransactionManagement开启事务

大家好不好奇这样的注解是怎么生效的?

点开看看呗

嘿,其实里面是个Import注解

Import注解的3种使用方式

我知道,肯定有小伙伴懂得Import注解如何使用,但是为了照顾不懂的小伙伴,阿鉴还是要讲一讲,懂的小伙伴就当复习啦

1.普通的组件

public class Man {
    public Man(){
        System.out.println("Man was init!");
    }
}
@Import({Man.class})
@Configuration
public class MainConfig {
}

在配置类上使用@Import注解,值放入需要注入的Bean就可以啦

2.实现ImportSelector接口

public class Child {
    public Child(){
        System.out.println("Child was init!");
    }
}
public class MyImport implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.my.source.spring.start.forimport.Child"};
    }
}
@Import({MyImport.class})
@Configuration
public class MainConfig {
}

这种方式往Spring中注入的是一个ImportSelector,当Spring扫描到MyImport,将会调用selectImports方法,将selectImports中返回的String数组中的类注入到容器中。

3.实现ImportBeanDefinitionRegistrar接口

public class Baby {
    public Baby(){
        System.out.println("Baby was init!");
    }
}
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinition beanDefinition = new RootBeanDefinition(Baby.class);
        registry.registerBeanDefinition("my-baby",beanDefinition);
    }
}
@Import({MyImportBeanDefinitionRegistrar.class})
@Configuration
public class MainConfig {
}

类似于第二种,当Spring扫描到该类时,将会调用registerBeanDefinitions方法,在该方法中,我们手动往Spring中注入了一个Baby的Bean,理论上可以通过这种方式不限量的注入任何的Bean

SpringBootApplication注解

我们在使用SpringBoot项目时,用到的唯一的注解就是@SpringBootApplication,所以我们唯一能下手的也只有它了,打开它看看吧。

嘿!看看我们发现了什么?EnableAutoConfiguration!妥妥的大线索呀

EnableAutoConfiguration本质上也是通过Import完成的,并且Import了一个Selector

让我们瞧一瞧里面的代码逻辑吧~

selectImports

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
  }
  AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
  return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

getAutoConfigurationEntry

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
  }
  AnnotationAttributes attributes = getAttributes(annotationMetadata);
  // 获取候选的配置类
  List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
  // 移除重复的配置
  configurations = removeDuplicates(configurations);
  // 获取到要排除的配置
  Set<String> exclusions = getExclusions(annotationMetadata, attributes);
  checkExcludedClasses(configurations, exclusions);
  // 移除所有要排除的配置
  configurations.removeAll(exclusions);
  // 过滤掉不具备注入条件的配置类,通过Conditional注解
  configurations = getConfigurationClassFilter().filter(configurations);
  // 通知自动配置相关的监听器
  fireAutoConfigurationImportEvents(configurations, exclusions);
  // 返回所有自动配置类
  return new AutoConfigurationEntry(configurations, exclusions);
}

我们主要看看是如何从配置文件读取的

getCandidateConfigurations

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
  // 这里就是关键,使用SpringFactoriesLoader加载所有配置类,是不是像我们SPI的ServicesLoader
  List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                                                                       getBeanClassLoader());
  Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
                  + "are using a custom packaging, make sure that file is correct.");
  return configurations;
}

getSpringFactoriesLoaderFactoryClass

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
  return EnableAutoConfiguration.class;
}

结合上一步,就是加载配置文件,并且读取key为EnableAutoConfiguration的配置

loadFactoryNames

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
  String factoryTypeName = factoryType.getName();
  return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
  try {
    // FACTORIES_RESOURCE_LOCATION的值为:META-INF/spring.factories
    // 这步就是意味中读取classpath下的META-INF/spring.factories文件
    Enumeration<URL> urls = (classLoader != null ?
                             classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                             ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    // 接下来就是读取出文件内容,封装成map的操作了
    result = new LinkedMultiValueMap<>();
    while (urls.hasMoreElements()) {
      URL url = urls.nextElement();
      UrlResource resource = new UrlResource(url);
      Properties properties = PropertiesLoaderUtils.loadProperties(resource);
      for (Map.Entry<?, ?> entry : properties.entrySet()) {
        String factoryTypeName = ((String) entry.getKey()).trim();
        for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
          result.add(factoryTypeName, factoryImplementationName.trim());
        }
      }
    }
    cache.put(classLoader, result);
    return result;
  }
  catch (IOException ex) {
    throw new IllegalArgumentException("Unable to load factories from location [" +
                                       FACTORIES_RESOURCE_LOCATION + "]", ex);
  }
}

over, 后面的过滤逻辑阿鉴就不在这里说了,毕竟本节的重点是自动装配机制,小伙伴明白了原理就ok啦

ps: 因为后面的逻辑其实挺复杂的,展开了说就太多啦

小结

本篇介绍了关于SpringBoot的自动装配原理,我们先通过SPI机制进行了小小的热身,然后再根据SPI的机制进行推导Spring的自动装配原理,中间还带大家回顾了一下@Import注解的使用,最后成功破案~

下节预告:实现自定义starter

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
2月前
|
XML Java 开发者
Spring Boot开箱即用可插拔实现过程演练与原理剖析
【11月更文挑战第20天】Spring Boot是一个基于Spring框架的项目,其设计目的是简化Spring应用的初始搭建以及开发过程。Spring Boot通过提供约定优于配置的理念,减少了大量的XML配置和手动设置,使得开发者能够更专注于业务逻辑的实现。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,为开发者提供一个全面的理解。
38 0
|
12天前
|
Java 数据库连接 Maven
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
自动装配是现在面试中常考的一道面试题。本文基于最新的 SpringBoot 3.3.3 版本的源码来分析自动装配的原理,并在文未说明了SpringBoot2和SpringBoot3的自动装配源码中区别,以及面试回答的拿分核心话术。
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
|
19天前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
70 14
|
2月前
|
Java Spring
SpringBoot自动装配的原理
在Spring Boot项目中,启动引导类通常使用`@SpringBootApplication`注解。该注解集成了`@SpringBootConfiguration`、`@ComponentScan`和`@EnableAutoConfiguration`三个注解,分别用于标记配置类、开启组件扫描和启用自动配置。
62 17
|
2月前
|
消息中间件 Java 数据库
解密Spring Boot:深入理解条件装配与条件注解
Spring Boot中的条件装配与条件注解提供了强大的工具,使得应用程序可以根据不同的条件动态装配Bean,从而实现灵活的配置和管理。通过合理使用这些条件注解,开发者可以根据实际需求动态调整应用的行为,提升代码的可维护性和可扩展性。希望本文能够帮助你深入理解Spring Boot中的条件装配与条件注解,在实际开发中更好地应用这些功能。
43 2
|
2月前
|
Java 容器
springboot自动配置原理
启动类@SpringbootApplication注解下,有三个关键注解 (1)@springbootConfiguration:表示启动类是一个自动配置类 (2)@CompontScan:扫描启动类所在包外的组件到容器中 (3)@EnableConfigutarion:最关键的一个注解,他拥有两个子注解,其中@AutoConfigurationpackageu会将启动类所在包下的所有组件到容器中,@Import会导入一个自动配置文件选择器,他会去加载META_INF目录下的spring.factories文件,这个文件中存放很大自动配置类的全类名,这些类会根据元注解的装配条件生效,生效
|
6月前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
131 0
|
3月前
|
Java Spring 容器
springboot @RequiredArgsConstructor @Lazy解决循环依赖的原理
【10月更文挑战第15天】在Spring Boot应用中,循环依赖是一个常见问题,当两个或多个Bean相互依赖时,会导致Spring容器陷入死循环。本文通过比较@RequiredArgsConstructor和@Lazy注解,探讨它们解决循环依赖的原理和优缺点。@RequiredArgsConstructor通过构造函数注入依赖,使代码更简洁;@Lazy则通过延迟Bean的初始化,打破创建顺序依赖。两者各有优势,需根据具体场景选择合适的方法。
130 4
|
4月前
|
Java 应用服务中间件 API
Vertx高并发理论原理以及对比SpringBoot
Vertx 是一个基于 Netty 的响应式工具包,不同于传统框架如 Spring,它的侵入性较小,甚至可在 Spring Boot 中使用。响应式编程(Reactive Programming)基于事件模式,通过事件流触发任务执行,其核心在于事件流 Stream。相比多线程异步,响应式编程能以更少线程完成更多任务,减少内存消耗与上下文切换开销,提高 CPU 利用率。Vertx 适用于高并发系统,如 IM 系统、高性能中间件及需要较少服务器支持大规模 WEB 应用的场景。随着 JDK 21 引入协程,未来 Tomcat 也将优化支持更高并发,降低响应式框架的必要性。
Vertx高并发理论原理以及对比SpringBoot
|
4月前
|
Java 开发者 数据格式
【Java笔记+踩坑】SpringBoot基础4——原理篇
bean的8种加载方式,自动配置原理、自定义starter开发、SpringBoot程序启动流程解析
【Java笔记+踩坑】SpringBoot基础4——原理篇