首页 » 技术资讯 » 高频面试题:谈谈你对 Spring Boot 自动装配机制的理解(配置注解装配加载机制)

高频面试题:谈谈你对 Spring Boot 自动装配机制的理解(配置注解装配加载机制)

神尊大人 2024-07-24 01:33:32 技术资讯 0

扫一扫用手机浏览

文章目录 [+]

阅读完本文你能知道:

Spring Boot 诞生背景什么是 Spring Boot 自动装配?Spring Boot 启动时的自动配置的原理知识Spring Boot 启动时的自动配置的流程对于 Spring Boot 一些常用注解的了解

一步一步 debug 从浅到深。

注意:本文的 Spring Boot 版本为 2.6.3。

高频面试题:谈谈你对 Spring Boot 自动装配机制的理解(配置注解装配加载机制) 高频面试题:谈谈你对 Spring Boot 自动装配机制的理解(配置注解装配加载机制) 技术资讯
(图片来自网络侵删)
Spring Boot 诞生背景

使用过 Spring 的小伙伴,一定有被 XML 配置统治的恐惧。

我们回顾下原来搭建一个 Spring MVC 的 helloword 的 web 项目( xml 配置的)我们是不是要在 pom 中导入各种依赖,然后各个依赖有可能还会存在版本冲突需要各种排除。
当你历尽千辛万苦的把依赖解决了,然后还需要编写 web.xml 、 springmvc.xml 配置文件等。

高频面试题:谈谈你对 Spring Boot 自动装配机制的理解(配置注解装配加载机制) 高频面试题:谈谈你对 Spring Boot 自动装配机制的理解(配置注解装配加载机制) 技术资讯
(图片来自网络侵删)

我们只想写个 helloword 项目而已,确把一大把的时间都花在了配置文件和 jar 包的依赖上面。
大大的影响了我们开发的效率,以及加大了 web 开发的难度。

为了简化这复杂的配置、以及各个版本的冲突依赖关系,Spring Boot 就应运而生。

我们现在通过 idea 创建一个 Spring Boot 项目只要分分钟就解决了,你不需要关心各种配置(基本实现零配置)。
让你真正的实现了开箱即用。
它的出现不仅可以让你把更多的时间都花在你的业务逻辑开发上,而且还大大的降低了 web 开发的门槛。
所以 Spring Boot 还是比较善解人意,知道开发人员的痛点在哪。

为什么 Spring Boot 使用起来这么酸爽呢? 这得益于其自动装配。
自动装配可以说是 Spring Boot 的核心,那究竟什么是自动装配呢?

什么是 Spring Boot 自动装配?

Spring Boot 有一个全局配置文件: application.properties 或 application.yml 。
在这个全局文件里面可以配置各种各样的参数比如:你想改个端口啦 server.port 或者想调整下日志的级别都可以配置。

更多其他可以配置的属性可以参照 官网 。

这么多属性,这些属性在项目是怎么起作用的呢?

Spring Boot 项目看下来啥配置也没有( application.properties 或 application.yml 除外),既然从配置上面找不到突破口,那么我们就只能从启动类上面找入口了。
启动类也就一个光秃秃的一个 main 方法,类上面仅有一个注解 SpringBootApplication 这个注解是 Spring Boot 项目必不可少的注解。
那么自动配置原理一定和这个注解有着千丝万缕的联系!

我们下面来一起看看这个注解吧。

探究自动装配原理@SpringBootApplication

@SpringBootApplication 标注在某个类上说明这个类是 Spring Boot 的主配置类, Spring Boot 就应该运行这个类的 main 方法来启动 Spring Boot 应用;它的本质是一个 组合注解 ,我们点进去查看。

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication {

这里最上面四个注解的话没啥好说的,基本上自己实现过自定义注解的话,都知道分别是什么意思。
关键就在于后面三个注解:

@SpringBootConfiguration@ComponentScan@EnableAutoConfiguration

我们逐个分析下。

@SpringBootConfiguration

这个注解我们点进去就可以发现,它实际上就是一个 @Configuration 注解,这个注解大家应该很熟悉了,加上这个注解就是为了让当前类作为一个配置类交由 Spring 的 IOC 容器进行管理,因为 Spring Boot 本质上还是 Spring,所以原属于 Spring 的注解 @Configuration 在 Spring Boot 中也可以直接应用。

由此可见, @SpringBootConfiguration 注解的作用与 @Configuration 注解相同,都是标识一个可以被组件扫描器扫描的配置类,只不过 @SpringBootConfiguration 是被 Spring Boot 进行了重新封装命名而已。

@ComponentScan

这个注解也很熟悉,用于定义 Spring 的扫描路径,等价于在 xml 文件中配置 <context:component-scan> ,假如不配置扫描路径,那么 Spring 就会默认扫描当前类所在的包及其子包中的所有标注了 @Component , @Service , @Controller 等注解的类。

@EnableAutoConfiguration

这个注解才是实现自动装配的关键,点进去之后发现,它是一个由 @AutoConfigurationPackage 和 @Import 注解组成的复合注解。

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration {

看起来很多注解,实际上关键在 @Import 注解,它会加载 AutoConfigurationImportSelector 类,然后就会触发这个类的 selectImports() 方法。
根据返回的 String 数组(配置类的 Class 的名称)加载配置类。

我们重点看下 AutoConfigurationImportSelector 。

AutoConfigurationImportSelectorAutoConfigurationImportSelector中的selectImport是自动装配的核心实现,它主要是读取META-INF/spring.factories文件,经过去重、过滤,返回需要装配的配置类集合。

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

我们点进 getAutoConfigurationEntry() 方法:

getAttributes 获取 @EnableAutoConfiguration 中的 exclude 、 excludeName 等。
getCandidateConfigurations 获取所有自动装配的配置类,也就是读取 spring.factories 文件,后面会再次说明。
removeDuplicates 去除重复的配置项。
getExclusions 根据 @EnableAutoConfiguration 中的 exclude 、 excludeName 移除不需要的配置类。
fireAutoConfigurationImportEvents 广播事件。
最后根据多次过滤、判重返回配置类合集。

现在我们结合 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);configurations = getConfigurationClassFilter().filter(configurations);fireAutoConfigurationImportEvents(configurations, exclusions);return new AutoConfigurationEntry(configurations, exclusions);}第 1 步:判断自动装配开关是否打开。

默认 spring.boot.enableautoconfiguration=true ,可在 application.properties 或 application.yml 中设置。

第 2 步 :

用于获取 EnableAutoConfiguration 注解中的 exclude 和 excludeName 。

第 3 步:

获取需要自动装配的所有配置类,读取 META-INF/spring.factories 。

我们点进 getCandidateConfigurations() 方法:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { 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;}

获取候选配置了使用了 Spring Framework 自定义的 SPI 机制,使用 SpringFactoriesLoader#loadFactoryNames 加载了类路径下 /META-INF/spring.factories 文件中的配置类,里面是以 key/value 形式存储,其中一个 key 是 EnableAutoConfiguration 类的全类名,而它的 value 是一个以 AutoConfiguration 结尾的类名的列表。
以 spring-boot-autoconfigure 模块为例,其 spring.factories 内容如下。

不光是这个依赖下的 META-INF/spring.factories 被读取到,所有 Spring Boot Starter 下的 META-INF/spring.factories 都会被读取到。

如果,我们自定义一个 Spring Boot Starter,就需要创建 META-INF/spring.factories 文件。

第 4 步 :

到这里可能面试官会问你:“ spring.factories 中这么多配置,每次启动都要全部加载么?”。

很明显,这是不现实的。
我们 debug 到后面你会发现, configurations 的值变小了。

虽然 133 个全场景配置项的自动配置启动的时候默认全部加载。
但实际经过后续处理后只剩下 25 个配置项真正加载进来。
很明显,Spring Boot 只会加载实际你要用到的场景中的配置类。
这是如何做到的了?

按需加载

这里我们分析剩下的 25 个自动配置类,观察到每一个自动配置类都有着 @Conditional 或者其派生条件注解。

@ConditionalOnBean:当容器里有指定 Bean 的条件下@ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下@ConditionalOnSingleCandidate:当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean@ConditionalOnClass:当类路径下有指定类的条件下@ConditionalOnMissingClass:当类路径下没有指定类的条件下@ConditionalOnProperty:指定的属性是否有指定的值@ConditionalOnResource:类路径是否有指定的值@ConditionalOnExpression:基于 SpEL 表达式作为判断条件@ConditionalOnJava:基于 Java 版本作为判断条件@ConditionalOnJndi:在 JNDI 存在的条件下差在指定的位置@ConditionalOnNotWebApplication:当前项目不是 Web 项目的条件下@ConditionalOnWebApplication:当前项目是 Web 项 目的条件下

@Configuration( proxyBeanMethods = false)// 检查是否有该类才会进行加载@ConditionalOnClass({ RedisOperations.class})// 绑定默认配置信息@EnableConfigurationProperties({ RedisProperties.class})@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})public class RedisAutoConfiguration { public RedisAutoConfiguration() { } ...}

所以当 classpath 下存在某一个 Class 时,某个配置才会生效。

上面所有的注解都在做一件事:注册 bean 到 Spring 容器。
他们通过不同的条件不同的方式来完成:

@SpringBootConfiguration 通过与 @Bean 结合完成 Bean 的 JavaConfig 配置;@ComponentScan 通过范围扫描的方式,扫描特定注解注释的类,将其注册到 Spring 容器;@EnableAutoConfiguration 通过 spring.factories 的配置,并结合 @Condition 条件,完成bean的注册;@Import 通过导入的方式,将指定的 class 注册解析到 Spring 容器;

我们在这里画张图把 @SpringBootApplication 注解包含的几个注解分别解释一下。

我们现在提到自动装配的时候,一般会和 Spring Boot 联系在一起。
但是,实际上 Spring Framework 早就实现了这个功能。
Spring Boot 只是在其基础上,通过 SPI 的方式,做了进一步优化。

SPI 全称为 Service Provider Interface,是一种服务发现机制。
为了被第三方实现或扩展的 API,它可以用于实现框架扩展或组件替换

SPI 机制本质是 将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载文件中的实现类,这样可以在运行时,动态为接口替换实现类 。
正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

用生活中的例子说就是,你买了一台小米的手机。
但是你用的充电器并不一定非要是小米充电器,你可以拿其他厂商的充电器来进行充电,只要满足协议、端口等要求,那么就是可以充电的。
这也是一种热拔插的思想,并不是固定死的。

换成代码来说也是一样的,我定义了一个接口,但是不想固定死具体的实现类,因为那样如果要更换实现类就要改动源代码,这往往是不合适的。

那么我也可以定义一个规范,在之后需要更换实现类或增加其他实现类时,遵守这个规范,我也可以动态的去发现这些实现类。

一个接口可以有很多实现,比如数据库驱动,有 oracle,mysql,postgress 等等,他们都遵循JDBC 规范,为了解耦,我们可以抽象出一个高层的 Driver 接口,让各个数据库服务商去实现各自的驱动,在使用的时候我们可以选择加载具体的实现方式,这时候我们就可以使用 SPI 这种技术。

JDK 中的 SPI

讲到 JDK 中的 SPI ,我们不得不说 java.util.ServiceLoader 这个类,我们先跑起来。

1、创建一个接口,Message

public interface Message{ void send()}

2、在 resources 资源目录下创建 META-INF/services 文件夹

3、在 services 文件夹中创建文件,以接口全名命名

4、创建接口实现类

public class SmsMessage implements Message{ public void send(){ System.out.println("send sms message"); }}public class EmailMessage implements Message{ public void send(){ System.out.println("send email message"); }}

5、测试

public class TestServiceLoader{ public static void main(String[] args){ ServiceLoader<Message> messages = ServiceLoader.load(Message.class); for(Message msg : messages){ msg.send(); } }}

打印结果:

send email messagesend sms message

简单一点来说,就是在 META-INF/services 下面定义个文件,然后通过一个特殊的类加载器,启动的时候加载你定义文件中的类,这样就能扩展原有框架的功能。

Spring Boot 中的 SPI 机制

在 Spring Boot 的自动装配过程中,最终会加载 META-INF/spring.factories 文件,Spring Boot 是通过 SpringFactoriesLoader#loadFactoryNames 方法加载的。

Spring Boot 定义了一套接口规范,这套规范规定:Spring Boot 在启动时会扫描外部引用 jar 包中的 META-INF/spring.factories 文件,将文件中配置的类型信息加载到 Spring 容器(此处涉及到 JVM 类加载机制与 Spring 的容器知识),并执行类中定义的各种操作。
对于外部 jar 来说,只需要按照 Spring Boot 定义的标准,就能将自己的功能装置进 Spring Boot。

本文我们主要介绍了 Spring Boot 自动装配原理。

简单来说, Spring Boot 通过 @EnableAutoConfiguration 开启自动装配,通过 SpringFactoriesLoader 最终加载 META-INF/spring.factories 中的自动配置类实现自动装配,自动配置类其实就是通过 @Conditional 按需加载的配置类,想要其生效必须引入 spring-boot-starter-xxx 包实现起步依赖。

好了,关于 Spring Boot 自动装配机制就聊这么多,感兴趣的同学也可以手撸一个自定义 Starter 来加深一遍印象。

标签:

相关文章