SpringBoot 自动装配流程及源码剖析

前言

Spring Framework 一直在致力于解决一个问题,就是如何让 bean 管理变得更简单,如何让开发者尽可能的少关注一些基础化的 bean 配置,从而实现自动装配。所谓的自动装配,实际上就是如何自动将 bean 装载到 Ioc 容器中来

实际上在 spring 3.x 版本中,Enable 模块驱动注解的出现,已经有了一定的自动装配的雏形,而真正能够实现这一机制,还是在 spirng 4.x 版本中,conditional 条件注解的出现;@EnableXxx 注解其实本质上就是 @Import 注解的体现,而 @Import 注解是为了替代之前的 <import> 标签而出现的.

@Import 可以根据添加的不同类型来作出不一样的操作

  • 普通类型:直接注入该类型的对象
  • 实现了 ImportBeanDefinitionRegistrar 接口:不注入该类型的对象,调用 registerBeanDefinitions 方法,通过注册器进行注入
  • 实现了 ImportSelector 接口:不注入该类型的对象,调用 selectImports 方法,将返回的数据注入到容器中

深入分析装配过程

启动类注解:@SpringBootApplication->内置注解:@EnableAutoConfiguration

EnableAutoConfiguration:主要作用就是帮助 SpringBoot 应用把所有符合条件的 @Configuration 配置都加载到当前创建且使用的 IOC 容器中

@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    
    

如上可以看到,通过 Import 导入的类型不仅仅是一个普通的配置类,而是一个实现 ImportSelector 接口的类型,它基于动态 bean 加载的功能;由于其接口下的核心方法是 selectImports,可以追踪一下其源码实现

selectImports 方法

public String[] selectImports(AnnotationMetadata annotationMetadata) {
    
    
	if (!isEnabled(annotationMetadata)) {
    
    
		return NO_IMPORTS;
	}
	// 从配置文件(spring-autoconfigure-metadata.properties)中加载
	AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
			.loadMetadata(this.beanClassLoader);
	// 获取所有候选配置类 EnableAutoConfiguration
	AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
			annotationMetadata);
	return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

getAutoConfigurationEntry 方法

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
			AnnotationMetadata annotationMetadata) {
    
    
	if (!isEnabled(annotationMetadata)) {
    
    
		return EMPTY_ENTRY;
	}
	// 获取元注解中的属性
	AnnotationAttributes attributes = getAttributes(annotationMetadata);
	// 使用 SpringFactoriesLoader 加载 classpath 路径下 META-INF\spring.factories中,
	// key= org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的value
	List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
	// 去重
	configurations = removeDuplicates(configurations);
	// 应用 exclusion 属性
	Set<String> exclusions = getExclusions(annotationMetadata, attributes);
	checkExcludedClasses(configurations, exclusions);
	configurations.removeAll(exclusions);
	// 过滤,检查候选配置类上的注解 @ConditionalOnClass,如果要求的类不存在,则这个候选类会被过滤不被加载
	configurations = filter(configurations, autoConfigurationMetadata);
	// 广播事件
	fireAutoConfigurationImportEvents(configurations, exclusions);
	return new AutoConfigurationEntry(configurations, exclusions);
}

本质上来说,其实 EnableAutoConfiguration 会帮助 SpringBoot 应用把所有符合 @Configuration 配置都加载到当前 SpringBoot 创建的 IOC 容器,而这里面借助了 Spring 框架提供的一个工具类 SpringFactoriesLoader 的支持;以及用到了 Spring 提供的条件注解 @Conditional,选择性的针对需要加载的 bean 进行条件过滤

SpringFactoriesLoader 其实和 SPI 实现机制的是一样的,只不过它不会像 SPI 一次性把所有的类全部加载完,而是通过 key「全限定类名」加载其对应 value,加载的文件名称:classpath:META-INF/spring.factories

条件过滤配置类

在 spring.factories 文件中配置的类有很多,有时候配置类又要依赖于其他的类型存在才得以生存,所以在 SpringBoot 自动装配中还提供了 ConditionalOnClass、ConditionalOnBean 这些条件来过滤所加载的配置类,摘取部分 spring-boot-autoconfigure 模块下 spring-autoconfigure-metadata.properties 文件源码

org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration=
org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration.ConditionalOnClass=io.lettuce.core.RedisClient
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration=
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.ConditionalOnClass=org.springframework.data.redis.core.RedisOperations

通过这种条件过滤可以有效的减少 @Configuration 类数量从而降低 SpringBoot 启动时间

小结

通过启动类注解下自动配置注解实现的 ImportSelector 选择器,去扫描指定文件下所有的配置类,按照系统需要去加载和过滤相关的配置类,满足系统运行其他中间件的需要,通过以下图来小结上面所介绍的内容,接下来就是介绍容器是在那个过程中去执行扫描工作的

在这里插入图片描述

深入解析过程

以上内容只是介绍配置类是如何装配进去的,具体的解析还是交由我们容器去处理的,而且这些配置类也不是说在拿到以后就直接去注入的,它们是等待所依赖的类型先注入以后,到最后才去处理的.

在这里插入图片描述

通过类图可以看出,@Import 导入的选择器并不是直接实现 ImportSelector 接口,而是实现的 DeferredImportSelector,其字面含义就是延迟导入,对父接口做了增强处理

DeferredImportSelector 接口

通过以上的类结构可以看出 DeferredImportSelector 接口是基于 ImportSelector 接口的一个扩展

DeferredImportSelector 接口本身也有 ImportSelector 接口的功能,如果我们仅仅是实现了DeferredImportSelector 接口,重写了 selectImports 方法,那么 selectImports 方法还是会被执行的,来看代码

public class MyImportSelector implements DeferredImportSelector {
    
    
	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    
    
		System.out.println("MyImportSelector implements DeferredImportSelector >>>");
		return new String[0];
	}
}
@Configuration
@Import(MyImportSelector.class)
public class MyAutoConfig {
    
    
	public static void main(String[] args) {
    
    
		ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyAutoConfig.class);
	}
}

但是当我们重写了 DeferredImportSelector 中的 Group 接口,并重写了 getImportGroup 方法,那么容器在启动时就不会执行 selectImports 方法了,而是执行 getImportGroup 方法,进而执行 Group 接口中重写的方法,代码如下:

public class MyImportSelector implements DeferredImportSelector {
    
    
	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    
    
		System.out.println("MyImportSelector implements DeferredImportSelector >>>");
		return new String[0];
	}

	@Override
	public Class<? extends Group> getImportGroup() {
    
    
		System.out.println("MyImportSelector#getImportGroup");
		return MyGroup.class;
	}

	public static class MyGroup implements Group {
    
    
		private List<Entry> imports = new ArrayList<>();

		@Override
		public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
    
    
			System.out.println("MyImportSelector->MyGroup#process");
		}

		@Override
		public Iterable<Entry> selectImports() {
    
    
			System.out.println("MyImportSelector->MyGroup#selectImports");
			return imports;
		}
	}
}

ImportSelector 实例的 selectImports 方法的执行时机,是在 @Configuration 注解中的其他逻辑被处理之前,所谓的其他逻辑,包括对 @ImportResource、@Bean 这些注解的处理(注意,这里只是对 @Bean 修饰方法的处理,并不是立即调用 @Bean 修饰的方法,这个区别很重要!)

上面的结论以及从流程图分析,我们可以直接在源码中找到到对应的答案,首先定位到 ConfigurationClassParser#parse 方法

  • 首先看到调用的是 doProcessConfigurationClass:循环遍历每一个处理配置类
  • 处理 @Import 注解方法中,在这个方法可以看到 @Import 注解的实现逻辑,处理 ImportSelector 接口、子接口不同类型的实现:DeferredImportSelector、ImportSelector,在处理前者时将对应的实例存储了起来
  • 等待其他的配置类都已经处理完成以后,到 parse 方法块后面,执行 deferredImportSelectorHandler#process 方法
    • 先处理的是 register 方法,获取我们重写的 importGroup 方法的返回值,如果为空说明没有重写 Group 接口,那么就使用原来的 ImportSelector 实现类对象且创建默认的 Group 实现 DefaultDeferredImportSelectorGroup,否则就是使用自定义的 Group 对象
    • 再看 processGroupImports 方法,主要看的是方法块里面的 grouping.getImports 方法,在这里面会根据 Group 实现类的不同来执行 process 方法,如果是默认实现,那么调用的就是 ImportSelector 实现类的 selectImports 方法返回,否则就调用自定义 Group 对象的 process 方法,在这里面会看到 getAutoConfigurationEntry 装配的核心方法被调用,返回自动装配的那些配置类
    • 到这里,就可以清晰的了解到自动装配里面核心的解析过程是什么的!!!

同时,从以上可以看出,ImportSelector 与 DeferredImportSelector 的区别,就是执行 selectImports 方法时有所区别,这个差别期间,Spring 容器对此 Configuration 配置类做了其他的逻辑:包括 @ImportResource、@Bean 这些注解处理

ConfigurationClassPostProceesor 核心类流程详解:Spring 核心类 ConfigurationClassPostProcessor 流程讲解及源码全面分析

自动装配示例

使用 HttpEncodingAutoConfiguration 来解释自动装配原理

/*
表名这是一个配置类,
*/
@Configuration(proxyBeanMethods = false)
/*
启动指定类的 ConfigurationProperties 功能,进入 HttpProperties 查看,将配置文件中对应的值和 HttpProperties 绑定起来,并把 HttpProperties 加入到 ioc 容器中
*/
@EnableConfigurationProperties(HttpProperties.class)
/*
spring 底层 @Conditional 注解,根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效
此时表示判断当前应用是否是 web 应用,如果是,那么配置类生效
*/
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
/*
判断当前项目由没有这个类 CharacterEncodingFilter,springmvc 中进行乱码解决的过滤器
*/
@ConditionalOnClass(CharacterEncodingFilter.class)
/*
判断配置文件中是否存在某个配置:spring.http.encoding.enabled
如果不存在,判断也是成立的,
即使我们配置文件中不配置spring.http.encoding.enabled=true,也是默认生效的
*/
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
    
    

    // 和 springboot 配置文件映射
	private final HttpProperties.Encoding properties;

    // 只有一个有参构造器的情况下,参数的值就会从容器中拿
	public HttpEncodingAutoConfiguration(HttpProperties properties) {
    
    
		this.properties = properties.getEncoding();
	}

    // 给容器中添加一个组件,这个组件的某些值需要从 properties 中获取
	@Bean
	@ConditionalOnMissingBean//判断容器中是否有此组件
	public CharacterEncodingFilter characterEncodingFilter() {
    
    
		CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
		filter.setEncoding(this.properties.getCharset().name());
		filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
		filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
		return filter;
	}

	@Bean
	public LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
    
    
		return new LocaleCharsetMappingsCustomizer(this.properties);
	}

	private static class LocaleCharsetMappingsCustomizer
			implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
    
    

		private final HttpProperties.Encoding properties;

		LocaleCharsetMappingsCustomizer(HttpProperties.Encoding properties) {
    
    
			this.properties = properties;
		}

		@Override
		public void customize(ConfigurableServletWebServerFactory factory) {
    
    
			if (this.properties.getMapping() != null) {
    
    
				factory.setLocaleCharsetMappings(this.properties.getMapping());
			}
		}

		@Override
		public int getOrder() {
    
    
			return 0;
		}
	}
}

根据当前不同的条件判断,决定这个配置类是否生效

​ 1、springboot启动会加载大量的自动配置类

​ 2、查看需要的功能有没有在springboot默认写好的自动配置类中华

​ 3、查看这个自动配置类到底配置了哪些组件

​ 4、给容器中自动配置类添加组件的时候,会从properties类中获取属性

@Conditional:自动配置类在一定条件下才能生效

@Conditional扩展注解 作用
@ConditionalOnJava 系统的java版本是否符合要求
@ConditionalOnBean 容器中存在指定Bean
@ConditionalOnMissingBean 容器中不存在指定Bean
@ConditionalOnExpression 满足SpEL表达式
@ConditionalOnClass 系统中有指定的类
@ConditionalOnMissingClass 系统中没有指定的类
@ConditionalOnSingleCandidate 容器中只有一个指定的Bean,或者是首选Bean
@ConditionalOnProperty 系统中指定的属性是否有指定的值
@ConditionalOnResource 类路径下是否存在指定资源文件
@ConditionOnWebApplication 当前是web环境
@ConditionalOnNotWebApplication 当前不是web环境
@ConditionalOnJndi JNDI存在指定项

总结

最后,在解析自动装配的过程中涉及到的比较重要的类 ConfigurationClassPostProcessor,它既实现了 BeanDefinitionRegisterPostProcessor 同时也实现了 BeanFactoryPostProcessor,这个类后面会单独写一篇文章来对其里面的核心处理过程分析。

更多技术文章可以查看: vnjohn 个人博客

猜你喜欢

转载自blog.csdn.net/vnjohn/article/details/125992571