Spring Boot 自动装配(二)
前言
最近在学习Spring Boot相关的课程,过程中以笔记的形式记录下来,方便以后回忆,同时也在这里和大家探讨探讨,文章中有漏的或者有补充的、错误的都希望大家能够及时提出来,本人在此先谢谢了!
开始之前呢,希望大家带着几个问题去学习:
1、Spring Boot 自动装配是什么?
2、这个功能在什么时代背景下发明产生的?
3、这个功能有什么用?
4、怎么实现的?
5、优点和缺点是什么?
6、这个功能能应用在工作中?
这是对自我的提问,我认为带着问题去学习,是一种更好的学习方式,有利于加深理解。好了,接下来进入主题。
1、起源
在上篇文章中我们讲到 Spring 注解虽然可以代替以往XML的形式,帮助我们自动注册Bean以及初始化组件,简化我们的开发,但还是做不到真正意义上的自动装配,今天我们就来讲讲 Spring Boot 是如何深度整合 Spring 注解编程模型、@Enable 模块驱动及条件装配等 Spring 原生特性来实现自动装配的。
注:本篇文章所用到的 Spring Boot版本是 2.1.6.BUILD-SNAPSHOT
2、Spring Boot 自动装配实现
我们都知道 Spring Boot 的启动过程非常简单,只需要启动一个 main 方法,项目就可以运行,就算依赖了诸多外部模块如:MVC、Redis等,也不需要我们进行过多的配置,那它的底层原理是什么呢?接下来,我们就一起去看一看。
我们先来看一段 Spring Boot 的启动类代码:
@SpringBootApplication
public class LoongSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(LoongSpringBootApplication.class, args);
}
}
我们需要关注的是 @SpringBootApplication
这个注解:
@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 {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
我们来看一看它的组成部分:
@SpringBootConfiguration
:它里面标注了@Configuration
注解,上篇文章说过,表明这是个配置类,功能与@Configuration
无异。@EnableAutoConfiguration
:这个就是实现自动装配的核心注解,是用来激活自动装配的,其中默认路径扫描以及组件装配、排除等都通过它来实现。@ComponentScan
:上篇文章我们讲过这是用来扫描被@Component
标注的类 ,只不过这里是用来过滤 Bean 的,指定哪些类不进行扫描,而且用的是自定义规则。Class<?>[] exclude()
:根据class来排除,排除指定的类加入spring容器,传入的类型是class类型。且继承自@EnableAutoConfiguration
中的属性。String[] excludeName()
:根据class name来排除,排除特定的类加入spring容器,参数类型是class的全类名字符串数组。同样继承自@EnableAutoConfiguration
。String[] scanBasePackages()
:可以指定多个包名进行扫描。继承自@ComponentScan
。Class<?>[] scanBasePackageClasses()
:可以指定多个类或接口的class,然后扫描 class 所在包下的所有组件。同样继承自@ComponentScan
。
2.1、@EnableAutoConfiguration 实现
上面我们说到 @EnableAutoConfiguration
是实现自动装配的核心注解,是用来激活自动装配的,看注解前缀我们应该知道是上篇文章中所讲的 Spring @Enable 模块驱动的设计模式,所以它必然会有 @Import
导入的被 @Configuration
标注的类或实现 ImportSelector
或 ImportBeanDefinitionRegistrar
接口的类。接着,我们来看看它的定义:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}
可以看到它由两部分组成:
@AutoConfigurationPackage
:这是用来将启动类所在包,以及下面所有子包里面的所有组件扫描到Spring容器中,这里的组件是指被@Component
或其派生注解标注的类。这也就是为什么不用标注@ComponentScan
的原因。@Import(AutoConfigurationImportSelector.class)
:这里导入的是实现了ImportSelector
接口的类,组件自动装配的逻辑均在重写的selectImports
方法中实现。
接下来我们就来看看这两者具体是怎么实现的。
2.1.1、 获取默认包扫描路径
我们先来看看 Spring Boot
是如何通过 @AutoConfigurationPackage
注解获取默认包扫描路径的,进入它的实现:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
可以看到它是通过 @Import
导入了 AutoConfigurationPackages.Registrar
类,该类实现了 ImportBeanDefinitionRegistrar
接口,所以按照上篇文章所讲的,它是在重写的方法中直接注册相关组件。继续往下:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImport(metadata).getPackageName());
}
....
}
private static final class PackageImport {
private final String packageName;
PackageImport(AnnotationMetadata metadata) {
this.packageName = ClassUtils.getPackageName(metadata.getClassName());
}
....
}
这里主要是通过 metadata
元数据信息构造 PackageImport
类。先获取启动类的类名,再通过 ClassUtils.getPackageName
获取启动类所在的包名。我们接着往下看:
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
if (registry.containsBeanDefinition(BEAN)) {
BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
}
else {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(BasePackages.class);
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(BEAN, beanDefinition);
}
}
最后就是将这个包名保存至 BasePackages
类中,然后通过 BeanDefinitionRegistry
将其注册,进行后续处理,至此该流程结束。
2.1.2、获取自动装配的组件
该部分就是实现自动装配的入口,从上面得知这里也是通过 @Import
来实现的,来看看导入的类:
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
....
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
....
}
主要关注重写的 selectImports
方法,其中 AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
是加载自动装配的元信息。而AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata)
该方法返回的就是自动装配的组件,我们进去看看:
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 获取 @EnableAutoConfigoration 标注类的元信息,也就是获取该注解 exclude 和 excludeName 属性值
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 该方法就是获取自动装配的类名集合
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 去除重复的自动装配组件,就是将List转为Set进行去重
configurations = removeDuplicates(configurations);
// 这部分就是根据上面获取的 exclude 及 excludeName 属性值,排除指定的类
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
// 这里是过滤那些依赖不满足的自动装配 Class
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
// 返回的就是经过一系列去重、排除、过滤等操作后的自动装配组件
return new AutoConfigurationEntry(configurations, exclusions);
}
该方法中就是先获取待自动装配组件的类名集合,然后通过一些列的去重、排除、过滤,最终返回自动装配的类名集合。主要关注 getCandidateConfigurations(annotationMetadata, attributes)
这个方法,里面是如何获取自动装配的类名集合:
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;
}
其中getSpringFactoriesLoaderFactoryClass()
返回的是EnableAutoConfiguration.class
。
继续往下,执行的是 SpringFactoriesLoader#loadFactoryNames
方法:
public final class SpringFactoriesLoader {
...
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
// 前面可以看到,这里的 factoryClass 是 EnableAutoConfiguration.class
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
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 factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
...
}
最终的实现逻辑都在这里,主要过程如下:
(1)搜索classpath路径下以及所有外部jar包下的META-INF文件夹中的spring.factories
文件。主要是spring-boot-autoconfigur
包下的
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
...
可以看到其中内容,存储的是key-value格式的数据,且key是一个类的全路径名称,value是多个类的全路径名称,且以逗号分割。
(2)将所有的spring.factories
文件转成Properties
格式,将里面key-value格式的数据转成Map,该Map的value是一个List,之后将相同Key的value合并到List中,将该Map作为方法返回值返回。
(3)返回到 loadFactoryNames
方法,通过上面得知factoryClassName
的值为EnableAutoConfiguration
,所以通过 getOrDefault(factoryClassName, Collections.emptyList())
方法,获取 key 为EnableAutoConfiguration
的类名集合。
ps:
getOrDefault
第一个入参是key的name,如果key不存在,则直接返回第二个参数值
至此,流程结束,最后返回的就是自动装配的组件,其中有我们比较熟悉的Redis、JDBC、SpringMVC等,可以看到一个特点,这些自动装配的组件都是以 AutoConfiguration
结尾。但该组件列表只是候选组件,因为后面还有去重、排除、过滤等一系列操作,这里就不再详细述说。下面我们来看看自动装配的组件内部是怎么样的。
2.2、自动装配的组件内部实现
就拿比较熟悉的 Web MVC 来看,看看是如何实现 Web MVC 自动装配的。先来代码组成部分:
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
...
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ResourceLoaderAware {
...
@Bean
@ConditionalOnBean(View.class)
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
...
}
...
}
@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
@Bean
@Override
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
...
}
@Bean
@Primary
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
...
}
}
...
}
注解部分:
@Configuration
:这个大家都比较熟悉,标识该类是一个配置类@ConditionalXXX
:这是上篇文章所讲的 Spring 条件装配,只不过经由 Spring Boot 扩展形成了自己的条件化自动装配,且都是@Conditional
的派生注解。@ConditionalOnWebApplication
:参数值是 Type 类型的枚举,当前项目类型是任意、Web、Reactive其中之一则实例化该 Bean。这里指定如果为 Web 项目才满足条件。@ConditionalOnClass
:参数是 Class 数组,当给定的类名在类路径上存在,则实例化当前Bean。这里当Servlet.class
、DispatcherServlet.class
、WebMvcConfigurer.class
存在才满足条件。@ConditionalOnMissingBean
:参数是也是 Class 数组,当给定的类没有实例化时,则实例化当前Bean。这里指定当WebMvcConfigurationSupport
该类没有实例化时,才满足条件。
- 装配顺序
@AutoConfigureOrder
:参数是int类型的数值,数越小越先初始化。@AutoConfigureAfter
:参数是 Class 数组,在指定的配置类初始化后再加载。@AutoConfigureBefore
:参数同样是 Class 数组,在指定的配置类初始化前加载。
代码部分:
- 这部分就比较直接了,实例化了和 Web MVC 相关的Bean,如
HandlerAdapter
、HandlerMapping
、ViewResolver
等。其中,出现了DelegatingWebMvcConfiguration
类,这是上篇文章所讲的@EnableWebMvc
所@Import
导入的配置类。
- 这部分就比较直接了,实例化了和 Web MVC 相关的Bean,如
可以看到,在Spring Boot
自动装配的类中,经过了一系列的 @Conditional
条件判断,然后实例化某个模块需要的Bean,且无需我们配置任何东西,当然,这都是默认实现,当这些不满足我们的要求时,我们还得手动操作。
3、总结
关于Spring boot自动装配的内容就告一段落,不难看出Spring Boot自动装配所依赖的注解驱动、@Enable
模块驱动、条件装配等特性均来自 Spring Framework。而自动装配的配置类均来源于spring.factories
文件中。核心则是基于“约定大于配置”理念,通俗的说,就是Spring boot为我们提供了一套默认的配置,只有当默认的配置不满足我们的需求时,我们再去修改默认配置。当然它也存在缺点就是组件的高度集成,使用的时候很难知道底层实现,加深了理解难度。
以上就是本章的内容,如过文章中有错误或者需要补充的请及时提出,本人感激不尽。
参考:
《Spring Boot 编程思想》
Spring Boot 自动装配(二)的更多相关文章
- Spring Boot系列(二):Spring Boot自动装配原理解析
一.Spring Boot整合第三方组件(Redis为例) 1.加依赖 <!--redis--> <dependency> <groupId>org.springf ...
- Spring Boot自动装配
前言 一些朋友问我怎么读源码,这篇文章结合我看源码时候一些思路给大家聊聊,我主要从这三个方向出发: 确定目标,这个目标要是一个具体,不要一上来我要看懂Spring,这是不可能的,目标要这么来定,比如看 ...
- Spring Boot自动装配原理源码分析
1.环境准备 使用IDEA Spring Initializr快速创建一个Spring Boot项目 添加一个Controller类 @RestController public class Hell ...
- Spring Boot 自动装配流程
Spring Boot 自动装配流程 本文以 mybatis-spring-boot-starter 为例简单分析 Spring Boot 的自动装配流程. Spring Boot 发现自动配置类 这 ...
- Spring Boot 自动装配原理
Spring Boot 自动装配原理 Spring Boot 在启动之前还有一系列的准备工作,比如:推断 web 应用类型,设置初始化器,设置监听器,启动各种监听器,准备环境,创建 applicati ...
- 从源码中理解Spring Boot自动装配原理
个人博客:槿苏的知识铺 一.什么是自动装配 SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot在启动时会扫描外部引用jar包中的META-INF/spring.factori ...
- Spring Boot 自动装配(一)
目录 目录 前言 1.起源 2.Spring 模式注解 2.1.装配方式 2.2.派生性 3.Spring @Enable 模块驱动 3.1.Spring框架中@Enable实现方式 3.2.自定义@ ...
- 深度剖析Spring Boot自动装配机制实现原理
在前面的分析中,Spring Framework一直在致力于解决一个问题,就是如何让bean的管理变得更简单,如何让开发者尽可能的少关注一些基础化的bean的配置,从而实现自动装配.所以,所谓的自动装 ...
- spring boot 自动装配的原理
参考: https://blog.csdn.net/Dongguabai/article/details/80865599.如有侵权,请联系本人删除! 入口: import org.springfra ...
随机推荐
- C++学习笔记12_各种文件和引入
1. 编译过程 预处理->编译->汇编->链接->.exe 预处理: ①将所有的“#define”删除,并且展开所有的宏定义 ②处理所有的条件编译指令,如:“#if”.“#if ...
- javaScript中this到底指向谁
1.前言 在JavaScript中,this的指向一直是大多数初学者的易错点,总是搞不清楚this到底指向谁,而在求职面试中,this的指向问题往往又是高频考点.本篇博文就来总结一下在JavaScri ...
- 两张图弄懂函数的递归(以golang为例)
函数递归时要遵守的原则: 执行一个函数时,就要创建一个新的受保护的独立空间(新函数栈) 函数的局部变量是独立的,不会相互影响: 递归必须向退出递归的条件逼近,否则就会无限递归: 当一个函数执行完毕,或 ...
- 详细讲解 Redis 的两种安装部署方式
Redis 是一款比较常用的 NoSQL 数据库,我们通常使用 Redis 来做缓存,这是一篇关于 Redis 安装的文章,所以不会涉及到 Redis 的高级特性和使用场景,Redis 能够兼容绝大部 ...
- .NET后端知识汇总
C#.net系列后端知识点汇总(也有些许数据库.svn等),他山之石. 1..net相关技术:XML.webservice.SOAP,其中webservice使用三大技术:XML.SOAP.WSDL. ...
- 如何对 React 函数式组件进行优化
文章首发个人博客 前言 目的 本文只介绍函数式组件特有的性能优化方式,类组件和函数式组件都有的不介绍,比如 key 的使用.另外本文不详细的介绍 API 的使用,后面也许会写,其实想用好 hooks ...
- hdu 1205 吃糖果 (抽屉原理<鸽笼原理>)
吃糖果Time Limit: 6000/3000 MS (Java/Others) Memory Limit: 65535/32768 K (Java/Others)Total Submissi ...
- Windows平台LoadLibrary加载动态库搜索路径的问题
一.背景 在给Adobe Premiere/After Effects等后期制作软件开发第三方插件的时候,我们总希望插件依赖的动态库能够脱离插件的位置,单独存储到另外一个地方.这样一方面可以与其他程序 ...
- 领扣(LeetCode)两句话中的不常见单词 个人题解
给定两个句子 A 和 B . (句子是一串由空格分隔的单词.每个单词仅由小写字母组成.) 如果一个单词在其中一个句子中只出现一次,在另一个句子中却没有出现,那么这个单词就是不常见的. 返回所有不常用单 ...
- oracle:表重命名
SQL> rename test1 to test; Table renamed. SQL> alter table test rename to test1; Table altered ...