Spring中的父子容器原理解析

 更新时间:2023年07月18日 09:31:49   作者:江南一点雨  
这篇文章主要为大家介绍了Spring中的父子容器原理解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

相信有小伙伴也听说过,在 SSM 项目中,Spring 容器是父容器,SpringMVC 是子容器,子容器可以访问父容器的 Bean,但是父容器不能访问子容器的 Bean。

更近一步,有小伙伴可能也了解过,不用父子容器,单纯就用一个 SpringMVC 容器似乎也可以,项目也能运行。

那么现在问题来了:既然单纯一个 SpringMVC 容器就能使项目跑起来,那我们为什么还要用父子容器?父子容器的优势是什么?

带着这个问题,今天松哥来和小伙伴们聊一聊父子容器。

1. 父子容器

首先,其实父子这种设计很常见,松哥记得在之前的 Spring Security 的系列文章中,Spring Security 中的 AuthenticationManager 其实也是类似的设计,估计那里就是借鉴了 Spring 中的父子容器设计。

当使用了父子容器之后,如果去父容器中查找 Bean,那么就单纯的在父容器中查找 Bean;

如果是去子容器中查找 Bean,那么就会先在子容器中查找,找到了就返回,没找到则继续去父容器中查找,直到找到为止(把父容器都找完了还是没有的话,那就只能抛异常出来了)。

2. 为什么需要父子容器

2.1 问题呈现

为什么需要父子容器?老老实实使用一个容器不行吗?

既然 Spring 容器中有父子容器,那么这个玩意就必然有其使用场景。

松哥举一个简单的例子。

假设我有一个多模块项目,其中有商家模块和客户模块,商家模块和客户模块中都有角色管理 RoleService,项目结构如下图:

├── admin
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   └── resources
├── consumer
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── org
│       │   │       └── javaboy
│       │   │           └── consumer
│       │   │               └── RoleService.java
│       │   └── resources
│       │       └── consumer_beans.xml
├── merchant
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── org
│       │   │       └── javaboy
│       │   │           └── merchant
│       │   │               └── RoleService.java
│       │   └── resources
│       │       └── merchant_beans.xml
└── pom.xml

现在 consumer 和 merchant 中都有一个 RoleService 类,然后在各自的配置文件中,都将该类注册到 Spring 容器中。

org.javaboy.consumer.RoleService:

public class RoleService {
    public String hello() {
        return "hello consumer";
    }
}

org.javaboy.merchant.RoleService:

public class RoleService {
    public String hello() {
        return "hello merchant";
    }
}

consumer_beans.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean class="org.javaboy.consumer.RoleService" id="roleService"/>
</beans>

merchant_beans.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean class="org.javaboy.merchant.RoleService" id="roleService"/>
</beans>

大家注意,这两个 Bean 同名。

现在,在 admin 模块中,同时依赖 consumer 和 merchant,同时加载这两个配置文件,那么能不能同时向 Spring 容器中注册两个来自不同模块的同名 Bean 呢?

代码如下:

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
ctx.setConfigLocations("consumer_beans.xml", "merchant_beans.xml");
ctx.refresh();
org.javaboy.merchant.RoleService rs1 = ctx.getBean(org.javaboy.merchant.RoleService.class);
org.javaboy.consumer.RoleService rs2 = ctx.getBean(org.javaboy.consumer.RoleService.class);

这个执行之后会抛出如下问题:

小伙伴们看到,这个是找不到 org.javaboy.consumer.RoleService 服务,但是另外一个 RoleService 其实是找到了,因为默认情况下后面定义的同名 Bean 把前面的覆盖了,所以有一个 Bean 就找不到了。

如果不允许 Bean 的覆盖,那么可以进行如下配置:

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
ctx.setConfigLocations("consumer_beans.xml", "merchant_beans.xml");
ctx.setAllowBeanDefinitionOverriding(false);
ctx.refresh();

此时一启动就直接报错了:

意思也说的比较明确了,Bean 的定义冲突了,所以定义失败。

那么有没有办法能够优雅的解决上面这个问题呢?答案就是父子容器!

2.2 父子容器

对于上面的问题,我们可以将 consumer 和 merchant 配置成父子关系或者兄弟关系,就能很好的解决这个问题了。

2.2.1 兄弟关系

先来看兄弟关系,代码如下:

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
ClassPathXmlApplicationContext child1 = new ClassPathXmlApplicationContext("consumer_beans.xml");
ClassPathXmlApplicationContext child2 = new ClassPathXmlApplicationContext("merchant_beans.xml");
child1.setParent(ctx);
child2.setParent(ctx);
ctx.setAllowBeanDefinitionOverriding(false);
ctx.refresh();
org.javaboy.consumer.RoleService rs1 = child1.getBean(org.javaboy.consumer.RoleService.class);
org.javaboy.merchant.RoleService rs2 = child2.getBean(org.javaboy.merchant.RoleService.class);
System.out.println("rs1.hello() = " + rs1.hello());
System.out.println("rs2.hello() = " + rs2.hello());

小伙伴们看一下,这种针对 consumer 和 merchant 分别创建了容器,这种容器关系就是兄弟容器,这两个兄弟有一个共同的 parent 就是 ctx,现在可以在各个容器中获取到自己的 Bean 了。

需要注意的是,上面这种结构中,子容器可以获取到 parent 的 Bean,但是无法获取到兄弟容器的 Bean,即如果 consumer 中引用了 merchant 中的 Bean,那么上面这个配置就有问题了。

2.2.2 父子关系

现在假设用 consumer 做 parent 容器,merchant 做 child 容器,那么配置如下:

ClassPathXmlApplicationContext parent = new ClassPathXmlApplicationContext("consumer_beans.xml");
ClassPathXmlApplicationContext child = new ClassPathXmlApplicationContext("merchant_beans.xml");
child.setParent(parent);
child.refresh();
org.javaboy.consumer.RoleService rs1 = parent.getBean(org.javaboy.consumer.RoleService.class);
org.javaboy.merchant.RoleService rs2 = child.getBean(org.javaboy.merchant.RoleService.class);
org.javaboy.consumer.RoleService rs3 = child.getBean(org.javaboy.consumer.RoleService.class);
System.out.println("rs1.hello() = " + rs1.hello());
System.out.println("rs2.hello() = " + rs2.hello());
System.out.println("rs3.hello() = " + rs3.hello());

首先创建两个容器,分别是 parent 和 child,然后为 child 容器设置 parent,设置完成后记得要刷新 child 容器。

现在我们就可以从 parent 容器中去获取 parent 容器中原本就存在的 Bean,也可以从 child 容器中去获取 child 容器原本的 Bean 或者是 parent 的 Bean 都可以。

这就是父子容器。

父容器和子容器本质上是相互隔离的两个不同的容器,所以允许同名的 Bean 存在。当子容器调用 getBean 方法去获取一个 Bean 的时候,如果当前容器没找到,就会去父容器查找,一直往上找,找到为止。

核心就是 BeanFactory,这个松哥之前文章已经和小伙伴们介绍过了(BeanFactoryPostProcessor 和 BeanPostProcessor 有什么区别?),BeanFactory 有一个子类 HierarchicalBeanFactory,看名字就是带有层级关系的 BeanFactory:

public interface HierarchicalBeanFactory extends BeanFactory {
    /**
     * Return the parent bean factory, or {@code null} if there is none.
     */
    @Nullable
    BeanFactory getParentBeanFactory();
    /**
     * Return whether the local bean factory contains a bean of the given name,
     * ignoring beans defined in ancestor contexts.
     * &lt;p&gt;This is an alternative to {@code containsBean}, ignoring a bean
     * of the given name from an ancestor bean factory.
     * @param name the name of the bean to query
     * @return whether a bean with the given name is defined in the local factory
     * @see BeanFactory#containsBean
     */
    boolean containsLocalBean(String name);
}

只要是 HierarchicalBeanFactory 的子类就能配置父子关系。父子关系图如下:

2.3 特殊情况

需要注意的是,并不是所有的获取 Bean 的方法都支持父子关系查找,有的方法只能在当前容器中查找,并不会去父容器中查找:

ClassPathXmlApplicationContext parent = new ClassPathXmlApplicationContext("consumer_beans.xml");
ClassPathXmlApplicationContext child = new ClassPathXmlApplicationContext("merchant_beans.xml");
child.setParent(parent);
child.refresh();
String[] names1 = child.getBeanNamesForType(org.javaboy.merchant.RoleService.class);
String[] names2 = child.getBeanNamesForType(org.javaboy.consumer.RoleService.class);
System.out.println("names1 = " + Arrays.toString(names1));
System.out.println("names2 = " + Arrays.toString(names2));

如上,根据类型去查找 Bean 名称的时候,我们所用的是 getBeanNamesForType 方法,这个方法是由 ListableBeanFactory 接口提供的,而该接口和 HierarchicalBeanFactory 接口并无继承关系,所以 getBeanNamesForType 方法并不支持去父容器中查找 Bean,它只在当前容器中查找 Bean。

但是!如果你确实有需求,希望能够根据类型查找 Bean 名称,并且还能够自动去父容器中查找,那么可以使用 Spring 给我们提供的工具类,如下:

ClassPathXmlApplicationContext parent = new ClassPathXmlApplicationContext("consumer_beans.xml");
ClassPathXmlApplicationContext child = new ClassPathXmlApplicationContext();
child.setParent(parent);
child.refresh();
String[] names = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(child, org.javaboy.consumer.RoleService.class);
for (String name : names) {
    System.out.println("name = " + name);
}

不过这个查找,对于父子容器中同名的 Bean 是查找不出来名字的。

2.4 Spring 和 SpringMVC

上面的内容理解了,Spring 和 SpringMVC 之间的关系就好理解了,Spring 是父容器,SpringMVC 则是子容器。

在 SpringMVC 中,初始化 DispatcherServlet 的时候,会创建出 SpringMVC 容器,并且为 SpringMVC 容器设置 parent,相关代码如下:

FrameworkServlet#initWebApplicationContext:

protected WebApplicationContext initWebApplicationContext() {
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;
    if (this.webApplicationContext != null) {
        // A context instance was injected at construction time -&gt; use it
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext cwac &amp;&amp; !cwac.isActive()) {
            // The context has not yet been refreshed -&gt; provide services such as
            // setting the parent context, setting the application context id, etc
            if (cwac.getParent() == null) {
                // The context instance was injected without an explicit parent -&gt; set
                // the root application context (if any; may be null) as the parent
                cwac.setParent(rootContext);
            }
            configureAndRefreshWebApplicationContext(cwac);
        }
    }
    if (wac == null) {
        // No context instance was injected at construction time -&gt; see if one
        // has been registered in the servlet context. If one exists, it is assumed
        // that the parent context (if any) has already been set and that the
        // user has performed any initialization such as setting the context id
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // No context instance is defined for this servlet -&gt; create a local one
        wac = createWebApplicationContext(rootContext);
    }
    return wac;
}

这里的 rootContext 就是父容器,wac 就是子容器,无论哪种方式得到的子容器,都会尝试给其设置一个父容器。

如果我们在一个 Web 项目中,不单独配置 Spring 容器,直接配置 SpringMVC 容器,然后将所有的 Bean 全部都扫描到 SpringMVC 容器中,这样做是没有问题的,项目是可以正常运行的。但是一般项目中我们还是会把这两个容器分开,分开有如下几个好处:

  • 方便管理,SpringMVC 主要处理控制层相关的 Bean,如 Controller、视图解析器、参数处理器等等,而 Spring 层则主要控制业务层相关的 Bean,如 Service、Mapper、数据源、事务、权限等等相关的 Bean。
  • 对于新手而言,两个容器分开配置,可以更好的理解 Controller、Service 以及 Dao 层的关系,也可以避免写出来在 Service 层注入 Controller 这种荒唐代码。

另外再额外说一句,有的小伙伴可能会问,如果全部 Bean 都扫描到 Spring 容器中不用 SpringMVC 容器行不行?这其实也可以!但是需要一些额外的配置,这个松哥下篇文章再来和小伙伴们细述。

3. 小结

好啦,Spring 容器中的父子容器现在大家应该明白了吧?可以给非 ListableBeanFactory 容器设置父容器,父容器不可以访问子容器的 Bean,但是子容器可以访问父容器的 Bean。

以上就是Spring中的父子容器原理解析的详细内容,更多关于Spring父子容器的资料请关注脚本之家其它相关文章!

相关文章

  • 利用Java编写一个Java虚拟机

    利用Java编写一个Java虚拟机

    这篇文章主要为大家详细介绍了如何使用 Java17 编写的 Java 虚拟机,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的可以了解下
    2023-07-07
  • MyBatis-Plus如何关闭SQL日志打印详解

    MyBatis-Plus如何关闭SQL日志打印详解

    在使用mybatisplus进行开发时,日志是一个非常有用的工具,它可以帮助我们更好地了解和调试我们的代码,这篇文章主要给大家介绍了关于MyBatis-Plus如何关闭SQL日志打印的相关资料,需要的朋友可以参考下
    2024-03-03
  • 使用SpringBoot中的Schedule定时发送邮件的方法

    使用SpringBoot中的Schedule定时发送邮件的方法

    在SpringBoot中,你可以使用@Scheduled注解来创建定时任务,@Scheduled注解可以应用于方法上,表示这个方法是一个定时任务,可以根据指定的时间间隔或固定时间执行,本文就给大家介绍一下如何使用SpringBoot中的Schedule定时发送邮件,需要的朋友可以参考下
    2023-08-08
  • mybatis对传入基本类型参数的判断方式

    mybatis对传入基本类型参数的判断方式

    这篇文章主要介绍了mybatis对传入基本类型参数的判断方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • 使用JavaSE来模拟斗地主

    使用JavaSE来模拟斗地主

    这篇文章主要介绍了使用JavaSE来模拟斗地主,对棋牌游戏有研究的同学可以参考一下
    2021-04-04
  • springboot集成普罗米修斯(Prometheus)的方法

    springboot集成普罗米修斯(Prometheus)的方法

    这篇文章主要介绍了springboot集成普罗米修斯(Prometheus)的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-08-08
  • Springboot微服务分布式框架Rouyi Cloud权限认证(登录流程之token解析)

    Springboot微服务分布式框架Rouyi Cloud权限认证(登录流程之token解析)

    这篇文章主要介绍了Springboot微服务分布式框架Rouyi Cloud权限认证的相关知识,重点讲解下整个框架的入口,登录流程之token解析,感兴趣的朋友跟随小编一起看看吧
    2024-04-04
  • 在Spring Boot中加载初始化数据的实现

    在Spring Boot中加载初始化数据的实现

    这篇文章主要介绍了在Spring Boot中加载初始化数据的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • Base64与File之间的相互转化方式

    Base64与File之间的相互转化方式

    这篇文章主要介绍了Base64与File之间的相互转化方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • SpringBoot3.x中spring.factories SPI 服务发现机制的改变问题小结

    SpringBoot3.x中spring.factories SPI 服务发现机制的改变问题小结

    spring.factories其实是SpringBoot提供的SPI机制,底层实现是基于SpringFactoriesLoader检索ClassLoader中所有jar引入的META-INF/spring.factories文件,这篇文章主要介绍了SpringBoot3.x中spring.factories SPI 服务发现机制的改变,需要的朋友可以参考下
    2023-05-05

最新评论