• 【SpringBoot】之自动配置原理分析(源码级别)


    目录


    一、SpringBoot 简介


    1、什么是 SpringBoot

    SpringBoot 是 Spring 项目中的一个子工程,Spring Boot 与 Spring Framework、Spring Cloud 并称为 Spring 的三大核心产品。

    SpringBoot 的官方地址:https://spring.io/projects/spring-boot

    从官方文档我们可以看出,Spring Boot 是为了让我们能够简单快速地搭建独立的、能够直接运行的基于 Spring 的产品级应用程序。

    SpringBoot 通过 Spring 平台和大量的第三方库,只需要最小化的 Spring 配置就能让我们快速的构建庞大的 Spring 项目,做到开箱即用,迅速上手,让我们关注与业务而非配置。

    2、SpringBoot 的特点

    SpringBoot 主要特点是:

    • 可以快速地创建独立的 Spring 应用程序;
    • 直接内嵌 Tomcat、Jetty 或 Undertow 服务器,无需手动部署 war 文件;
    • 通过完善的 starter 依赖体系,大大简化了应用程序的构建配置;
    • 尽可能地自动配置 Spring 和第三方库;
    • 提供了一些大型项目中常见的非功能性特性,如指标、健康检查和外部化配置等;
    • 绝对没有代码生成,也无需 XML 配置。

    3、为什么使用 SpringBoot

    在 SpringBoot 开始流行之前,我们一般都是使用 Strust2 或者 SpringMVC 框架来开发 web 应用,但是这两个框架都有一个特点就是配置非常的繁琐,要写一大堆的配置文件,虽然 Spring 在支持了注解开发之后稍微有些改善,但有的时候还是会比较麻烦。

    这种情况下,SpringBoot 的优势就体现出来了,通过 SpringBoot 特有的自动配置就可以做到只需一个 propertiesyml 配置文件就可以简化 SpringMVC 中需要在 xml 中配置的一大堆的 bean。


    二、SpringBoot 自动配置原理


    1、分析 SpringBoot 的自动配置原理

    每个 SpringBoot 工程的主启动类都有一个 @SpringBootApplication 注解,比如:

    @SpringBootApplication(scanBasePackages = {"com.example.demo"})
    public class DemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们点进入看一下这个注解:

    @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 {
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以看到 SpringBootApplication 注解是由一系列的注解组合而成,其中最核心的就是 @EnableAutoConfiguration 注解,这个注解的含义就是开启自动装配,能够直接被各种组件的 bean 装配到 IOC 容器中,我们点进去看一下:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @AutoConfigurationPackage
    @Import(AutoConfigurationImportSelector.class)
    public @interface EnableAutoConfiguration {
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    同样,EnableAutoConfiguration 也是一个复合注解,其中关键的注解是 @Import(AutoConfigurationImportSelector.class),这个 Import 注解导入了 AutoConfigurationImportSelector.class 这个类,而这个类是我们自动装配的导入选择器,点进这个类去,可以看到一个 selectImports() 核心方法:

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
    	if (!isEnabled(annotationMetadata)) {
    		return NO_IMPORTS;
    	}
    	// 加载元数据
    	AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
    			.loadMetadata(this.beanClassLoader);
    	// 获取自动配置的 Entry 实体
    	AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
    			annotationMetadata);
    	return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们点进 getAutoConfigurationEntry 这个方法去看一下:

    protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
    			AnnotationMetadata annotationMetadata) {
    	// 省略代码
    	......
    	// 核心代码:获取候选的配置类
    	List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    	// 后面是对配置的去重、排除等操作
    	......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里面的核心方法就是 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们跟踪 loadFactoryNames() 方法来到了最终加载资源的方法 loadSpringFactories(),里面加载资源的核心代码是:

    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 properties = PropertiesLoaderUtils.loadProperties(resource);
    		for (Map.Entry<?, ?> entry : properties.entrySet()) {
    			String factoryTypeName = ((String) entry.getKey()).trim();
    			// 解析里面的 key-value 值
    			for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
    				result.add(factoryTypeName, factoryImplementationName.trim());
    			}
    		}
    	}
    	cache.put(classLoader, result);
    	return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    其中获取资源地址中的 FACTORIES_RESOURCE_LOCATION 常量的定义值为 META-INF/spring.factories

    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    
    • 1

    这块代码的核心流程是这样子的:

    • 首先,通过 classLoader.getResources(“META-INF/spring.factories”) 扫描所有 jar 包类路径下的 META-INF/spring.factories 文件,得到该文件的 URL 路径地址;
    • 接着,通过 PropertiesLoaderUtils.loadProperties() 方法将这些文件上的内容封装成 properties 对象;
    • 最后对 properties 对象的值进行解析成一个个 key-value 值,并打包返回最终结果,而这些返回结果就是我们要交给容器中的所有组件。

    我们进入 SpringBoot 自动配置的 jar 包去看一下 META-INF/spring.factories 文件内容,具体所在路径如下(以 2.2.10版本为例):

    在文件内容中找到 EnableAutoConfiguration 的部分值如下:

    所以,在加载自动配置组件的代码中,factoryTypeName 就是 org.springframework.boot.autoconfigure.EnableAutoConfiguration,而 factoryImplementationName 就是对应的每个值,比如:org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,代表要自动配置的类。相当于把 EnableAutoConfiguration 值里面的这些组件都加到了容器中。

    原理分析总结:

    至此,我们基本上已经知道了整个自动装配的流程:扫描并获取所有 jar 包类路径下的 META-INF/spring.factories 文件的 URL 地址,将这些文件加载成 properties 对象,然后获取其中 EnableAutoConfiguration 所有的值,每个值都是自动配置类,最终都会添注入到容器中。


    2、分析 Starter 组件的自动配置原理

    上面我们分析了 SpringBoot 自动配置的原理,那么组件是如何进行自动装配的呢?SpringBoot 的 META-INF/spring.factories 文件中包含了上百个自动配置类,难道 SpringBoot 启动的时候就会把这些所有组件实例化到容器中?

    下面,我们就来分析一下:自动配置的组件是如何自动装配到容器中的

    我们以 Redis 为例,从 META-INF/spring.factories 文件中的 EnableAutoConfiguration 值中可以找到 Redis 的自动配置类为:org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,我们点进去看一下这个自动配置类的源码:

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(RedisOperations.class)
    @EnableConfigurationProperties(RedisProperties.class)
    @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
    public class RedisAutoConfiguration {
    
    	@Bean
    	@ConditionalOnMissingBean(name = "redisTemplate")
    	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
    			throws UnknownHostException {
    		RedisTemplate<Object, Object> template = new RedisTemplate<>();
    		template.setConnectionFactory(redisConnectionFactory);
    		return template;
    	}
    
    	@Bean
    	@ConditionalOnMissingBean
    	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
    			throws UnknownHostException {
    		StringRedisTemplate template = new StringRedisTemplate();
    		template.setConnectionFactory(redisConnectionFactory);
    		return template;
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这里面有4个注解修饰着 RedisAutoConfiguration 自动配置类,我们逐一分析它们的作用。

    先分析 @Configuration 这个注解,它表示所修饰的类为配置类,熟悉 Spring 开发的都知道,在配置类中,我们可以通过 @Bean 注解很方便的将组件的 Bean 注册到 IOC 容器中。这里,Redis 为我们注册了 redisTemplatestringRedisTemplate 两个 Bean,用于操作 Redis 服务器。同时使用 @ConditionalOnMissingBean 注解表示只有当 Bean 不存在时才注册到容器中。

    接着再来分析一下 @EnableConfigurationProperties(RedisProperties.class) 这个注解,它的作用是用来生效 @ConfigurationProperties 注解的,显然,RedisProperties.class 就是个被 @ConfigurationProperties 注解所修饰的配置属性类:

    @ConfigurationProperties(prefix = "spring.redis")
    public class RedisProperties {
    	......
    
    	/**
    	 * Redis server host.
    	 */
    	private String host = "localhost";
    
    	/**
    	 * Login password of the redis server.
    	 */
    	private String password;
    
    	/**
    	 * Redis server port.
    	 */
    	private int port = 6379;
    	
    	......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    配置属性类是用来从 propertiesyml 配置文件中加载配置值的,通过 prefix 还可以指定配置的前缀名称,所以,我们可以这样配置 Redis 所需的属性配置:

    spring:
    	redis:
    		host: http://localhost
    		port: 6379
    		password: 1234
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这样,RedisProperties 在实例化的时候就会读取对应的值注入到相应的属性上。

    再来看一下 @Import 这个注解,它给我们的自动配置类导入了两个类:LettuceConnectionConfiguration.classJedisConnectionConfiguration.class ,它们是用来连接 Redis 服务器的配置类,一个是使用 Lettuce,另一个使用 Jedis,这两个都是常用的连接 Redis 的组件。以 JedisConnectionConfiguration.class 为例:

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class })
    class JedisConnectionConfiguration extends RedisConnectionConfiguration {
    
    	// 连接配置依赖 RedisProperties 属性配置
    	JedisConnectionConfiguration(RedisProperties properties,
    			ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration,
    			ObjectProvider<RedisClusterConfiguration> clusterConfiguration) {
    		super(properties, sentinelConfiguration, clusterConfiguration);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看到 Redis 连接配置类需要依赖 RedisProperties 属性配置类,很明显,只有通过属性配置类我们才能获取 Redis 服务器的地址、端口、密码等信息,有了这些信息才能连接到 Redis 服务器。

    最后就是最关键的注解:@ConditionalOnClass(RedisOperations.class) ,这个注解决定了 RedisAutoConfiguration 是否会生效,它的意思是只有当存在 RedisOperations.class 这个类的时候才会生效 RedisAutoConfiguration,而 RedisOperations.class 是一个最终执行操作 Redis 命令的类,它的定义在 spring-data-redis 包下,所以,需要引入对应的依赖:

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    现在,我们可以对 Redis 组件自动配置原理进行总结了:

    • SpringBoot 在启动的时候会将 RedisAutoConfiguration 注入容器当中,但由于 @ConditionalOnClass 注解的限制,我们需要提供 RedisOperations.class 这个类的定义,而这个类在 spring-data-redis 包下,所以,只有引入了相应的依赖才能生效整个组件;
    • RedisAutoConfiguration 为我们提供了 redisTemplatestringRedisTemplate 这两个 Bean 用来操作 Redis;
    • 连接 Redis 服务器使用的是 Lettuce 或 Jedis 这两个组件,其中连接 Redis 服务器的配置信息来自 RedisProperties 这个配置属性类,里面包含了服务器的地址、端口、密码等信息,这些信息可以通过项目的 properties 或 yml 配置文件进行设置。

    至此,我们已经基本知道了组件是如何被自动装配的了,关键点就是在 META-INF/spring.fatories 文件包含相应的自动配置类,然后再自动配置类中实例化相应的组件、属性配置类等各种 bean。


    知识扩展:自动加载的相关注解


    1)条件依赖注解:

    • @ConditionalOnClass:应用中包含某个类时,对应的配置才生效;
    • @ConditionalOnMissingClass:应用中不包含某个类时,对应的配置才生效;
    • @ConditionalOnBean:Spring 容器中存在指定 class 的实例对象时,对应的配置才生效;
    • @ConditionalOnMissionBean:Spring 容器中不存在指定 class 的实例对象时,对应的配置才生效;
    • @ConditionalOnProperty:项目配置文件中存在指定属性值时,对应的配置才生效;
    • @ConditionalOnResource:指定资源文件存在时,对应的配置才生效;
    • @ConditionalOnWebApplication:当前处于 Web 环境(WebApplicationContext)时,对应的配置才生效;
    • @ConditionalOnNotWebApplication:当前不处于 Web 环境时,对应的配置才生效;
    • @ConditionalOnExpression:项目配置文件中存在指定属性值时,对应的配置才生效;与 ConditionalOnProperty 不同的是,该注解使用的是 SpringEL 表达式;

    2)加载顺序注解:

    • @AutoConfigureAfter:在指定的 Configuration 类之后加载;
    • @AutoConfigureBefore:在指定的 Configuration 类之前加载;
    • @AutoConfigureOrder:指定 Configuration 类的加载顺序,默认值为0;
  • 相关阅读:
    序列化与反序列化笔记
    近期的一些小总结(关于TCP/IP协议相关的)
    【无标题】
    [矩阵论] Unit 3. 矩阵的分解 - 知识点整理
    The 2022 ICPC Asia Xian Regional Contest--C. Clone Ranran
    【机器学习-黑马程序员】人工智能、机器学习概述
    渗透测试-若依框架的杀猪交易所系统管理后台
    Redis持久化策略原理及使用场景选择
    OpenAI Java SDK——chatgpt-java-v1.0.4更新支持GPT-3.5-Turbo,支持语音转文字,语音翻译。
    【牛客编程题】python入门103题(输入&类型,字符串&列表&字典&元组,运算&条件&循环,函数&类&正则)
  • 原文地址:https://blog.csdn.net/aiwangtingyun/article/details/126568396