@[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 容器中,这样做是没有问题的,我的项目是能够失常运行的。然而个别我的项目中咱们还是会把这两个容器离开,离开有如下几个益处:
- 方便管理,SpringMVC 次要解决管制层相干的 Bean,如 Controller、视图解析器、参数处理器等等,而 Spring 层则次要管制业务层相干的 Bean,如 Service、Mapper、数据源、事务、权限等等相干的 Bean。
- 对于老手而言,两个容器离开配置,能够更好的了解 Controller、Service 以及 Dao 层的关系,也能够防止写进去在 Service 层注入 Controller 这种荒谬代码。
另外再额定说一句,有的小伙伴可能会问,如果全副 Bean 都扫描到 Spring 容器中不必 SpringMVC 容器行不行?这其实也能够!然而须要一些额定的配置,这个松哥下篇文章再来和小伙伴们细述。
3. 小结
好啦,Spring 容器中的父子容器当初大家应该明确了吧?能够给非 ListableBeanFactory 容器设置父容器,父容器不能够拜访子容器的 Bean,然而子容器能够拜访父容器的 Bean。