前言
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 个人博客