前言

  开心一刻   

    女儿: “妈妈,你这么漂亮,当年怎么嫁给了爸爸呢?”
    妈妈: “当年你爸不是穷嘛!‘
    女儿: “穷你还嫁给他!”
    妈妈: “那时候刚刚毕业参加工作,领导对我说,他是我的扶贫对象,我年轻理解错了,就嫁给他了!”
    女儿......

@Import注解应用

  应用开发中,当我们的功能模块比较多时,往往会按模块或类别对Spring的bean配置文件进行管理,使配置文件模块化,更容易维护;spring3.0之前,对Spring XML bean文件进行拆分, 例如

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <import resource="config/user.xml"/>
<import resource="config/role.xml"/>
<import resource="config/permission.xml"/> </beans>

  spring3.0及之后,引入了@Import注解,提供与Spring XML中的<import />元素等效的功能;spring4.2之前,@Import只支持导入配置类(@Configuration修饰的类、ImportSelector实现类和ImportBeanDefinitionRegistrar实现类),而spring4.2及之后不仅支持导入配置类,同时也支持导入常规的java类(如普通的User类)

  示例地址:spring-boot-autoconfig,四种都有配置,不用down下来运行,看一眼具体如何配置即可

  运行测试用例,结果如下

  可以看到,Dog、Cat、Role、User、Permission的实例都已经注册到了spring容器,也就是说上述讲的@Import的4种方式都是能够将实例注册到spring容器的

@Import注解原理

  @Import何以有如此强大的功能,背后肯定有某个团队在运作,而这个团队是谁了,就是spring;spring容器肯定在某个阶段有对@Import进行了处理,至于spring是在什么时候对@Import进行了怎样的处理,我们来跟一跟源码;ConfigurationClassPostProcessor实现了BeanDefinitionRegistryPostProcessor,那么它会在spring启动的refresh阶段被应用,我们从refresh的invokeBeanFactoryPostProcessors方法开始

  ConfigurationClassPostProcessor

    注意此时spring容器中的bean定义与bean实例,数量非常少,大家可以留心观察下

    一路跟下来,我们来到processConfigBeanDefinitions方法,该方法会创建一个ConfigurationClassParser对象,该对象会分析所有@Configuration注解的配置类,产生一组ConfigurationClass对象,然后从这组ConfigurationClass对象中加载bean定义

  ConfigurationClassParser

    主要是parse方法

public void parse(Set<BeanDefinitionHolder> configCandidates) {
this.deferredImportSelectors = new LinkedList<>(); // 通常情况下configCandidates中就一个BeanDefinitionHolder,关联的是我们的启动类
// 示例中是:com.lee.autoconfig.AutoConfigApplication
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
// 被@Configuration注解修饰的类会被解析为AnnotatedGenericBeanDefinition,AnnotatedGenericBeanDefinition实现类AnnotatedBeanDefinition接口
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
} // 处理延迟的ImportSelector,这里本文的重点:自动配置的入口
processDeferredImportSelectors();
}

    从启动类(示例中是com.lee.autoconfig.AutoConfigApplication)开始,递归解析配置类以及配置类的父级配置类;边跟边注意beanFactory中beanDefinitionMap的变化,ConfigurationClassParser对象有beanFactory的引用,属性名叫registry;我们可以仔细看下doProcessConfigurationClass方法

/**
* 通过从源类中读取注解、成员和方法来构建一个完整的配置类:ConfigurationClass
* 注意返回值,是父级类或null(null包含两种情况,没找到父级类或之前已经处理完成)
*/
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException { // 递归处理配置类内置的成员类
processMemberClasses(configClass, sourceClass); // 处理配置类上所有@PropertySource注解
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
} // 处理配置类上所有的@ComponentScan注解,包括@ComponentScans和ComponentScan
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// 立即扫描@ComponentScan修饰的配置类,
// 通常是从启动类所在的包(示例中是com.lee.autoconfig)开始扫描,扫描配置类(被@Configuration修饰的类)
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// 进一步检查通过配置类扫描得到的bean定义集,并在需要时递归解析
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
} // 处理配置类上所有的@Import注解
// 包括@Import支持的4种类型:ImportSelector、ImportBeanDefinitionRegistrar、@Configuration和普通java类
// 普通java类会被按@Configuration方式处理
processImports(configClass, sourceClass, getImports(sourceClass), true); // 处理配置类上所有的@ImportResource注解,xml方式的bean就是其中之一
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
} // 处理配置类中被@Bean修饰的方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
} // 处理默认的方法或接口
processInterfaces(configClass, sourceClass); // 处理父级类,如果有的话
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
} // No superclass -> processing is complete
return null;
}

    上述代码中写了相关注释,有兴趣的同学可以更进一步的去跟,这里我只跟下processImports方法,因为这个与自动配置息息相关

    起始的ConfigurationClass包括:1、工程中所有我们自定义的被@Configuration修饰的类,示例中就只有AnimalConfig;2、应用的启动类,示例中是:AutoConfigApplication。

    我们自定义的ConfigurationClass一般不会包含多级父级ConfigurationClass,例如AnimalConfig,就没有父级ConfigurationClass,解析就比较简单,我们无需关注,但AutoConfigApplication就不一样了,他往往会被多个注解修饰,而这些注解会牵扯出多个ConfigurationClass,需要递归处理所有的ConfigurationClass;上图中,我们跟到了一个比较重要的类:AutoConfigurationImportSelector,实例化之后封装成了DeferredImportSelectorHolder对象,存放到了ConfigurationClassParser的deferredImportSelectors属性中

自动配置源码解析

  有人可能有这样的疑问:哪来的AutoConfigurationImportSelector,它有什么用? 客观莫急,我们慢慢往下看

  我们的应用启动类被@SpringBootApplication,它是个组合注解,详情如下

  相信大家都看到@Import(AutoConfigurationImportSelector.class)了,ConfigurationClassParser就是从此解析到的AutoConfigurationImportSelector,至于AutoConfigurationImportSelector有什么用,马上揭晓;我们回到ConfigurationClassParser的parse方法,里面还有个很重要的方法:processDeferredImportSelectors,值得我们详细跟下

  processDeferredImportSelectors

    说的简单点,从类路径下的所有spring.facoties文件中读取全部的自动配置类(spring.factories文件中org.springframework.boot.autoconfigure.EnableAutoConfiguration的值),然后筛选出满足条件的配置类,封装成ConfigurationClass,存放到ConfigurationClassParser的configurationClasses属性中

    说的详细点,分两个方法进行说明

      selectImports方法

public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 从类路径下的spring.factories文件中读取所有配置类(org.springframework.boot.autoconfigure.EnableAutoConfigurationd的值)
// 得到所有配置类的全路径类名的集合 - 数组
// 此时得到的是类名,至于该类存不存在,还需要在下面步骤中进行检验
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
// 去重重复的
configurations = removeDuplicates(configurations);
// 获取需要排除的配置类,@SpringBootApplication exclude和excludeName的值
// 以及配置文件中spring.autoconfigure.exclude的值
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 验证排除的配置类是否存在 - 类路径下是否存在该类
checkExcludedClasses(configurations, exclusions);
// 剔除需要排除的配置类
configurations.removeAll(exclusions);
// 进行过滤 - 通过配置类的条件注解(@ConditionalOnClass、@ConditionalOnBean等)来判断配置类是否符合条件
configurations = filter(configurations, autoConfigurationMetadata);
// 触发自动配置事件 - ConditionEvaluationReportAutoConfigurationImportListener
fireAutoConfigurationImportEvents(configurations, exclusions);
// 返回@Import方式 所有满足条件的配置类
return StringUtils.toStringArray(configurations);
}

        从类路径下的所有spring.facoties文件中读取org.springframework.boot.autoconfigure.EnableAutoConfiguration的所有值,此时获取的是全路径类名的数组,然后进行筛选过滤,1、先去重处理,因为多个spring.factories中可能存在重复的;2、然后剔除我们配置的需要排除的类,包括@SpringBootApplication注解的exclude、excludeName,以及配置文件中的spring.autoconfigure.exclude;3、条件过滤,过滤出满足自己条件注解的配置类。最终获取所有满足条件的自动配置类,示例中有24个。

        条件注解更详细的信息请查看:spring-boot-2.0.3源码篇 - @Configuration、Condition与@Conditional,读取spring.facoties文件的详细信息请查看:spring-boot-2.0.3启动源码篇一 - SpringApplication构造方法

      processImports方法

        这个方法在解析ConfigurationClassParser的parse方法的时候已经用到过了,只是没有做说明,它其实就是用来处理配置类上的@Import注解的;上述selectImports方法解析出来的配置类,每个配置类都会经过processImports方法处理,递归处理@Import注解,就与递归处理我们的启动类的@Import注解一样,从而获取所有的自动配置类;springboot的自动配置就是这样实现的。

  此时还只是获取了满足条件的自动配置类,配置类中的bean定义加载还没有进行,我们回到ConfigurationClassPostProcessor的processConfigBeanDefinitions方法,其中有如下代码

// 各种方式的配置类的解析,包括springboot的自动配置 - @Import、AutoConfigurationImportSelector
parser.parse(candidates);
parser.validate(); Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed); // Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses); // 将配置类中的bean定义加载到beanFactory

  至此,springboot的自动配置源码解析就完成了,有兴趣的可以更近一步的深入

总结

  1、各个方法之间的调用时序图如下,结合这个时序图看上面的内容,更好看懂

  2、springboot自动配置底层依赖的是SpringFactoriesLoader和AutoConfigurationImportSelector;@EnableAutoConfiguration注解就像一个八爪鱼,抓取所有满足条件的配置类,然后读取其中的bean定义到spring容器,@EnableAutoConfiguration得以生效的关键组件关系图如下

springboot2.0.3源码篇 - 自动配置的实现,发现也不是那么复杂的更多相关文章

  1. spring-boot-2.0.3源码篇 - pageHelper分页,绝对有值得你看的地方

    前言 开心一刻 说实话,作为一个宅男,每次被淘宝上的雄性店主追着喊亲,亲,亲,这感觉真是恶心透顶,好像被强吻一样.........更烦的是我每次为了省钱,还得用个女号,跟那些店主说:“哥哥包邮嘛么叽. ...

  2. spring-boot-2.0.3源码篇 - filter的注册,值得一看

    前言 开心一刻 过年女婿来岳父家走亲戚,当时小舅子主就问:姐夫,你什么时候能给我姐幸福,让我姐好好享受生活的美好.你们这辈子不准备买一套大点的房子吗?姐夫说:现在没钱啊!不过我有一个美丽可爱的女儿,等 ...

  3. spring-boot-2.0.3源码篇 - 国际化

    前言 针对spring boot,网上已有很多优质的系列教程,我就不再班门弄斧了(实际上是担心没别人写的好,哈哈哈!).但是还是想蹭蹭spring boot的热度,即使不考虑微服务,spring bo ...

  4. spring-boot-2.0.3源码篇 - @Configuration、Condition与@Conditional

    前言 开心一刻 一名劫匪慌忙中窜上了一辆车的后座,上车后发现主驾和副驾的一男一女疑惑地回头看着他,他立即拔出枪威胁到:“赶快开车,甩掉后面的警车,否则老子一枪崩了你!”,于是副驾上的男人转过脸对那女的 ...

  5. Spring Boot源码探索——自动配置的内部实现

    前面写了两篇文章 <Spring Boot自动配置的魔法是怎么实现的>和 <Spring Boot起步依赖:定制starter>,分别分析了Spring Boot的自动配置和起 ...

  6. spring-boot-2.0.3启动源码篇 - 阶段总结

    前言 开心一刻 朋友喜欢去按摩,第一次推门进来的是一个学生美眉,感觉还不错:后来经常去,有时是护士,有时是空姐,有时候是教师.昨天晚上推门进去的是一个女警察,长得贼好看,身材也很好,朋友嗷的一声就扑上 ...

  7. spring-boot-2.0.3启动源码篇二 - run方法(一)之SpringApplicationRunListener

    前言 Springboot启动源码系列还只写了一篇,已经过去一周,又到了每周一更的时间了(是不是很熟悉?),大家有没有很期待了?我会尽量保证启动源码系列每周一更,争取不让大家每周的期望落空.一周之中可 ...

  8. 深入浅出Mybatis系列(五)---TypeHandler简介及配置(mybatis源码篇)

    上篇文章<深入浅出Mybatis系列(四)---配置详解之typeAliases别名(mybatis源码篇)>为大家介绍了mybatis中别名的使用,以及其源码.本篇将为大家介绍TypeH ...

  9. 深入浅出Mybatis系列(四)---配置详解之typeAliases别名(mybatis源码篇)

    上篇文章<深入浅出Mybatis系列(三)---配置详解之properties与environments(mybatis源码篇)> 介绍了properties与environments, ...

随机推荐

  1. FlowerVisor理解

    Ï来自FlowVisor: A Network Virtualization Layer这篇论文的理解 1. 简介 论文讲述如何虚拟化一个网络,并描述一个特殊的系——FlowVisor 网络虚拟化用来 ...

  2. mysql练习

    1.表关系 创建如下表格,并创建相关约束 ##(1)创建一个数据库 create database db2 default charset utf8; ##切换到db2数据库中 use db2 ##创 ...

  3. Vue知识点总结

    1.属性名已$开头的都是内部提供的属性 2.为什么使用事件修饰符的原因:methods 只有纯粹的数据逻辑,而不是去处理 DOM 事件细节 3.v-if 如果值为false,元素在页面中不存在:值为t ...

  4. Openstack的视频学习

    0.安装环境准备 部署架构: 网络模式(红色Net0为管理网络,Net1接外网,Net2是接虚拟机网络流量的): 虚拟化平台为VirtualBox,虚拟网络Host-Only网络的配置: Net0:管 ...

  5. AJAX-同源策略 跨域访问

    ## 同源策略 概述: 同源策略是浏览器的一种安全策略,视为同源是指域名,协议,端口完全相同.只有同源的地址才可以通过AJAX方式请求.同源或者不同源说的是两个地址的关系,不同源地址之间请求我们称之为 ...

  6. ubuntu18.04新体验

    虽然ubuntu18.04LST版本早出来了,但自己原来的ubuntu16.04还可以用,就懒得折腾了. 但最近ubuntu崩了,就想尝尝鲜...结果发现还挺好用的,准确地说,ubuntu是越来越好用 ...

  7. Live2D插件--漂浮的二次元小姐姐

    这个插件找了很久,都没找到,今天偶然翻到一个小哥的博客发现了这个,果断偷走. 教程转自简书:https://www.jianshu.com/p/1cedcf183633 还有这些,你可能有用 修改位置 ...

  8. python处理参数的getopt的使用

    在写脚本程序的时候需要添加一些额外的参数来实现脚本的附加功能或者增强功能,通常的做法是同sys.argv[i]直接来获取参数的值,但是这个比较局限,要求参数的输入一定要按照顺序. fileName = ...

  9. ubuntu 修改系统时间无效

    用root用户修改服务器时间无效:使用hwclock -w也不行 解决方法: 需要取消自动从互联网同步时间才可以的 timedatectl set-ntp 0 上面的命令可以关闭自动同步,然后你再设置 ...

  10. 1.8 Double-Opening and Virtual Machine

    Since plug-in will be replaced by RN as following years, what is the future of plug-in? the answer i ...