1、前奏
接上一篇《线上出 BUG 原因竟是 Spring 父子容器,反手我就去扒了它的底裤》文章发布后,我就在想平时开发用的基础框架更多的是 SpringBoot 而不是 Spring ,那为什么在 SpringBoot 项目中却从来都没有遇到过有类似的问题(被容器管理的 Bean ,会有获取不到这一问题)。
那能不能猜想一下,在 SpringBoot 项目中,只要符合 Bean 的定义且被 IOC 容器管理,那就一定能从容器中获取对应的 Bean 信息。
有了猜想,那就上代码把!
本次文章所涉及的代码获取地址👉:Gitee
Controller 控制层代码
@RestController @RequestMapping("/test") public class MvcTestController { @Autowired private MyTestService myTestService; @Autowired private MyTestHandle myTestHandle; private ApplicationContext applicationContext; public MvcTestController(ApplicationContext applicationContext){ this.applicationContext = applicationContext; System.out.println("===========MvcTestController-init============="); } @GetMapping("/") public String test(){ Map<String, MyTestService> beansOfType = applicationContext.getBeansOfType(MyTestService.class); System.out.println(beansOfType); myTestService.test(); myTestHandle.handle(); return "success"; } }
Service 业务层代码
@Service public class MyTestService { private ApplicationContext applicationContext; public MyTestService(ApplicationContext applicationContext){ this.applicationContext = applicationContext; System.out.println("===========MyTestService-init============="); } public void test(){ Map<String, MvcTestController> beansOfType = applicationContext.getBeansOfType(MvcTestController.class); System.out.println(beansOfType); } }
就这点代码,我想应该能说明以下这几个问题:
- SpringBoot 项目中的容器启动流程
- SpringBoot 项目在 Controller 和 Service 中分别注入什么 IOC 容器
- Controller 能否通过注入的容器对象拿到根据类型获得到 Bean
- Service 能否通过注入的容器对象拿到根据类型获得到 Controller Bean
那既然问题已经摆出来了,咱们就开始对着案例源码分析一波吧!
我发现很多人都不太会进行源码分析,一开始我也不会,且秉承着会用就行的观念。后来我发现这样太局限了,使用过程中发现问题永远只有百度,而且百度得到的大部分是对自己毫无用处的,所以我做出了改变。就是定位问题代码、断点调试、找到上下文、一步步进行因果分析,如果一次断点没有效果就多来几次,别怕麻烦,多来几次肯定会有结果的。
2、SpringBoot 容器启动分析
容器启动肯定是会往里面添加 Bean 的,所以我们先分别在 Controller 和 Service 中的 Bean 中编写好有参构造器并打好断点,如图所示:
接着就是启动主类、记住是 Debug 模式启动。如果不出意外的话,程序启动将会停留在 MvcTestController 类的构造器方法断点中。
此时我们要做的就是看 IDEA 左下方的方法调用栈了,从下到上依次是程序从主类 main 方法开始到 MvcTestController 构造器结束的一个栈调用顺序。那接下来的工作就是分析这一整个过程的栈调用顺序了,先从 main 方法进入。
org.springframework.boot.SpringApplication # run
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return new SpringApplication(primarySources).run(args); }
这个方法明显是有两部分:
- new SpringApplication 对象
- 调用 run 方法
咱们这次要分析的重点是在 run 方法,而 new SpringApplication 对象所要做的事情我也大致介绍一下主要就是如下几个:
- 保存运行主类到 SpringApplication 对象属性中
- 设置应用启动类型 webApplicationType 的值,一共三种:REACTIVE、SERVLET、NONE
- 加载所有 spring.factories 文件中配置的类信息
好了,那继续回到 run 方法中吧
public ConfigurableApplicationContext run(String... args) { // 创建记录器对象 StopWatch stopWatch = new StopWatch(); // 开始记录创建 stopWatch.start(); // 创建 BootstrapContext 上下文,这是不是一个父容器呢? DefaultBootstrapContext bootstrapContext = createBootstrapContext(); // 盲猜这个就是我们要找的 IOC 容器 ConfigurableApplicationContext context = null; configureHeadlessProperty(); // 获取从 spring.factories 配置信息获得到的监听器 SpringApplicationRunListeners listeners = getRunListeners(args); // 循环启动这些监听器 listeners.starting(bootstrapContext, this.mainApplicationClass); try { // 封装参数 ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 配置环境 ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments); configureIgnoreBeanInfo(environment); // 打印 Banner Banner printedBanner = printBanner(environment); // 创建上下文对象,本次所涉及的一个重点 context = createApplicationContext(); // 准备上下文 context.setApplicationStartup(this.applicationStartup); prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); // 刷新上下文,要开始创建 Bean 相关信息了,本次所涉及的一个重点 refreshContext(context); // 空方法 afterRefresh(context, applicationArguments); // 计算耗时 stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } listeners.started(context); callRunners(context, applicationArguments); }catch (Throwable ex) { handleRunFailure(context, ex, listeners); throw new IllegalStateException(ex); }try { listeners.running(context); }catch (Throwable ex) { handleRunFailure(context, ex, null); throw new IllegalStateException(ex); } return context; }
上面我对 run 方法的每行代码都做了点注释,虽然有些地方可能有理解性的偏差,但是那都不是本次关注的重点,咱们关注下面这几个:
- DefaultBootstrapContext bootstrapContext = createBootstrapContext();
- context = createApplicationContext();
- refreshContext(context);
这三行代码涉及到 bootstrap 上下文的创建、application 上下文的创建、上下文的刷新,所以这就是我们要抓出来分析的点了。
2.1、bootstrap 上下文的创建
进入下面代码,看看其创建过程
org.springframework.boot.SpringApplication # createBootstrapContext
private DefaultBootstrapContext createBootstrapContext() { DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext(); this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext)); return bootstrapContext; }
其创建了一个默认的 DefaultBootstrapContext 上下文对象,接着遍历初始化了 bootstrapRegistryInitializers 中的所有初始化器。
那这些初始化器是哪里来的,有没有思考过。
还记得 new SpringApplication 对象的这个代码嘛,里面就有这么一个方法 getBootstrapRegistryInitializersFromSpringFactories() 它就是用来专门读取下图中配置的这部分数据,并将其赋值给 bootstrapRegistryInitializers 属性。
最后将 DefaultBootstrapContext 返回出去,好像没看出来它和 IOC 容器有什么关系,倒是觉得它就是一个结合外部配置,将加载到内部的配置进行使用的一个功能。
好了,不纠结这个了,但是我们要记住这个创建的对象,最终我们是要来验证我们定义的 Bean 是被放在那个上下文的对象中。
2.2、application 上下文的创建
分析了上面那个,那再进入这个方法看看
org.springframework.boot.SpringApplication # createApplicationContext
private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT; protected ConfigurableApplicationContext createApplicationContext() { // 根据 webApplicationType 创建对应上下文 return this.applicationContextFactory.create(this.webApplicationType); }
org.springframework.boot.ApplicationContextFactory # DEFAULT
@FunctionalInterface public interface ApplicationContextFactory { /** * A default {@link ApplicationContextFactory} implementation that will create an * appropriate context for the {@link WebApplicationType}. */ ApplicationContextFactory DEFAULT = (webApplicationType) -> { try { switch (webApplicationType) { case SERVLET: // 创建 web 类型的上下文 return new AnnotationConfigServletWebServerApplicationContext(); case REACTIVE: return new AnnotationConfigReactiveWebServerApplicationContext(); default: return new AnnotationConfigApplicationContext(); } } catch (Exception ex) { throw new IllegalStateException("Unable create a default ApplicationContext instance, " + "you may need a custom ApplicationContextFactory", ex); } }; }
最终程序根据 webApplicationType 创建出了 AnnotationConfigServletWebServerApplicationContext 类型的上下文对象,并直接返回出去了,啥事也没做。
虽然代码看着没做啥事,但你点进 AnnotationConfigServletWebServerApplicationContext 类的构造器方法就会发现里面的初始化代码非常之多。
public AnnotationConfigServletWebServerApplicationContext() { // 创建读取 BeanDefinition 的注解读取器 this.reader = new AnnotatedBeanDefinitionReader(this); // 创建扫描 BeanDefinition 的扫描器 this.scanner = new ClassPathBeanDefinitionScanner(this); }
最终程序根据 webApplicationType 创建出了 AnnotationConfigServletWebServerApplicationContext 类型的上下文对象,并直接返回出去了,啥事也没做。
虽然代码看着没做啥事,但你点进 AnnotationConfigServletWebServerApplicationContext 类的构造器方法就会发现里面的初始化代码非常之多。
public AnnotationConfigServletWebServerApplicationContext() { // 创建读取 BeanDefinition 的注解读取器 this.reader = new AnnotatedBeanDefinitionReader(this); // 创建扫描 BeanDefinition 的扫描器 this.scanner = new ClassPathBeanDefinitionScanner(this); }
虽然这个构造器只有两行代码,但是做的事情可不少,总体来说都是为了后面程序中定义的 Bean 能够起效而做的前期准备。
并且在查阅 AnnotationConfigServletWebServerApplicationContext 的父类构造器是,我发现了下面代码:
org.springframework.context.support.GenericApplicationContext # GenericApplicationContext()
public GenericApplicationContext() { // 存放 Bean 的默认工厂 this.beanFactory = new DefaultListableBeanFactory(); }
看到这个,大家心里是不是瞬间明朗起来了。
这个在上篇分析文章中经常出现,就是存放 Bean 的东西,并且我也更加确信这就是我们要找的 IOC 容器了,至于后面会不会在出现一个 IOC 容器就不得而知了,反正现在我们是找到了一个 IOC 容器了。
2.3、上下文的刷新
终于是来到了刷新阶段了,看下面代码:
org.springframework.boot.SpringApplication # refreshContext
private void refreshContext(ConfigurableApplicationContext context) { if (this.registerShutdownHook) { shutdownHook.registerApplicationContext(context); } // 调用刷新方法 refresh(context); }
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext # refresh
@Override public final void refresh() throws BeansException, IllegalStateException { // ... // 调用父类的刷新方法 super.refresh(); // ... }
在上篇我们分析过了 refresh 方法是基于上下文容器对象调用进来的,也就是说进入 refresh 方法之后只会在容器中添加相关 Bean 而不会再次创建新的容器,所以从应用启动到 refresh 方法我们只看到了一个 IOC 容器的创建。
事实上,如果接着 Debug 一步步进入 refresh 方法我们可以发现在 obtainFreshBeanFactory() 方法中时我们的 Controller 和 Service 包下的类都被封装成了 BeanDefinition 而存储在了 DefaultListableBeanFactory 对象中。并且在 finishBeanFactoryInitialization() 方法执行时,封装的 BeanDefinition 也全部被实例化出来存在 DefaultListableBeanFactory 的单利缓冲池中了。
所以就自然而然的可以想通在对应的 MyTestController 和 MyTestService 中注入的 ApplicationContext 上下文对象是不是同一个对象了。
下图是 Debug 是断点运行时的状态图,可以看看两个对象注入的是不是同一个 ApplicationContext 。
3、总结
这里本来想分析一下 applicationContext.getBeansOfType(Class) 方法的,但是由于上篇我们已经讲解过了,所以就不在分析,直接说结果了。
到现在我们可以大胆地说,SpringBoot 中只有一个 IOC 容器中了。
而 applicationContext.getBeansOfType 方法的实现就是只在本容器中寻找对应类型的 Bean,这肯定是可以找到的,因为整个应用就一个 IOC 容器。
且我们在 Service 中也是可以拿到被 IOC 容器管理的 Controller 相关的 Bean 信息。
那我们有没有想过 SpringBoot 中为什么只有一个 IOC 容器呢!
我说一下我的理解:
SpringBoot 是一个框架,所有功能的启动入口只有一个,那就是 main 方法主类。因为入口只有一个,所以一些扫描规则也只能是符合一个扫描器去配置,当然也可以多规则多扫描器,但这就不符和 SpringBoot 的宗旨了(简化配置、约定大于配置),那这样一来父子容器的搭配是不是就有点复杂,所以为了消除这个才放到一个启动流程里统一扫描、统一初始化、统一存储,也既一个 IOC 容器管理所有 Bean 。
而 Spring 框架我们知道如果单独使用 Spring 也是没有父子容器的,只有当我们引入了 SpringMVC 框架之后,程序的入口从以前的一个变成了两个才意识到要进行职责分明。就是什么框架管理什么 Bean 定义好,不然就会出现单利 Bean 而被初始化了多次的情况。
这是我的理解,从程序的启动入口,职责,复杂度这几个考量,如果有什么不对或欠缺大家可以评论区说出来一起探讨探讨。
好了,今天的内容到这里就结束了,关注我,我们下期见