乐趣区

关于spring: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。

退出移动版