Spring源码笔记(一):追根刨底IOC

  • 内容
  • 评论
  • 相关

 

很多人信奉“不去造轮子”,看似性价比很高的一句话,却不知道这句话是有前提的,那就是“你得知道轮子的内部结构”,否则就是自我麻痹

 

关于Spring,作为一个javaer必须要了解的框架,我觉得我们有必要对其追根刨底。

 

废话不多说,干货才是硬道理。

 

1、首先,我们从main函数入口。

 

2、进入SpringApplication类内部,调用构造方法和run方法。

 

3、在构造方法中,初始化系统资源,如:primarySources,咱们的启动类;如webApplicationType,这个参数在Spring Boot 1.x的时候,类型是一个boolean值,在当前版本,有三个值,NONE,SERVLET,REACTIVE。三个参数来源于WebAplicationType枚举类。这里我们返回的是SERVLET,代表启动Servlet Web服务器。

 

 

4、紧接着,我们进入run方法,让我们看一下run方法大概是个什么造型,其中最为主要的有createApplicationContext、refreshContext等。

 

5、当我们进入createApplicationContext内部,Spring Boot为我们创建容器,这时候我们使用在构造初始化完成的参数:webApplicationType创建相对应的容器,这里通过反射创建容器org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext。

 

6、接下来看一下这个容器的继承结构,为什么要了解一下其继承结构呢?因为在后续的使用过程中面向接口编程,我们需要知道其继承结构,才能找到对应执行的源码。

 

7、接下来继续回到run方法中,继续往下走,准备执行一个非常重要的方法:refresh。

 

8、我们沿着源码继续往深处走。

这里调用我们容器的refresh方法,而容器是AnnotationConfigServletWebServerApplicationContext,该容器没有实现refresh方法,这时候在之前了解到的继承结构就有派场了,父类ServletWebSerApplicationContext实现了方法。

 

9、继续往上走,AbstractApplicationContext,经过一系列准备,我们终于来到核心地带。

 

10、AbstractApplicationContext存在spring-context包下,接下来我们来分析分析容器的初始化过程。这里obtainFreshBeanFactory方法会创建一个beanFactory,如果存在旧的则销毁旧的再创建新的。

 

11、在obtainFreshBeanFactory方法中,会调用父类的refreshBeanFactory方法,我们再次利用继承结构,我们会进入父类GenericApplicationContext当中,而不是AbstractRefreshableApplicationContext。在以前,如果是基于xml方式去生成bean,我们经常会用到FileSystemXmlApplicationContext或ClassPathXmlApplicationContext去当我们的容器,这个时候就会进入父类AbstractRefresnableApplicationContext当中去初始化容器。

 

12、在GenericApplicationContext当中,其实我们早就已经初始化容器了,在之前没有提及,我们在初始化AnnotationConfigServletWebServerApplicationContext容器的时候,这个容器的父类继承ServletWebServerApplicationContext,而它又继承GenericWebApplicationContext,而它又继承GenericApplicationContext。

所以我们我们此时已经拥有了beanFactory,它的实现是DefaultListableBeanFactory,接下来看源码。

 

13、我们验证一下这个beanFactory的类型,其实在之前基于XML的方式也是DefaultListableBeanFactory,所以这个容器类就变得至关重要了。

 

14、接下来,我们要进入invokeBeanFactoryPostProcessors方法进行初始化IOC容器。若是以基于XML配置的,IOC容器初始化则是在obtainFreshBeanFactory内。

 

15、执行invokeBeanFactoryPostProcessors会做一层封装,然后进入PostProcessorRegistrationDelegate的invokeBeanFactoryPostProcessors。

 

16、在这个方法里,经过一系列配置后,进入invokeBeanDefinitionRegistryPostProcessors。

 

17、我们继续往深处走

 

18、我们看到postProcessor的类型为ConfigurationClassPostProcessor,所以在进入postProcessBeanDefinitionRegistry我们也需要进入该类,然后逐步分析源码。

 

19、我们继续深入,没有办法,封装的很严实,很深。

 

20、我们马上就要注册bean实现IOC容器的初始化了,但是在这之前,我们需要扫描所有的类。这里我们传入的candidates,是我们的启动类DemoApplication,在这个类上,有@SpringBootApplication注解。
在这里,Spring会去扫描Bean并注册。

 

21、继续往下走,我们传进来的configCandidates是我们的启动类集合,只有一个,bean被BeanDefinition封装这,而BeanDefinition又被BeanDefinitionHolder封装着,继续进入parse方法。

 

22、继续往下走,进入processConfigurationClass方法。

 

23、这个时候Spring才准备开始扫描指定包下的所有类,继续前进。

 

24、在这里,Spring又进行一系列准备,继续深入parse方法。

 

25、Spring继续进行一系列准备操作,这次是对包的一个过滤操作,最后执行doScan方法。在Spring中,以do开始的都是小弟,都是干活的,我们传入的包basePackages,是我们指定的包,如果没有指定,那就将启动类所在目录作为扫描包,Spring将扫描该包下的所有类(带注解或不带注解,都会被扫描,在后续会对不是Spring注解的进行一个过滤,不进行注册。)。

 

26、在doScan里,Spring会扫描所有的类,然后解析,是需要注册的Bean,在这里进行注册,完成IOC容器beanDefinitionMap的一个初始化过程。具体来看,findCandidateComponents方法是找到所有候选组件,也就是可进行注册的Bean,也就是说,在这个方法里,Spring返回的BeanDefinition集合,都是需要进行注册的,在方法内部已经完成筛选。

 

27、这里又做了一层过滤,我们继续往下走。

 

28、在这里就是准备resources,准备解析class,解析完成后做一个筛选,然后返回给IOC进行注册。

这里获取metadataReaderFactory,类型是CachingMetadataReaderFactory,看名字也能看出来,优先使用缓存。

代码如下,继承结构如下。

 

29、缓存得不到,我们就去父类获取metadataReader,再放入缓存,以便下次直接从缓存获取,而它的父类在上面提到,父类是SimpleMetadaReaderFactory。

 

30、进入父类,new了一个SimpleMetadataReader,继续深入,看构造方法。

 

31、在这个构造方法里,初始化了这个resource所对应的class,然后去解析这个class。继续看accept方法。

 

在这个里面就不用再看了,基本上已经到底了,这里就是ClassReader如何去解析了。

好,现在我们蓦然回首,你会发现,这条路崎岖不堪。

跑题了,我们言归正传。

 

32、

当这个类被解析完毕后,被visitor装载,而在解析的下面几行代码,将visitor赋值给SimpleMetadaReader对象的两个属性里,所以我们在第28的时候,这个metadataReader是SimpleMetadataReader对象,且两个成员属性是一样的,都是这个visitor,它是AnnotationMetadataReadingVisitor类的对象,装载着这个resource对应class的所有信息,尤其是注解(因为我们本身就是基于注解开发)。

 

33、

我们看到第28,我们获取到metadaReader后会对它进行判定,判断是否为候选组件,也就是说有没有注解(当然,Spring做了比这更多的事情)。

 

在这里,this.excludeFilters是一个List,包含是三个TypeFilter,其中有一个是我们的启动类DemoApplication,因为在beanFactory已经注册过了,所以这里不需要再重复注册。

this.includeFilters是包含的规则,它也是一个List,里面两个TypeFilter,分别是Component和ManageBean,前者是Spring提供的,后者是Java自带的。也就是说只要在扫描包内类上标志这两个注解,都会提供给IOC注册。

但是问题来了,我们平时用的@Controller,@RestController,@Service,@Repository,@Configuration等等这些注解也不是@Component啊?里面怎么搞?

别着急,我们先来看一下Controller注解

Spring的@Component上有@Indexed,代表可被继承,所以@Controller继承@Component,自然而然只需要判断@Component就可以达到Spring想要的效果了。

 

34、我们回到26,我们得到了candidates,它是需要注册的bean集合,后续进行遍历。

这里,我们先思考一个问题,我们知道IOC容器是一个Map,

如果Bean名称重复,它是如何解决的?

在第26,余下的部分代码没有截图,这里奉上

 

35、在这个方法里,Spring解决bean名称冲突的问题

 

36、Spring到底如何比较两个名称一致的bean?

 

里面进行三个判断,如果满足其中一个,则会返回true,然后在外面一层,则不会抛异常,返回false,然后跳过该bean的注册。

第一个:

beanDefinition是否是ScannedGenericBeanDefinition。细心的读者会发现,我们目前所生成的BeanDefinition就是ScannedGenericBeanDefinition,为什么,请看第28,在这里,有这样一行代码:

如果是Spring自己扫描的,则就是一个这样的Bean,如果是其他方式注册的,就可能不属于ScannedGenericBeanDefinition

false,再取反,true,进入方法,返回false,跳过本次注册。

第二个第三个:比较source和比较两个对象。

 

37、在上一步说了Spring如何解决两个对象bean名称一致的问题,以及如何比较旧bean和新bean。

我们看第35的图发现,如果bean不一致,但是名称一致,会抛一个异常,我们来做一个【测试】

 

38、我们继续看第34这张图的代码,当Spring判断为可进行注册的bean,则往下走,BeanDefinitionHolder封装着BeanDefinition,然后进行注册。

 

在DefaultListableBeanFactory中,还有一个重要的参数,allowBeanDefinitionOverriding:从名称上看代表允许覆盖BeanDefinition,没错,就是这样的。

首先,它的默认值是true,代表可以可以覆盖。如果为true,发现两个名称一致的bean,后者会把前者覆盖,如果为false,发现两个名称一致的bean,则会抛一个异常:BeanDefinitionOverrideException。

这个时候可能就会问,之前不是已经筛选过了吗?为什么这里还需要做一层?之前是注册(register),这次是IOC容器(beanDefinitionMap)。

 

 

 

Spring源码不是一朝一夕就能读完的,本文篇幅有些许的长,后续的源码分析中会小步快跑,感谢阅读。

 

 

 

 

你的关注,是我的荣幸

 

 

你要做多大的事情,就该承受多大的压力。

喜欢 4

评论

4条评论
  1. Gravatar 头像

    228 回复

    如此好文章一定要留下名啊

    • Gravatar 头像

      27wy.cn 回复

      @228 哈哈~相互学习相互学习,不对的地方多指点~

  2. Gravatar 头像

    asdasd 回复

    dahnsdadas

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Title - Artist
0:00