Spring源码的简单分析

前言

前段时间面试的时候被问到了Spring的源码,问的其实也不算深,但由于距离上次看Spring源码也隔了挺久的了,差不多都忘了,导致基本都没回答出来。

为了巩固同时也为了搞懂一些以前不太清楚的问题,于是有了这篇文章。全文较长,但总体而言不难理解,就是从宏观上分析一下一些常见问题:
Spring的创建过程bean的生命周期bean的创建过程beanDefinition的创建过程Spring Boot的启动过程以及一些小点

内容参考:

  1. Spring详细源码分析:https://www.bilibili.com/video/BV1T54y1n7PB/?p=77
  2. IOC和AOP概况分析:https://www.bilibili.com/video/BV1584y1r7n6

Spring的创建过程

首先要搞清的是,我们每次创建的Spring应用,主要分为两种接口,BeanFactoryApplicationContext,其中后者继承了前者,拥有比前者更多的功能,比如AOP。

在 Spring 早期版本中,使用 XmlBeanFactory 是一种创建 Spring 容器的方式。
XmlBeanFactory 实现了 BeanFactory 接口,可以从 XML 配置文件中解析 Bean 的定义
并初始化 Bean 对象。在这个版本中,Spring 并没有直接集成 AOP 功能,但是可以通过
Spring AOP 模块集成第三方 AOP 框架,如 AspectJ 或 JBoss AOP。

但是,随着 Spring 版本的不断升级更新,XmlBeanFactory 被废弃,并被
ApplicationContext 所取代。ApplicationContext 继承了 BeanFactory 接口,并为
用户提供了更多的功能支持,其中就包括对 AOP 的原生支持。因此,如果你要使用
Spring AOP,建议使用 ApplicationContext 作为你的 Spring 容器,而不是过时的
XmlBeanFactory。

详细可参考Java Guide上面写的。

这两个接口的实现类,就相当于是一个工厂,这里就运用到了工厂模式。只不过这个工厂有很多复杂的功能,这也侧面说明工厂模式的一个好处就是集中管理对象创建的话,能够统一增加一些额外的功能。

其中Spring对于要求单例的对象是通过ConcurrentHashMap来管理的,以保证线程安全。同时在创建单例对象时,也会用到sychronized锁,具体参照org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton方法。

bean的生命周期

bean的生命周期主要分为:初始化、使用、销毁三个阶段。且可以通过下列方法进行控制:

  1. 实例化:当 Spring 容器启动时,它会通过反射机制实例化所有在配置文件或注解中配置的 Bean。这是 Bean 生命周期的起始阶段。

  2. 属性赋值:实例化之后,Spring 会根据配置文件或注解中指定的属性值,为 Bean 的属性进行赋值。这是 Bean 生命周期的第二个阶段。

  3. 自定义初始化方法:当属性赋值完成后,Spring 会调用自定义初始化方法,可以通过实现 InitializingBean 接口或在配置文件或注解中指定 init-method 方法来实现。这是 Bean 生命周期的第三个阶段。

  4. 使用:初始化方法执行完毕后,Bean 就可以被应用程序使用了。在此阶段,Bean 可以接收各种请求和调用,为应用服务。

  5. 自定义销毁方法:当应用程序结束时,Spring 容器会调用 Bean 的自定义销毁方法。可以通过实现 DisposableBean 接口或在配置文件或注解中指定 destroy-method 方法来实现。这是 Bean 生命周期的最后一个阶段。

bean的创建过程

首先,通过断点Spring容器的创建,我们可以知道是在org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization这个方法中,bean被创建了出来。

一路断点,可以来到org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons方法,这个方法我们可以看到是根据beanName来查找beanDefinition来判断是否存在该bean的。

这里是不断遍历beanDefinitionNames集合去创建bean,注意到集合是List类型,说明保留了顺序,即 这里创建bean是严格按照顺序去创建的。

另外如果该bean是抽象的,或者非单例的,或者懒加载的,都不会被初始化。这里值得注意的一点就是只有这样的bean是在beanFactory创建时就随着一起初始化了,其他类型的bean都不会这样。

最终我们会来到org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean方法。这个方法控制着bean的创建和获取,即 这个方法用来获取bean,如果获取不到,就调用不同情况进行处理,并调用org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean进行初始化,然后再返回bean。值得注意的是在调用createBean()之前,会先准备好该bean构造依赖,循环依赖的报错也是在这个时候产生的。

上面说到的createBean()是创建bean的核心方法,里面是bean创建前后的整个过程。其中创建bean的话,会来到org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean这个方法,如下图:

doCreateBean()中可以将bean的创建过程分为三个部分:构造对象createBeanInstance()、填充属性populateBean()、初始化实例initializeBean()注册销毁registerDisposableBeanIfNecessary()。见下图:

构造对象阶段就单纯的是创建一个对象

填充属性阶段就是对标有@Autowire注解的属性进行注入,这里就用到了三级缓存,还记得之前提到的doGetBean()吗,这里一来会先调用getSingleton(),而这个查找会向三级缓存中逐级查找,如下图:

如果allowEarlyReferencetrue,那么就会执行刚刚添加的singletonFactory,已完成AOP操作。同样如果在创建完对象开始属性填充时,发生了循环依赖,这时也没关系,因为会在三级缓存中查找。

初始化实例阶段主要就是调用我们写好的各种钩子函数,比如实现的BeanNameWare接口、实现的initializingBean接口。AOP也是这个时候执行的,这里并不用担心在三级缓存时做过一次AOP之后,这里再做一次AOP,Spring会把做过AOP的bean放入集合以判断bean时候已经AOP过。

注册销毁阶段就是将实现销毁接口的bean进行注册

关于三级缓存详细可以参考这篇文章:Spring中的循环依赖及解决

上面doGetBean()这一段代码同样也解释了为什么如果是构造器循环依赖的话,则无法解决(因为至少要初始化完对象后,才会加入缓存,注意看addSingletonFactory执行的位置),如果是prototype的话,也无法解决(因为addSingletonFactory的前置条件是这个bean是单例模式)。

beanDefinition的创建过程

对于Spring应用,会根据创建的ApplicationContext的类型(ClassPathXmlApplication、FileSystemXmlApplication、XmlWebApplicationContext),去采用不同的策略读取xml文件以完成beanDefinition的创建。

首先要明确一个思路,上面说到了,Spring IOC的核心就是beanFactory。在我们创建ApplicationContext的过程中,会调用org.springframework.context.support.AbstractApplicationContext#refresh,这个方法完成了beanFactory的创建。

在这个方法中,首先会通过obtainFreshBeanFactory()创建一个beanFactory。在obtainFreshBeanFactory()中会调用到方法org.springframework.context.support.AbstractXmlApplicationContext#loadBeanDefinitions完成beanDefinition的创建。这里会创建一个XmlBeanDefinitionReader来读取我们启动程序时传入的xml文件位置,解析标签并封装成BeanDefinition类型对象,将其存储在beanFactory的beanDefinitionMap(也是ConcurrentHashMap类型)中。BeanDefinition就相当于bean的信息,bean就是依靠这些信息反射创建的。

这里提到的处理方式是基于ClassPathXmlApplication。不同ApplicationContext的实现类获取beanDefinition方式会有略微不同。

beanFactory创建后,refresh()会再调用finishBeanFactoryInitialization(),完成单例非懒加载的bean的创建,就和我们上面说到的一样,部分bean是在beanFactory创建的过程中创建的。

Spring Boot的启动过程

Spring Boot启动分为四个阶段:服务构建、环境准备、容器创建、填充容器

详细可参考上面第二个链接。

任何Spring Boot项目都要运行SpringApplication.run()(注意这里是SpringApplication类的静态run())以启动。这个方法首先会创建一个SpringApplication类型对象,这个对象创建过程中会完成很多必要的配置ApplicationContext的准备操作(比如获得当前系统配置信息、根据classpath下class文件判断web应用类型、打印banner图等等操作)。

然后会调用刚刚创建的SpringApplication类型对象的run()

这个方法中会完成容器(ApplicationContext)的创建。其中createApplicationContext()会初始化一个ApplicationContext,将刚刚获得到的信息进行组合,同时prepareContext()中会初始化好beanDefinitionMap,之后会调用refreshContext()进行beanFactory的相关操作。

值得注意的是refreshContext()进行beanFactory的相关操作,这里和Spring中一样,也会来到org.springframework.context.support.AbstractApplicationContext#refresh中:

同样也是调用obtainFreshBeanFactory()创建一个beanFactory,但是这个方法此时不做任何操作,因为Spring Boot是用ServletWebServerApplicationContext作为容器,beanFactory已经创建好了,此时调用obtainFreshBeanFactory()实际上就不会做任何操作了。

另外Tomcat服务器的启动是在该方法中的onRefresh()中完成的。

关于refresh()方法中调用的各个方法详细介绍可以参考视频:https://www.bilibili.com/video/BV1hv4y1z7PQ。

另外关于Spring Boot中beanDefinition的扫描就和上面说得那种方式不一样。Spring Boot开始时就已经有一些基本的beanDefinition,比如我们自己程序的启动类,然后它是通过调用invokeBeanFactoryPostProcessors(),执行各种beanFactory的后置处理器,其中最重要的就是ConfigurationClassPostProcessor,它会扫描所有配置类,并用ConfigurationClassParser取扫描它们

它实现了BeanFactoryPostProcessor接口的postProcessBeanFactory(),可以看到这个方法会调用processConfigBeanDefinitions()去处理,这里会调用ConfigurationClassUtils.checkConfigurationClassCandidate()判断是否是配置类要不要进行处理,跟踪这个方法的源码就可以知道其实这里说的配置类不一定得是有@Configuration,有@Component@ComponentScan@Import@ImportResource都可以被处理。

之后就会调用ConfigurationClassParserparse()方法去处理前面扫描到的配置类。这个方法最终会来到doProcessConfigurationClass()去递归处理所有配置类(这里的配置类也不只是指有@Configuration的才算,任何直接或间接标注了@Component的类都算)。

以上这一小段关于Spring Boot自动配置的过程,详细可参考视频:https://www.bilibili.com/video/BV1NY411P7VX

至此Spring Boot启动的主要的四个步骤全部完成。

一些小点

Spring是怎么根据类型查找对应bean的?

Spring的beanFactory中有一个map集合allBeanNamesByType,键为Class类型,存储依赖类型,值为String[],存储所有单例bean和非单例bean名称(id)。

Spring中FactoryBean是怎么存储的?

如果是通过xml文件去配置一个bean的静态或者非静态工厂方法(factory-method),那这个原理不难想清,这里主要讨论的是当利用FactoryBean接口时,Spring是怎么处理的。

首先对于beanDefinition,实现了FactoryBean并注入容器了的类和其他注入到容器中的类一样,只会产生一个beanDefinition,并且这个beanDefinition的类型就是该类的类型,而非其要产生的对象(getObject())的类型

那么照理来说Spring只能拿到FactoryBean类型的bean,它是怎么拿到其产生的对象的呢?

经过上面的推到我们知道bean最终都是通过doGetBean()去获取,这里在获取到bean之后,还是会调用getObjectForBeanInstance(),如下图:

这个方法不展开讲了,大致就是判断此次是获取bean还是bean的factory(注意到这里传进去了name和beanName)。如果是获取factoryBean,那么直接返回就好了,因为最开始beanDefinitionMap里面存储的就是该工厂bean(factoryBean)。如果是想获得该工厂产生的对象,那么视情况调用工厂方法去创建对象,因为如果确定了该对象是单例模式,Spring会将其缓存在beanFactoryfactoryBeanObjectCache,这样创建过一次的对象直接在这里取就行了。

注意到工厂bean产生的对象并不存储在singletonObjects中。

为什么@AutoConfiguration可以延后注册

我们知道配置类总是会先于自动配置类,这里来讲讲为什么是这样。具体原理有点绕,这里分几步讲解。

注册自动配置类

首先介绍一下怎么注册自动配置类。不同于普通的配置类加注解即可,自动配置类的注册会不一样。

在Spring Boot 2.7.0之前,还没有@AutoConfiguration的时候,我们应该在项目路径创建文件夹META-INF/spring.factories,然后在文件中如下配置:

这里忘了格式其实也不要紧,随便看一个有自动配置功能的jar,都会有这个文件,模仿格式即可。

在Spring Boot 2.7.0之后,有@AutoConfiguration的时候,我们应该在项目路径创建文件夹META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,然后在文件中如下配置:

这里就只需要写配置类的全限定类名就行了。

然后记得给该类加上@Configuration/@AutoConfiguration就行了。

另外3.0之后只能使用第二种方式声明自动配置类了。

配置类、自动配置类并不是指的是有没有@AutoConfiguration(毕竟这个注解是Spring Boot 2.7.0之后才有的,但是自动配置类这个概念早就有了),而是从注册方式去区别的。

自动配置类多了很多功能,比如安排顺序,使得我们使用@ConditionalOn...这类注解时更加方便,因为这类注解是当即判断,即 加载时立即判断是否符合条件,所以对加载顺序很敏感。

比如beanA和beanB都在路径下,beanA只在beanB注入容器时才注入,但如果由于顺序安排不当导致beanA先注入,此时beanB还未注入,就会导致beanA注入失败。

依靠@AutoConfigureOrder,我们可以更改配置类的初始化顺序,注意不同于@Order,只能修改AOP顺序和bean注入时的自动装配顺序(详情可参考博客:Bean加载顺序之错误使用姿势辟谣,这里不展开介绍了)。

此外@AutoConfigureBefore等等也可以调整自动配置类的加载顺序,而这些注解统一都被@AutoConfiguration一个注解搞定了(都成了它的一个属性),另外还值得注意的就是@AutoConfigurationproxyBeanMethods默认为false,以提高Spring Boot的启动速度。

怎么扫描的自动配置类

上面我们注册了自动配置类,那么Spring Boot是怎么扫描的呢?

这点其实很多博客都提到了,就是我们Spring Boot的启动类上的注解@SpringBootApplication,这个注解中包含@EnableAutoConfiguration,而这个注解中又包含@Import(AutoConfigurationImportSelector.class)

其中这里就引入类AutoConfigurationImportSelector,这是一个ImportSelector,它会去扫描beanDefinition,所以直接看该类的selectImports()就可以搞清是怎么回事了,这里不展开说了,最后是用ClassLoadergetResources()去扫描那两个文件(因为3.0之前,spring.factories还是兼容的,可以一起使用)。

所以当递归解析Spring Boot项目的启动类时(上面提到的ConfigurationClassParser),当解析到启动类的@Import时,就会找到AutoConfigurationImportSelector,从而引入我们注册的自动配置类。

怎么保证自动配置类的扫描一定在配置类之后

由上面的分析我们知道了自动配置类是如何被扫描到的,但只依据上面说的,还无法保证自动配置类的扫描就一定在配置类之后,但现在基于上面的知识,这一点也能被推敲出来了。

上面提到的AutoConfigurationImportSelector,不仅是一个ImportSelector,更主要是它实现了DeferredImportSelector接口,这个接口继承了ImportSelector,而这就是核心点。

还记得上面提到是用ConfigurationClassParserparse()去递归扫描beanDefinition吗,这个方法中在扫描的最后会调用前面扫描中获得的DeferredImportSelector实现类,延后执行:

其实只看DeferredImportSelector的注释,上面也说清楚了,这个ImportSelector会在扫描完所有的配置类之后执行,上面的源码只是其保证该行为的直接证明。

但不知道大家是否还会有一个疑问,就是假如我们在自己启动类的同一路径下,如果声明了一个配置类,然后又在spring.factories中注册了该类,那么这个类的行为算配置类,还是自动配置类?

这个问题的答案其实是自动配置类。翻看ConfigurationClassParserparse()的源码我们会发现它其实会先判断是否是自动配置类org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter#getAutoConfigurations,如果是的话就不会现在加载。

猜你喜欢

转载自blog.csdn.net/weixin_55658418/article/details/131216667