线上出 BUG 原因竟是 Spring 父子容器,反手我就去扒了它的底裤

  • J3
  • Spring(父子容器 # BUG)

1、事情的缘由

一天,J3接到个小需求,对进入数据库的数据线索做拦截处理。进入数据库的数据具体要做三个渠道校验A、B、C而这三个渠道的校验规则都各不相同,只要这其中一个渠道不满足那就不符合入库要求,则不可入库。

看到这个,J3内心是无比的窃喜,因为太简单了。说着J3就对着需求写出了下面的代码:

@RestController
@RequestMapping("/insert")
public class InsertController {

    @Autowired
    private ApplicationContext applicationContext;

    @PostMapping("/")
    public void doInsert(@RequestBody Entity entity){
        // 校验,核心逻辑
        Map<String, CheckChain> beansOfType = applicationContext.getBeansOfType(CheckChain.class);
        beansOfType.entrySet().forEach(entry -> {
            if (!entry.getValue().check(entity)){
                return;
            }
        });
        // 往下调用业务类执行插入
        // ...
    }
}
// 校验接口
public interface CheckChain {
    Boolean check(Entity e1);
}
// A渠道校验逻辑
@Component
public class ACheckChain implements CheckChain{
    @Override
    public Boolean check(Entity e1)  {
        // 校验逻辑
        return Boolean.TRUE;
    }
}
// B渠道校验逻辑
@Component
public class BCheckChain implements CheckChain{
    @Override
    public Boolean check(Entity e1) {
        // 校验逻辑
        return Boolean.TRUE;
    }
}
// C渠道校验逻辑
@Component
public class CCheckChain implements CheckChain{
    @Override
    public Boolean check(Entity e1) {
        // 校验逻辑
        return Boolean.TRUE;
    }
}

以上代码,是在 Spring 环境下运行的,我是通过对每个校验规则单独进行的测试,所有规则都测试完毕后就直接上线使用了。

而隐患就来了,那就是只单独测试了每个校验规则,没有整个逻辑走一遍,从 Controller 出发到最终的数据落库这整个流程。此时J3还不知道,他这菜逼写了个大 Bug,而且还是四个月之后才被人发现的。

就在前几天公司运营那边反馈数据拦截有问题,该拦的没拦住,这时我才注意到这个问题,立马去排查相关实现代码,经过了一系列的排查最终发现了端倪,看下面代码:

在这里插入图片描述

Controller 中能通过 ApplicationContext 拿到 @Component 扫描的 bean 吗?在 Spring 中,答案是不能的。

所以,这是那个二比写的代码,我立马看代码提交记录,原地楞了几秒,居然是我写的代码,还是四个月前。

在这里插入图片描述

定位出问题后,我也大致清楚问题的根本原因了,就先将线上问题修复(怕耽误老板赚),之后我再来好好扒一扒这个问题的细节。

2、Spring 父子容器

通过上面的事情描述,相信很多人都已经猜到问题出在什么关键点上了:Spring 父子容器

那既然这,咱们就来扒一扒它的底(原理)吧!

分析原理前,我先说明一下接下来我们是要干嘛。

  1. 分析父容器启动流程;
  2. 父容器中存放那些 Bean;
  3. 分析子容器启动流程;
  4. 子容器中存放那些 Bean;
  5. Controller 中注入的 IOC 容器是子容器还是父容器;
  6. Service 中注入的 IOC 容器是子容器还是父容器;
  7. 通过 IOC 容器如何获取 Bean;

2.1、环境搭建

父子容器问题,我觉得最好是自己搭建一个 Spring 和 Spring MVC 整合的架子出来,这样好定位分析其中的原理。

这是本人搭建好的环境(JDK11),大家不想自己搭建的可以直接 clone 一份👉:地址

先介绍项目的基本结构

在这里插入图片描述

web.xml:web 项目启动的配置文件,里面配置了Spring 和 Spring MVC 启动的关键类信息。

spring-service.xml:父容器配置文件。

spring-web.xml:子容器配置文件。

2.2、父容器启动分析

环境准备好后,先来看看父容器是如何启动的。

web 项目常规操作都是将其打包放入 Tomcat 中运行,Tomcat 在运行的时候会读取每个项目中 webapp 下 WEB-INF 中的 web.xml 文件,所以这就是我们分析问题的源头了。

还记得在搭建 Spring 和 Spring MVC 项目的时候我们要在 web.xml 文件中配置什么嘛!对,就是一个监听器:org.springframework.web.context.ContextLoaderListener ,它实现了 ServletContextListener 接口而 ServletContextListener 属于 Servlet API ,当 Servlet 容器启动或终止Web 应用时,会触发 ServletContextEvent 事件,该事件由 ServletContextListener 来处理。在 ServletContextListener 接口中定义了处理 ServletContextEvent 事件的两个方法:

  • contextInitialized(ServletContextEvent sce) :当Servlet 容器启动Web 应用时调用该方法。
  • contextDestroyed(ServletContextEvent sce):当Servlet 容器终止Web 应用时调用该方法。

那,这就好办了,我们直接来到 ContextLoaderListener 实现的 contextInitialized 方法中:

org.springframework.web.context.ContextLoaderListener # contextInitialized

@Override
public void contextInitialized(ServletContextEvent event) {
   // 父容器启动入口
   initWebApplicationContext(event.getServletContext());
}

根据方法名,我们就知道是一个初始化 web上下文应用的入口,它主要会做两件事情:

  1. 创建出父容器
  2. 初始化创建出来的父容器

进入 initWebApplicationContext 方法

org.springframework.web.context.ContextLoader # initWebApplicationContext

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {

    // ...

    if (this.context == null) {
        // 1、创建出父容器
        this.context = createWebApplicationContext(servletContext);
    }
    if (this.context instanceof ConfigurableWebApplicationContext) {
        ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
        if (!cwac.isActive()) {
            if (cwac.getParent() == null) {
                // 给容器设置父容器,此时 parent = null,也即 Spring 父容器没有父容器
                ApplicationContext parent = loadParentContext(servletContext);
                cwac.setParent(parent);
            }
            // 2、初始化创建出来的父容器
            configureAndRefreshWebApplicationContext(cwac, servletContext);
        }
    }

    // ...

}

创建父容器没什么好说的,通过反射创建出一个容器对象即可。关键点在第二部分,父容器的初始化 configureAndRefreshWebApplicationContext 这个方法所包含的内容非常多,再此我也是只分析它的 Bean 生命周期相关的部分,也即本项目中的 MyTestController 和 MyTestService 这两个 Bean 的生命周期。

进入 configureAndRefreshWebApplicationContext 方法

org.springframework.web.context.ContextLoader # configureAndRefreshWebApplicationContext

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    // ...

    // 重点部分,刷新整个容器的核心方法
    wac.refresh();
}

了解过 Spring 框架的相信对 refresh 方法都不陌生,它可以说是整个 Spring 启动的主方法了。

进入 refresh 方法

org.springframework.context.support.AbstractApplicationContext # refresh

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {

        // ...
        
        // 创建 beanFactory 读取 spring-service.xml 配置文件
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // ...
        
        // 将扫描到的所有单实例 Bean 全部创建并初始化
        finishBeanFactoryInitialization(beanFactory);
    }
}

考虑篇幅的问题和本次案例所涉及的内容,我只分析这两个方法。

1、obtainFreshBeanFactory() 方法分析

这个方法大致做两件事情:

  1. 将 Bean 工厂创建出来
  2. 读取配置文件,将需要创建的 Bean 信息找出来并存到对应位置

这两件事的体现就是 refreshBeanFactory() 方法,那我们就进去瞧瞧:

org.springframework.context.support.AbstractRefreshableApplicationContext # refreshBeanFactory

protected final void refreshBeanFactory() throws BeansException {
    // 如果工厂存在就先销毁
    if (hasBeanFactory()) {
        destroyBeans();
        closeBeanFactory();
    }
    try {
        // 1、创建 Bean 工厂出来
        DefaultListableBeanFactory beanFactory = createBeanFactory();
        beanFactory.setSerializationId(getId());
        customizeBeanFactory(beanFactory);
        // 2、读取配置文件,将 bean 信息封装成 BeanDefinitions 放入 beanFactory中
        loadBeanDefinitions(beanFactory);
        synchronized (this.beanFactoryMonitor) {
            this.beanFactory = beanFactory;
        }
    }
    catch (IOException ex) {
        throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
    }
}

第一点的创建还是没啥分析的,他只是创建了一个 DefaultListableBeanFactory 类型的 beanFactory 。

第二点是本次分析的重点了,它会去加载 Spring 的配置文件,读取配置文件中定义的扫描规则,将符合规则的 Bean 定义信息封装成一个个的 BeanDefinitions 存入创建出来的 Bean 工厂中。

那继续来看看 loadBeanDefinitions(beanFactory) 方法吧!

org.springframework.web.context.support.XmlWebApplicationContext # loadBeanDefinitions

@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
   // 创建 xml 的读取器
   XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

   // ...
    
   // 初始化读取器
   initBeanDefinitionReader(beanDefinitionReader);
   // 根据配置文件,使用xml读取器加载 BeanDefinitions
   loadBeanDefinitions(beanDefinitionReader);
}

读取器的创建就不多介绍了,接着往下走,程序最终会通过创建的 Xml 读取器最终走到 doLoadBeanDefinitions(inputSource, encodedResource.getResource()) 方法去真正的加载配置文件。

瞧瞧 doLoadBeanDefinitions 方法。

org.springframework.beans.factory.xml.XmlBeanDefinitionReader # doLoadBeanDefinitions

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
    throws BeanDefinitionStoreException {
    // ...
    // 1、将 spring 配置文件封装成一个 Document 对象
    Document doc = doLoadDocument(inputSource, resource);
    // 2、解析 Document 对象,注册 BeanDefinitions
    return registerBeanDefinitions(doc, resource);
    // ...
}

第一点的解析 XML 文件生成对应的 Document 对象实现有点复杂,我就简单的以我的理解概括一下。在 spring-service.xml 中配置的所有标签元素,在此部分都会被解析成 Document 对象的一个属性。

现在明白了吧,后面 registerBeanDefinitions 方法就是去读取 Document 对象的属性来完成 BeanDefinitions 注册的。

所以这一方法,我们把重点关注在第二部分,因为第一部分你可以理解为把我们写的 spring-service.xml 配置文件变成 Spring 能看懂的一个东西,配置的信息实际还没有开始生效,registerBeanDefinitions(doc, resource) 才是正在开始生效我们配置的东西。

探访 registerBeanDefinitions(doc, resource)方法

org.springframework.beans.factory.xml.XmlBeanDefinitionReader # registerBeanDefinitions

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
   // 创建读取 Document 的读取器
   BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
   int countBefore = getRegistry().getBeanDefinitionCount();
   // 开始读取
   documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
   return getRegistry().getBeanDefinitionCount() - countBefore;
}

又来一个创建某某某的读取器,这不用猜就知道是用来解析或读取 Document 对象的。下面就会通过 BeanDefinitionDocumentReader 去解析 Document 对象注册相关 BeanDefinition 。

documentReader.registerBeanDefinitions 方法中会委派一个代表出来,专门处理从 Document 对象中解析出来的节点,最后根据不同节点调用不同的解析方法。

registerBeanDefinitions 方法中会有很多的方法调用,我会把调用步骤罗列出来,但不会贴代码,因为它不是我分析的重点而且全部列出来,篇幅太长了,就在这里跳过了,大家可以看看调用步骤:

  1. org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader # registerBeanDefinitions
  2. org.springframework.beans.factory.xml.BeanDefinitionParserDelegate # parseCustomElement
  3. org.springframework.context.annotation.ComponentScanBeanDefinitionParser # parse
  4. org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan

doScan 方法就是我们一步步跟进来的终点了,他的主要作用就是将我们定义的 Bean 封装成 BeanDefinitionHolder 返回给 Bean 工厂,实现代码如下:

// basePackages 我们配置的包扫描 cn.j3.myspring
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    // 定义 BeanDefinitionHolder 存放容器
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
    // 遍历要扫描的路径
    for (String basePackage : basePackages) {
        // 获取该路径下,所有符合 XXXFilters 规则的 @Components 注解标注的类
        Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
        // 遍历扫描出来的 BeanDefinition
        for (BeanDefinition candidate : candidates) {
            // 生成 Bean 对应的 name 并将 name 和 BeanDefinition 形成映射关系封装成 BeanDefinitionHolder 返回出去
            ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
            candidate.setScope(scopeMetadata.getScopeName());
            // 生成 beanName
            String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
            if (candidate instanceof AbstractBeanDefinition) {
                postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
            }
            if (candidate instanceof AnnotatedBeanDefinition) {
                AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
            }
            if (checkCandidate(beanName, candidate)) {
                BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                definitionHolder =
                    AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
                // 存入 set 集合中,返回给上层
                beanDefinitions.add(definitionHolder);
                // 注册到 beanFactory 中
                registerBeanDefinition(definitionHolder, this.registry);
            }
        }
    }
    // 返回
    return beanDefinitions;
}

本类的主要功能就是扫描配置文件定义的包路径,将符合定义的 bean 找出来进行封装形成 BefinitionHolder 注册到 beanFactory 中。

至此,我们自己定义的 Bean 就被注册到 BeanFactory 中了,至于实例化就要等到后面在处理了,实例化不是这一大步该做的事情。根据环境搭建的项目如果是一步步 Debug 到这里,会发现并没有 MyTestController 这个 Bean,为什么呢!

<!-- 设置扫描组件的包 -->
<context:component-scan base-package="cn.j3.myspring">
    <!-- 告知spring不扫描Controller注解的包-->
    <context:exclude-filter type="annotation"
                            expression="org.springframework.stereotype.Controller"/>
    <context:exclude-filter type="annotation"
                            expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
</context:component-scan>

看看我们的配置文件(spring-service.xml),它已经定义了 spring 告知其不要扫描 @Controller 标注的类,而这一体现就是代码:findCandidateComponents(basePackage) 方法中的 isCandidateComponent(metadataReader) 决定了。

分析到这里,我们已经有了 Bean 对应的定义信息也存入到了 BeanFactory 中,现就差将这些 Bean 实例化出来了了,那这一功能就是它(finishBeanFactoryInitialization(beanFactory))该干的活了。

2、finishBeanFactoryInitialization(beanFactory) 方法分析

该方法的主要功能就是实例化 beanFactory 中注册的所有非懒加载 bean,核心代码如下:

org.springframework.context.support.AbstractApplicationContext # finishBeanFactoryInitialization

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    // ...
    
    // 实例化所有非懒加载 bean
    beanFactory.preInstantiateSingletons();
}

因为 bean 信息是存储在 beanFactory 中,所以理应调用 beanFactory 去实例化,代码如下:

org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons

public void preInstantiateSingletons() throws BeansException {

    // ...

    // 获取出所有定义的 beanName
    List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

    // 循环实例化
    for (String beanName : beanNames) {
        // ...

        // 一系列的 Bean 生命周期流程
        getBean(beanName);
        
        // ...

    }

    // 循环触发所有 bean的初始化后回调
}

因为我们知道了父容器中因该有那些 Bean 了,所以 Bean 的实例化就不是我们的关注重点了,但有一点确实要知道的。

还记不记得我们在 MyTestService 中注入了一个 ApplicationContext 对象,那 Spring 在实例化 MyTestService 时,最终会注入那个 IOC 容器呢!

这里,你想都不想,百分之一百可以肯定,就是父容器。因为此时 Spring-MVC 子容器还没开始启动呢,怎么给你注入,所以父容器中的 Bean 注入的 IOC 容器一定是父容器,这无可非议。

画图高手对父容器启动流程做执行流程图,如下:

在这里插入图片描述

2.3、子容器启动分析

对于子容器的启动,我们也还是要看 web.xml 这个配置文件。

在 SpringMVC 框架中我们只在 web.xml 配置文件中配置了 org.springframework.web.servlet.DispatcherServlet ,所以我们的入口就是它。

先来看看其类继承结构:

在这里插入图片描述

上图可知,DispatcherServlet 间接继承 Servlet 所以它是一个 Servlet ,在 web 容器启动时会执行 Servlet 的生命周期方法。

而 DispatcherServlet 也不例外,所以我们的子容器启动分析的开始就是 DispatcherServlet 类的 init() 方法。

下面我们定位到 init() 方法中瞧瞧:

org.springframework.web.servlet.HttpServletBean # init

public final void init() throws ServletException {

    // ...

    // Let subclasses do whatever initialization they like.
    // HttpServletBean 子类实现,初始化 servletBean
    initServletBean();

    // ...
}

init 方法并没有做什么实际性的功能,但其预留了一个 initServletBean 方法给其子类进行扩展,而我们要的就是这个,玩下看。

org.springframework.web.servlet.FrameworkServlet # initServletBean

protected final void initServletBean() throws ServletException {
    
    // ...
    // 启动子容器的入口
    this.webApplicationContext = initWebApplicationContext();
    // 方法预留,没有任何逻辑
    initFrameworkServlet();
    
    // ...

}

终于看到我们想要的了 initWebApplicationContext ,它就是子容器启动的入口,点进去。

org.springframework.web.servlet.FrameworkServlet # initWebApplicationContext

protected WebApplicationContext initWebApplicationContext() {
    // 首先获取父容器,就是上面我们分析的父容器
    WebApplicationContext rootContext =
        WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;

    // 第一次初始化,this.webApplicationContext 肯定是 null
    if (this.webApplicationContext != null) {
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                if (cwac.getParent() == null) {
                    // 给子容器设置父容器
                    cwac.setParent(rootContext);
                }
                // 和父容器初始化一样
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        // 找 WebApplicationContext 肯定是找不到的,因为没有创建出来
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // 第一次执行的话,肯定会走这里,创建子容器!!!!!!!(重点)!!!!
        wac = createWebApplicationContext(rootContext);
    }

    if (!this.refreshEventReceived) {
        onRefresh(wac);
    }

    if (this.publishContext) {
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
                              "' as ServletContext attribute with name [" + attrName + "]");
        }
    }
    return wac;
}

初始化子容器方法主要有两个逻辑,第一个是子容器被创建过,二是没有被创建过。

  1. 子容器已经创建,获取父容器将其设置为子容器的父容器,并执行和父容器一样的初始化逻辑,即 configureAndRefreshWebApplicationContext 方法。
  2. 子容器未创建,先试着查找一次,如仍未找到,在进行创建,走 createWebApplicationContext(rootContext) 方法。

显然我们这次的情况,是走第二种逻辑,进入 createWebApplicationContext 方法。

org.springframework.web.servlet.FrameworkServlet # createWebApplicationContext()

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    // ...

    // 先创建出子容器
    ConfigurableWebApplicationContext wac =
        (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

    // 给子容器设置环境
    wac.setEnvironment(getEnvironment());
    // 给子容器设置父容器
    wac.setParent(parent);
    // 获取 spring-web.xml配置文件
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        // 这是子容器配置文件
        wac.setConfigLocation(configLocation);
    }
    // 开始初始化子容器
    configureAndRefreshWebApplicationContext(wac);

    // 返回初始化好的子容器
    return wac;
}

可以看出此方法又是常见的两大步,创建、初始化。

这里有个和父容器不同的点就是,子容器设置了一个父容器属性,将刚刚创建好的父容器设置到了子容器的 Parent 属性中,知道这是为什么嘛!

这当然是为了自动装配了,我们经常写的 Controller 中不就是要装配各种 Sertvice Bean嘛,而这些 Bean 哪里来,当然是父容器中来了。虽然我们也可以从子容器中来,就是在 spring-web.xml 中配置,但我们一般不这样做。

最后,我们的子容器初始化成功之后,大家再猜一下,我们项目中 MyTestController 中注入的 ApplicatuionContext 是子容器还是父容器,答案是子容器

画图高手对子容器启动流程做执行流程图,如下:

在这里插入图片描述

3、探讨 applicationContext.getBeansOfType(Class)

在上面我们已经扒光了 spring 父子容器的底裤,那我们现在就可以对着光秃秃的 Spring 来分析分析为什么在 Controller 使用 pplicationContext.getBeansOfType 方法会出现获取不到的问题了。

在 MyTestController 中先进入 getBeansOfType 方法。

org.springframework.beans.factory.support.DefaultListableBeanFactory#getBeansOfType

public <T> Map<String, T> getBeansOfType(@Nullable Class<T> type, boolean includeNonSingletons, boolean allowEagerInit)
    throws BeansException {
    // 根据 type 获取 bean 的名字
    String[] beanNames = getBeanNamesForType(type, includeNonSingletons, allowEagerInit);
    Map<String, T> result = new LinkedHashMap<>(beanNames.length);
    // 如果 beanNames 为空也就没有下面的流程了
    for (String beanName : beanNames) {
        // 根据 beanName 获取 bean
    }
    return result;
}

可以看出,该方法的重点在 getBeanNamesForType 方法,如果该方法没有获取到 beanName 那么也就没有后面的根据名称获取 Bean 这一步骤了,所以我们来看看 getBeanNamesForType 的实现逻辑。

org.springframework.beans.factory.support.DefaultListableBeanFactory # getBeanNamesForType

public String[] getBeanNamesForType(@Nullable Class<?> type, boolean includeNonSingletons, boolean allowEagerInit) {
    // 不太清楚这是什么判断,反正是不走
    if (!isConfigurationFrozen() || type == null || !allowEagerInit) {
        return doGetBeanNamesForType(ResolvableType.forRawClass(type), includeNonSingletons, allowEagerInit);
    }
    // 重点在下面这几步,上面那步是不会走的
    // 先从缓存中获取,第一次获取,这里肯定为空
    Map<Class<?>, String[]> cache =
        (includeNonSingletons ? this.allBeanNamesByType : this.singletonBeanNamesByType);
    String[] resolvedBeanNames = cache.get(type);
    if (resolvedBeanNames != null) {
        return resolvedBeanNames;
    }
    // 再根据类型,获取 bean,第一次获取,肯定是会走这里的
    resolvedBeanNames = doGetBeanNamesForType(ResolvableType.forRawClass(type), includeNonSingletons, true);
    if (ClassUtils.isCacheSafe(type, getBeanClassLoader())) {
        // 将获取过的放入缓存中,下次再来获取同样类型的 bean ,直接走缓存
        cache.put(type, resolvedBeanNames);
    }
    // 返回结果
    return resolvedBeanNames;
}

根据注释相信各位人才也能知道,这个方法的重点在 doGetBeanNamesForType 方法中,点进去。

org.springframework.beans.factory.support.DefaultListableBeanFactory # doGetBeanNamesForType

private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit) {
    List<String> result = new ArrayList<>();

    // 循环遍历 BeanFactory 中所有的 bean,判断是否是传入的 type 是则存入 list 中返回
    for (String beanName : this.beanDefinitionNames) {
        // 可以不用分析了
    }
}

看到注释不知道大家又没恍然大悟,这里要考考各位人才咯!

注意看这个 for 循环,它只循环了 this.beanDefinitionNames 集合,意味着 getBeansOfType 方法只会在本容器中根据 Class 类型寻找匹配的 Bean。

那我们再来回顾一下,父子容器启动后,其各自容器中都装了那些 Bean。我们本次项目中的 MyTestService 是定义在父容器中的,所以会存放在父容器中;而 MyTestController 是定义在 子容器中的,所以它会存放在子容器中。

又因为 MyTestController 中注入的 ApplicationContext 为子容器,所以通过 getBeansOfType 去找父容器中的 Bean 自然是找不到了。

当然,你去 MyTestService 中根据 getBeansOfType 获取 MyTestController 同样也是获取不到的,与上面同理。

多思考是不是就觉得豁然开朗了,对于 Spring 父子容器问题。

4、探讨 applicationContext.getBean(Class)

那我们继续来探讨 genBean 方法,因为在 MyTestController 中通过子容器可以获取父容器中的 bean。

进入 getBean 代码。

@Override
public <T> T getBean(Class<T> requiredType, @Nullable Object... args) throws BeansException {
    // 根据类型,在本容器中获取
    NamedBeanHolder<T> namedBean = resolveNamedBean(requiredType, args);
    if (namedBean != null) {
        // 本容器中有,直接返回
        return namedBean.getBeanInstance();
    }
    // 本容器没有,通过容器的父容器找
    BeanFactory parent = getParentBeanFactory();
    if (parent != null) {
        // 查找父容器
        return (args != null ? parent.getBean(requiredType, args) : parent.getBean(requiredType));
    }
    // 没有找到,报错
    throw new NoSuchBeanDefinitionException(requiredType);
}

代码很简单,先从自身容器中找 type 类型的 bean,如果找不到会往上找到容器的父容器(分析容器启动的时候有讲过),继续从父容器中找。

这就不难理解,我们在 MyTestController 使用子容器可以通过 getBean 获取到父容器中的 Bean 了。

那我们在来思考一下,在 MyTestService 中通过 getBean 找 MyTestController 类型的 Bean 是否可以找到呢!

答案是,找不到

至于为什么找不到,各位人才可以细心的思考一下,好了,这篇文章的篇幅也是有点长了,就先分析到这了。

如果对我最后给出的答案有不理解的地方,欢迎评论区留言或者直接联系本人,我很乐意和大家一起探讨的。

当这篇完结时,我知道三天的假期就没了,此时我的心情是这样的…

在这里插入图片描述

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

有任何问题,根据下面联系方式找到我:

QQ:1491989462

微信:13207920596


  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。
  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

J3code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值