源码再现,SpringBoot 居然只有一个 IOC 容器

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 源码再现,SpringBoot 居然只有一个 IOC 容器


1、前奏



接上一篇《线上出 BUG 原因竟是 Spring 父子容器,反手我就去扒了它的底裤》文章发布后,我就在想平时开发用的基础框架更多的是 SpringBoot 而不是 Spring ,那为什么在 SpringBoot 项目中却从来都没有遇到过有类似的问题(被容器管理的 Bean ,会有获取不到这一问题)。


那能不能猜想一下,在 SpringBoot 项目中,只要符合 Bean 的定义且被 IOC 容器管理,那就一定能从容器中获取对应的 Bean 信息。


image.png


有了猜想,那就上代码把!


本次文章所涉及的代码获取地址👉: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 中编写好有参构造器并打好断点,如图所示:


image.png


接着就是启动主类、记住是 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 对象所要做的事情我也大致介绍一下主要就是如下几个:


  1. 保存运行主类到 SpringApplication 对象属性中
  2. 设置应用启动类型 webApplicationType 的值,一共三种:REACTIVE、SERVLET、NONE
  3. 加载所有 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 方法的每行代码都做了点注释,虽然有些地方可能有理解性的偏差,但是那都不是本次关注的重点,咱们关注下面这几个:


  1. DefaultBootstrapContext bootstrapContext = createBootstrapContext();
  2. context = createApplicationContext();
  3. refreshContext(context);


这三行代码涉及到 bootstrap 上下文的创建、application 上下文的创建、上下文的刷新,所以这就是我们要抓出来分析的点了。


image.png


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 属性。


image.png


最后将 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();
}


看到这个,大家心里是不是瞬间明朗起来了。


image.png


这个在上篇分析文章中经常出现,就是存放 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 。


微信图片_20220427101455.png


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 而被初始化了多次的情况。


这是我的理解,从程序的启动入口,职责,复杂度这几个考量,如果有什么不对或欠缺大家可以评论区说出来一起探讨探讨。


image.png


好了,今天的内容到这里就结束了,关注我,我们下期见

目录
相关文章
|
4月前
|
JavaScript 前端开发 Java
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
439 1
|
5月前
|
前端开发 Java 关系型数据库
基于Java+Springboot+Vue开发的鲜花商城管理系统源码+运行
基于Java+Springboot+Vue开发的鲜花商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的鲜花商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。技术学习共同进步
420 7
|
5月前
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
386 70
|
4月前
|
供应链 JavaScript BI
ERP系统源码,基于SpringBoot+Vue+ElementUI+UniAPP开发
这是一款专为小微企业打造的 SaaS ERP 管理系统,基于 SpringBoot+Vue+ElementUI+UniAPP 技术栈开发,帮助企业轻松上云。系统覆盖进销存、采购、销售、生产、财务、品质、OA 办公及 CRM 等核心功能,业务流程清晰且操作简便。支持二次开发与商用,提供自定义界面、审批流配置及灵活报表设计,助力企业高效管理与数字化转型。
446 2
ERP系统源码,基于SpringBoot+Vue+ElementUI+UniAPP开发
|
4月前
|
XML Java 数据格式
Spring IoC容器的设计与实现
Spring 是一个功能强大且模块化的 Java 开发框架,其核心架构围绕 IoC 容器、AOP、数据访问与集成、Web 层支持等展开。其中,`BeanFactory` 和 `ApplicationContext` 是 Spring 容器的核心组件,分别定位为基础容器和高级容器,前者提供轻量级的 Bean 管理,后者扩展了事件发布、国际化等功能。
|
3月前
|
机器学习/深度学习 数据采集 人机交互
springboot+redis互联网医院智能导诊系统源码,基于医疗大模型、知识图谱、人机交互方式实现
智能导诊系统基于医疗大模型、知识图谱与人机交互技术,解决患者“知症不知病”“挂错号”等问题。通过多模态交互(语音、文字、图片等)收集病情信息,结合医学知识图谱和深度推理,实现精准的科室推荐和分级诊疗引导。系统支持基于规则模板和数据模型两种开发原理:前者依赖人工设定症状-科室规则,后者通过机器学习或深度学习分析问诊数据。其特点包括快速病情收集、智能病症关联推理、最佳就医推荐、分级导流以及与院内平台联动,提升患者就诊效率和服务体验。技术架构采用 SpringBoot+Redis+MyBatis Plus+MySQL+RocketMQ,确保高效稳定运行。
258 0
|
6月前
|
小程序 Java 关系型数据库
weixin117新闻资讯系统设计+springboot(文档+源码)_kaic
本文介绍了一款基于微信小程序的新闻资讯系统,涵盖其开发全过程。该系统采用Java的SSM框架进行后台管理开发,使用MySQL作为本地数据库,并借助微信开发者工具确保稳定性。管理员可通过个人中心、用户管理等功能模块实现高效管理,而用户则能注册登录并查看新闻与视频内容。系统设计注重可行性分析(技术、经济、操作),强调安全性与数据完整性,界面简洁易用,功能全面,极大提升了信息管理效率及用户体验。关键词包括基于微信小程序的新闻资讯系统、SSM框架和MYSQL数据库。
|
7月前
|
小程序 JavaScript Java
基于SpringBoot的智慧停车场微信小程序源码分享
智慧停车场微信小程序主要包含管理端和小程序端。管理端包括停车场管理,公告信息管理,用户信息管理,预定信息管理,用户反馈管理等功能。小程序端包括登录注册,预约停车位,停车导航,停车缴费,用户信息,车辆信息,钱包充值,意见反馈等功能。
279 5
基于SpringBoot的智慧停车场微信小程序源码分享
|
8月前
基于springboot+thymeleaf+Redis仿知乎网站问答项目源码
基于springboot+thymeleaf+Redis仿知乎网站问答项目源码
255 36
|
8月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue实现的留守儿童爱心网站设计与实现(计算机毕设项目实战+源码+文档)
博主是一位全网粉丝超过100万的CSDN特邀作者、博客专家,专注于Java、Python、PHP等技术领域。提供SpringBoot、Vue、HTML、Uniapp、PHP、Python、NodeJS、爬虫、数据可视化等技术服务,涵盖免费选题、功能设计、开题报告、论文辅导、答辩PPT等。系统采用SpringBoot后端框架和Vue前端框架,确保高效开发与良好用户体验。所有代码由博主亲自开发,并提供全程录音录屏讲解服务,保障学习效果。欢迎点赞、收藏、关注、评论,获取更多精品案例源码。