【Spring】深究SpringBoot自动装配原理

文章目录

    • 前言
    • 1、main入口
    • 2、@SpringBootApplication
    • 3、@EnableAutoConfiguration
    • 4、AutoConfigurationImportSelector
      • 4.1、selectImports()
      • 4.2、getAutoConfigurationEntry()
      • 4.3、getCandidateConfigurations()
      • 4.4、loadFactoryNames()
    • 5、META-INF/spring.factories
    • 6、总结

前言

早期的Spring项目需要添加需要配置繁琐的xml,比如MVC、事务、数据库连接等繁琐的配置。Spring Boot的出现就无需这些繁琐的配置,因为Spring Boot基于约定大于配置的理念,在项目启动时候,将约定的配置类自动装配到IOC容器里。

这些都因为Spring Boot有自动装配的特性。

接下来将会逐步从源码跟踪进去,一步步掀开自动装配的面纱。

1、main入口

在SpringBoot项目的启动类中,注解SpringBootApplication是必须需要添加的,既然是从这里启动的,那么自动装配的操作应该也是在启动的时候去执行的吧,我们一步步挖进去一探究竟。

@SpringBootApplication
public class SpringbootInitApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootInitApplication.class, args);
    }
}

2、@SpringBootApplication

点进来@SpringBootApplication注解之后会发现,这玩意头顶怎么挂着这么多注解的。不要着急,与Bean注入也只有三个相关,而自动装配的核心注解也是只有一个:

  • @SpringBootConfiguration:继承自Spring的 @Configuration 注解,作用也大致相同,支持在入口处通过@Bean等注解手动配置一下 Bean 加入到容器中;
  • @EnableAutoConfiguration:顾名思义,这玩意就是用来自动装配的,都写在人家脸上了,接下来主要的介绍核心也是该注解;
  • @ComponentScan:告诉Spring需要扫描哪些包或类,如果不设值的话默认扫描@ComponentScan注解所在类的同级类和同级目录下的所有类,这也是为什么放置启动类位置有要求的原因。
@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 {
    // 省略该注解内属性和方法
    // ......
}

3、@EnableAutoConfiguration

点进来@EnableAutoConfiguration注解,去掉那些有的没的注解,可以初步发现与自动装配有关的应该是有两个,分别是注解@AutoConfigurationPackage和导入的类AutoConfigurationImportSelector

点进去注解@AutoConfigurationPackage发现里面没有什么有用的信息,其作用是将添加该注解的类所在的package 作为自动装配 package 进行管理,感兴趣的小伙伴可以自行点进去查看。

AutoConfigurationImportSelector作为被导入的类,也是实现自动装配的核心类之一,接下来将会点进去查看其细节。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    // 省略该注解内属性和方法
    // ......
}

4、AutoConfigurationImportSelector

4.1、selectImports()

AutoConfigurationImportSelector类中,自动装配核心方法为selectImports(),在其文档中是这么介绍该方法的:

Select and return the names of which class(es) should be imported based on the AnnotationMetadata of the importing @Configuration class.

Returns: the class names, or an empty array if none

根据@Configuration中的AnnotationMetadata,选择并返回应该被导入的类的名称。

返回:类名,如果不存在则返回空数组

可以看到从这里便已经获取被设置需要自动装配的类的信息了,从源码中可以看到在selectImports()中的核心方法应该是getAutoConfigurationEntry()

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
    // 省略类中成员属性...
            
	@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}
    
    // 省略类中其余方法...
}

4.2、getAutoConfigurationEntry()

getAutoConfigurationEntry()方法同样处于AutoConfigurationImportSelector类中,在刚方法中主要返回的是已经配置好的配置数组,该配置数组被包装在类中,其中AutoConfigurationImportSelector.AutoConfigurationEntry的介绍如下:

Create an entry with the configurations that were contributed and their exclusions.

Params: configurations – the configurations that should be imported exclusions – the exclusions that were applied to the original list

使用所提供的配置及其排除项创建一个条目。

参数:configurations ——应该导入的配置 exclusions – 应用于原始列表的排除项

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);
}

4.3、getCandidateConfigurations()

方法getCandidateConfigurations()同样处于AutoConfigurationImportSelector类中,该方法主要用于获取应考虑的自动装配类名称,而获取候选项的方法为其中的loadFactoryNames()方法。

Return the auto-configuration class names that should be considered. By default this method will load candidates using ImportCandidates with getSpringFactoriesLoaderFactoryClass().

返回应考虑的自动配置类名称。默认情况下,此方法将使用ImportCandidates和getSpringFactoriesLoaderFactoryClass()加载候选项。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
	List<String> configurations = new ArrayList<>(
        	// 获取候选项
			SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
	ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
	Assert.notEmpty(configurations,
			"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
					+ "are using a custom packaging, make sure that file is correct.");
	return configurations;
}

4.4、loadFactoryNames()

挖到这里其实就差不多到头了,再往下就不太礼貌了。

在这里干的活在官方给出的文档中都说的很简单明了了:

Load the fully qualified class names of factory implementations of the given type from “META-INF/spring.factories”, using the given class loader.

As of Spring Framework 5.3, if a particular implementation class name is discovered more than once for the given factory type, duplicates will be ignored.

使用给定的类加载器,从"META-INF/spring.factories"加载给定类型的工厂实现的完全限定类名。

从 Spring Framework 5.3 开始,如果针对给定工厂类型多次发现特定实现类名称,则将忽略重复项。

从介绍中就可以看到所谓的自动装配,就是将META-INF/spring.factories中写明白的内容给加载出来,同时连上面提及到的排除项都写明白在这里了,而源码也是对官方文档进行直接翻译了,猜测是出于封装性和代码简洁度考虑,开发人员将核心逻辑封装成了私有方法loadSpringFactories()

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
	ClassLoader classLoaderToUse = classLoader;
	if (classLoaderToUse == null) {
		classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
	}
	String factoryTypeName = factoryType.getName();
    // 核心流程
	return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
	Map<String, List<String>> result = cache.get(classLoader);
	if (result != null) {
		return result;
	}
	result = new HashMap<>();
	try {
        // 获取资源,常量FACTORIES_RESOURCE_LOCATION的值为META-INF/spring.factories
		Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
        // 逐一处理加载到的资源
		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();
				String[] factoryImplementationNames =
						StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
				for (String factoryImplementationName : factoryImplementationNames) {
					result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
							.add(factoryImplementationName.trim());
				}
			}
		}
		// Replace all lists with unmodifiable lists containing unique elements
		result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
				.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
		cache.put(classLoader, result);
	}
	catch (IOException ex) {
		throw new IllegalArgumentException("Unable to load factories from location [" +
				FACTORIES_RESOURCE_LOCATION + "]", ex);
	}
	return result;
}

5、META-INF/spring.factories

spring-boot-autoconfigure.jar 包中的 META-INF/spring.factories 里面默认配置了很多aoto-configuration,如下:

image-20230802165739779

WebMvcAutoConfiguration为例:

package org.springframework.boot.autoconfigure.web.servlet;

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurerAdapter.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
    }

    @Bean
    @ConditionalOnMissingBean(HttpPutFormContentFilter.class)
    @ConditionalOnProperty(prefix = "spring.mvc.formcontent.putfilter", name = "enabled", matchIfMissing = true)
    public OrderedHttpPutFormContentFilter httpPutFormContentFilter() {
        return new OrderedHttpPutFormContentFilter();
    }
    
    ......etc
}

这里有一个地方不知道大家有没有注意到,有大部分的注解前面都有Conditional字样的,这其实也是SpringBoot的强大之处之一,其使用了 Spring 4 框架的新特性:@Conditional注释,此注解使得只有在特定条件满足时才启用一些配置。这也 Spring Boot “智能” 的关键注解。Conditional大家族如下:

注解描述
@ConditionalOnBean当容器中存在指定的Bean时生效
@ConditionalOnClass当类路径中存在指定的类时生效
@ConditionalOnExpression通过SpEL表达式指定条件,满足条件时生效
@ConditionalOnMissingBean当容器中不存在指定的Bean时生效
@ConditionalOnMissingClass当类路径中不存在指定的类时生效
@ConditionalOnNotWebApplication当应用程序不是Web应用时生效
@ConditionalOnResource当指定的资源存在于类路径中时生效
@ConditionalOnWebApplication当应用程序是Web应用时生效

6、总结

SpringBoot自动配置原理如下:

  1. @EnableAutoConfiguration 注解导入 AutoConfigurationImportSelector 类;
  2. 执行 selectImports() 方法调用 SpringFactoriesLoader.loadFactoryNames() 扫描所有 jar 下面的对应的 META-INF/spring.factories 文件;
  3. 限定为 @EnableAutoConfiguration 对应的 value,将这些装配条件的装配到 IOC 容器中。

自动装配简单来说就是自动将第三方的组件的 bean 装载到 IOC 容器内,不需要再去写 bean 相关的配置,符合约定大于配置理念。SpringBoot 基于约定大于配置的理念,配置如果没有额外的配置的话,就给按照默认的配置使用约定的默认值,按照约定配置到 IOC 容器中,无需开发人员手动添加配置,加快开发效率。

同时,对于自己开发SDK时,也可利用 SpringBoot 自动装配原理,编写自己的 META-INF/spring.factories 文件,从而将某些特定需求的类生成 Bean 放入容器中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/57393.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nginx实现反向代理和负载均衡

Nginx安装 本文章主要介绍下&#xff0c;如何使用Nginx来实现反向代理和负载均衡&#xff0c;Nginx安装和基础知识&#xff0c;可参考我的这篇文章 Nginx安装。 Nginx实现反向代理 实现反向代理需要准备两台Nginx服务器。一台Nginx服务器A&#xff0c;ip为 192.168.206.140&…

浅谈机器视觉

目录 1.什么是机器视觉 2.学习机器视觉需要掌握的知识 3.机器视觉的由来 4.机器视觉带来的福利 1.什么是机器视觉 机器视觉&#xff08;Computer Vision&#xff09;是人工智能领域中的一个分支&#xff0c;旨在通过模仿人类的视觉系统&#xff0c;使计算机能够理解和解释图…

【Leetcode】(自食用)找到消失的数字

step by step. 题目&#xff1a; 给你一个含 n 个整数的数组 nums &#xff0c;其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字&#xff0c;并以数组的形式返回结果。 示例 1&#xff1a; 输入&#xff1a;nums [4,3,2,7,8,2,3,1] 输…

Stable Diffusion教程(6) - 扩展安装

打开stable diffusion webUI界面 加载插件列表 依次点击扩展->可用->加载自 搜索插件 首先在搜索框输入你要安装的插件&#xff0c;然后点击插件后面的安装按钮 如果你需要的插件这里面没有找到&#xff0c;可通过通网址安装的方式安装。 在git仓库网址输入框输入的你插件…

分享 一个类似 ps 辅助线功能

效果图片&#xff1a; 提示&#xff1a;这里的样式我做边做了修改&#xff0c;根据个人情况而定。 //你也可以npm下载 $ npm install --save vue-ruler-tool特点 没有依赖可拖动的辅助线快捷键支持 开始使用 1. even.js /*** description 绑定事件 on(element, event, han…

FPGA项目设计:数字时钟

项目要求&#xff1a; 设计一个数字时钟&#xff0c;数码管前两位显示小时&#xff0c;数码管中间两位显示分钟&#xff0c;数码管后面两位显示秒。 项目设计&#xff1a; 系统框架图&#xff1a; 计数模块时序图&#xff1a; 代码实现&#xff1a; 计数模块&#xff1a; /…

举个栗子~Quick BI 技巧(2):创建柱线组合图

上一期举个栗子为数据粉们分享了如何简单几步创建趋势折线图&#xff0c;有一些数据粉发来疑问&#xff1a;如何利用 Quick BI 制作柱线图呢&#xff1f; 线柱图是一种非常重要且常用的组合图表&#xff0c;可以将两组数据在同一个表中直观的表达。今天的栗子&#xff0c;我们…

什么是头脑风暴法,有哪些原则?

1. 什么是头脑风暴法&#xff1f; 头脑风暴法&#xff08;Brainstorming&#xff09;是一种用于创造性思维和问题解决的方法。它旨在通过集体讨论和思维碰撞&#xff0c;激发团队成员的创造力和想象力&#xff0c;从而产生新的创意和解决方案。 在头脑风暴会议中&#xff…

【项目方案】OpenAI流式请求实现方案

文章目录 实现目的效果比对非stream模式stream模式实现方案方案思路总体描述前端方案对比event-source-polyfill代码示例前端实现遇到的问题与解决方法后端参考资料时序图关键代码示例后端实现时遇到的问题与解决方法实现目的 stream是OpenAI API中的一个参数,用于控制请求的…

Dockerfile构建Tomcat镜像(源码)

Dockerfile构建Tomcat镜像 目录 Dockerfile构建Tomcat镜像 1、建立工作目录 2、编写Dockerfile文件 3、构建镜像 4、测试容器 5、浏览器访问测试&#xff1a; 1、建立工作目录 [roothuyang1 ~]# mkdir tomcat[roothuyang1 ~]# cd tomcat/[roothuyang1 tomcat]# lsapach…

了解垃圾回收算法

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ 垃圾回收&#xff08;Garbage Collect&#xff09;是Java语言中的一种自动内存管理机制&#xff0c;用于自动回收不再使用的对象所占用的内存空间。Java虚拟机会自动追踪和…

C# Microsoft消息队列服务器的使用 MSMQ

先安装消息队列服务器 private static readonly string path ".\\Private$\\myQueue";private void Create(){if (!MessageQueue.Exists(path)){MessageQueue.Create(path);}}private void Send(){Stopwatch stopwatch new Stopwatch();stopwatch.Start();Message…

【爬虫逆向案例】某易云音乐(评论)js逆向—— params、encSecKey解密

声明&#xff1a;本文只作学习研究&#xff0c;禁止用于非法用途&#xff0c;否则后果自负&#xff0c;如有侵权&#xff0c;请告知删除&#xff0c;谢谢&#xff01; 【爬虫逆向案例】某易云音乐&#xff08;评论&#xff09;js逆向—— params、encSecKey解密 1、前言2、行动…

国标GB28181安防视频平台EasyGBS大批量通道接入后,创建角色接口未响应的排查

国标GB28181协议视频平台EasyGBS是基于国标GB28181协议的视频云服务平台&#xff0c;支持多路设备同时接入&#xff0c;并对多平台、多终端分发出RTSP、RTMP、FLV、HLS、WebRTC等格式的视频流。平台可提供视频监控直播、云端录像、云存储、检索回放、智能告警、语音对讲、平台级…

Python 一篇入门

目录 Python 的简介与特点 Python支持多种编程风格 解释运行 跨平台 可扩展强 可嵌入 丰富的库 Python版本选择 Python开发环境搭建 认识Python解释器 快速入门 变量和赋值 动态类型 变量命名规则 认识 "数字" 认识 "字符串" 认识 "…

TSINGSEE青犀视频汇聚平台EasyCVR视频广场面包屑侧边栏支持拖拽操作

TSINGSEE青犀视频汇聚平台EasyCVR可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有GB28181、RTSP/Onvif、RTMP等&#xff0c;以及厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等&#xff0c;能对外分发RTSP、RTMP、FLV、HLS、Web…

mysql 、sql server 常见的区别

&#xff2e;&#xff35;&#xff2c;&#xff2c;   处理 MySQL IFNULL(col , val) SQL Server ISNULL(col,val) 表名、列名等 一般不推荐用保留字 &#xff0c;如果非要保留字 MySQL 用用着重号&#xff0c;即 反引号 包括 select col from GROUP SQL Server 用用着重号…

【GO】go语言入门实战 —— 命令行在线词典

文章目录 程序介绍抓包代码生成生成request body解析respond body完整代码 字节青训营基础班学习记录。 程序介绍 在运行程序的时候以命令行的形式输入要查询的单词&#xff0c;然后程序返回单词的音标、释义等信息。 示例如下&#xff1a; 抓包 我们选择与网站https://fany…

Docker Compose 使用方法

目录 前言 安装 Docker Compose Ubuntu 安装与更新 Red Hat 安装与更新 验证是否安装 Docker Compose 创建 docker-compose.yml 文件 创建一个MySQL 与 tomcat 示例 使用Docker Compose启动服务 前言 Docker Compose 是一个工具&#xff0c;旨在帮助定义和 共享多容器…

Vue如何做一个左边栏

要求一-------点击之后能够实现页面跳转,使用router&#xff0c;点击之后跳到指定页面&#xff1a; 第二步&#xff1a;如何实现简易的前端路由 第三步 左侧边栏的正确写法&#xff0c;ul中li套router-link 第四步 实现嵌套路由 第五步 ul中嵌套着li 第六步嵌套路由 第七步&…