写点什么

Spring 中的父子容器是咋回事?

作者:江南一点雨
  • 2023-07-12
    广东
  • 本文字数:5906 字

    阅读完需:约 19 分钟

Spring 中的父子容器是咋回事?

@[toc]相信有小伙伴也听说过,在 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. * <p>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 -> use it    wac = this.webApplicationContext;    if (wac instanceof ConfigurableWebApplicationContext cwac && !cwac.isActive()) {      // The context has not yet been refreshed -> 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 -> 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 -> 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 -> create a local one    wac = createWebApplicationContext(rootContext);  }  return wac;}
复制代码


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


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


  1. 方便管理,SpringMVC 主要处理控制层相关的 Bean,如 Controller、视图解析器、参数处理器等等,而 Spring 层则主要控制业务层相关的 Bean,如 Service、Mapper、数据源、事务、权限等等相关的 Bean。

  2. 对于新手而言,两个容器分开配置,可以更好的理解 Controller、Service 以及 Dao 层的关系,也可以避免写出来在 Service 层注入 Controller 这种荒唐代码。


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

3. 小结

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

发布于: 刚刚阅读数: 3
用户头像

技术宅 2019-04-09 加入

Java猿

评论

发布
暂无评论
Spring 中的父子容器是咋回事?_Java_江南一点雨_InfoQ写作社区