Spring+SpringMVC父子容器?
这篇文章分享一下关于早期版本Spring集成SpringMVC时的一个重要概念:父子容器。
关于父子容器,主要有几个点需要了解:
(1)何为父子容器,怎么产生的,父子容器之间的联系?
(2)父子容器各自Bean在包扫描初始化时怎么被自动分配到对应容器里?
(3)在父子容器中获取Bean时有什么先后顺序,怎么验证这个结论?
(4)新版本的Spring+SpringMVC对于父子容器是怎么处理的?
首先了解何为父子容器,这个概念是什么原因导致的?
父子容器是早期版本3.0前Spring与SpringMVC整合时产生的一个概念,父子容器顾名思义就是一个WEB应用中包含多个Spring的容器,为了让这些容器彼此之间进行关联就采用父子节点的方式进行关联,这就是父子容器。在早期版本Spring和SpringMVC都有各自的applicationContext/spring配置文件,这两个配置文件会产生各自的context容器实例。那么Spring和SpringMVC的容器在设计时就设计成了一种父子关系,Spring容器为父容器,SpringMVC为子容器,子容器可以引用父容器中的Bean,而父容器不可以引用子容器中的Bean。但是在现在的版本这个父子容器已经被默认关闭了,也就是说后面版本默认只有一个容器了,后面通过SpringMVC源码以及测试代码进行验证。
关于父子容器介绍以及结构图介绍,官网中1.1.1. Context Hierarchy章节有简短介绍:
上图中显示了2个WebApplicationContext实例,为了进行区分,分别称之为:Servlet WebApplicationContext、Root WebApplicationContext。 其中:
- Servlet WebApplicationContext:这是对web层进行配置,如控制器(controller)、视图解析器(view resolvers)等相关的bean。通过spring mvc中提供的DispatchServlet来加载配置,通常情况下,配置文件的名称为spring.xml。
- Root WebApplicationContext:这是对J2EE三层架构中的service层、dao层进行配置,如业务bean,数据源(DataSource)等。通常情况下,配置文件的名称为applicationContext.xml。在web应用中,其一般通过ContextLoaderListener来加载。
父子容器都存在于DispatcheServlet当中,这两个配置文件在web.xml中有所体现,其中spring.xml是作为参数标签放在DispatcherServlet中,applicationContext.xml则作为全局配置由ContextLoaderListener来加载。
[code] <!-- Spring MVC 配置 并添加监听--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <!-- ContextLoaderListener监听器配置:web应用启动时能加载Spring环境, 由于是web应用没有main方法,因此使用这个监听器可被Tomcat启动时加载,作为程序入口 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- DispatcherServlet转发器配置,会加载classpath:spring.xml --> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
父子容器在web.xml中是怎么创建的呢?
下面通过applicationContext.xml创建的是Root WebApplicationContext父容器:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
下面通过spring.xml创建的就是子容器Servlet WebApplicationContext:
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
那么这里就引出一个问题:父子容器中的Bean在扫描时是怎么被自动分配到对应容器中的?
理解这个问题之前,思考一下为什么我们在使用SpringMVC时,@Service,@Component等注解为什么要与@Controller分开配置进行扫描?原因是在SpringMVC的配置文件中扫描Controlller Bean(@Controller),而在Spring的配置文件中只扫描除了Controller之外的其他Bean(如@Service,@Repository,@Component等),分开扫描有什么好处呢?
如下在web.xml引入的父子容器各自配置文件可看出,两个配置文件分别进行了包扫描:
根据上面对父子容器关系的结论可知:子容器可以获取父容器内的Bean,而父容器不能获取子容器内的Bean。于是Spring扫描出的@Service,@Repository,@Component等Bean则作为全局父容器Bean能被其它子容器获取,而SpringMVC作为子容器其扫描出的@Controller Bean则不会被Spring获取,这就保证了子容器的Bean不会对父容器Spring内部的逻辑产生影响,可以做到Bean的分容器管理。也因此按照以上配置,我们可以在@Controller中注入@Service,@Component,@Repository等Bean,但是不能在@Service中注入@Controller Bean。
如果不分开扫描,若Controller被Spring扫描,那么SpringMVC作为子容器将不能获取到父容器扫描出的Controller Bean,这也就很自然会导致整个WEB应用程序的崩溃。
那么回过头来学习,父子容器各自支持的Bean是怎么进行扫描的?
关于@Service,@Repository,@Component等Bean设计在Spring层面的父容器扫描,扫描机制是Spring Scanner相关的知识需要对Spring源码有较深入了解才能掌握Scanner的包扫描机制,这里小编不展开研究。
而对于@Controller Bean设计在SpringMVC层面的子容器扫描,由于官网没找到相关的描述网上资料也没深入讲解因此关于Controller的扫描暂时不做研究,但是可以进行猜想。那就是去扫描整个项目工程获取所有的类,然后判断哪些类是加了@Controller注解的,并且跳过@Service,@Repository,@Component属于Spring扫描的类。
在父子容器中获取Bean时有什么先后顺序,怎么验证这个结论?
在SpringMVC中,可以给容器的构造函数传入一个父容器对象,也就是A a = new A(); B b = new B(a); 当容器B初始化时可以将A容器传入构造函数中,这就构成了一个父子容器,其中A就是B的父容器。在早期版本SpringMVC中通过B.getBean查找Bean时,默认会尝试先去父容器A查找Bean。而在SpringMVC后来版本就默认父子容器已关闭,用的是同一个容器也就是说都到子容器找。
上面是3.0前早期和现在版本通过子容器查找Bean的顺序,那么怎么验证这个结论呢?在验证这个结论之前,让我们先看一下SpringMVC中何时创建父子容器,并看看新版本的父子容器默认设置关闭是回事?
我们知道父子容器最终是存在于DispatchServler中,因此肯定要往这个DispatchServler里面找,对于XML的配置方式通过前面流程原理的解读,我们大致可以知道这个初始化的工作应该先在ContextLoader.initWebApplicationContext()进行,然后再调往FrameworkServlet的initWebApplicationContext()方法中:
上面先创建父容器rootContext,然后是创建子容器WebApplicationContext wac = null;由于上面一个大if内部是对父容器的处理,因此此时得到的cwac依旧是父容器。对于子容器wac的初始化在下面进行:
会调用findWebApplicationContext扫描整个工程的子容器,如果工程中没有配置子容器则默认会帮应用创建一个子容器出来,并传入父容器构成父子容器。然后这个父子容器在哪里应用呢?通过SpringMVC流程源码可知,一个请求路径需要通过映射处理器进行处理,那么自然会获取映射处理器对应的Bean,因此这一个过程相比会经过父子容器获取Bean的逻辑,让我们用代码验证我们的推测,可以在RequestMappingHandlerMapping.afterPropertiesSet-->initHandlerMethods方法找到关于从父子容器中获取Bean的例子:
这里会有一个boolean类型的标识:
this.detectHandlerMethodsInAncestorContexts
这个标识就是我们上面说的是否开启父子容器的标识,在新版本中默认值都是false,除非自己调用指定方法修改这个标识。也就是说新版本的SpringMVC在执行
String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) : obtainApplicationContext().getBeanNamesForType(Object.class));
这行代码时,默认是执行obtainApplicationContext().getBeanNamesForType(Object.class),也就是默认只在子容器中查找Bean。那如果我们将父子容器标识打开呢,就会执行BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class),
从这个方法的内部实现我们就可以知道如果开启父子容器,则首先会去父容器找Bean。那么调用什么方法可以控制这个开关标识呢?那就只有调用AbstractHandlerMethodMapping对象的下面方法进行修改,这个方法在接下来测试结论时的测试类会使用到:
public void setDetectHandlerMethodsInAncestorContexts(boolean detectHandlerMethodsInAncestorContexts) {this.detectHandlerMethodsInAncestorContexts = detectHandlerMethodsInAncestorContexts; }
看完获取Bean的逻辑后,我们现在要进行验证。可以在Spring编译出的源码中tests包找到下面一个Test类,这个是由Spring工程师自己写的一个关于父子容器的测试类,验证结果请细看main方法的结果输出注释:
package com.myspringmvc.testcontains; import org.springframework.context.support.StaticApplicationContext; import org.springframework.stereotype.Controller; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping; import org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy; import org.springframework.web.util.UrlPathHelper; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.util.Collections; import java.util.Comparator; import java.util.Set; ///** // * Test for {@link AbstractHandlerMethodMapping}. // * // * @author Arjen Poutsma // * @author Rossen Stoyanchev // */ @SuppressWarnings("unused") public class HandlerMethodMappingTests { /** * 创建MyHandler,提供两个HandlerMethod,但是构造函数没有被执行,说明这个 */ @Controller static class MyHandler { public MyHandler(){ System.out.println("----------"); } @RequestMapping public void handlerMethod1() { } @RequestMapping public void handlerMethod2() { } } public void detectHandlerMethodsInAncestorContexts1() { //当前的父容器,在父容器注册上了两个handlerMethod StaticApplicationContext cxt = new StaticApplicationContext(); cxt.registerSingleton("myHandler", MyHandler.class); AbstractHandlerMethodMapping<String> mapping1 = new MyHandlerMethodMapping(); //新建一个子容器,传入cxt作为父容器 mapping1.setApplicationContext(new StaticApplicationContext(cxt)); mapping1.afterPropertiesSet(); //通过子容器获取handlerMethod的实例 System.out.println(mapping1.getHandlerMethods().size()); //这里会加载几个controller? 0个,因为新版本没有开启父子节点,这里只能去子容器找,因此子容器找不到 //还是上面的父子容器,设置父子容器开关为true AbstractHandlerMethodMapping<String> mapping2 = new MyHandlerMethodMapping(); mapping2.setDetectHandlerMethodsInAncestorContexts(true); mapping2.setApplicationContext(new StaticApplicationContext(cxt)); mapping2.afterPropertiesSet(); System.out.println(mapping2.getHandlerMethods().size()); //这里会加载几个controller? 2个,因为开启父子容器则默认先去父容器找,会找到2个 } public static void main(String[] args) throws Exception{ HandlerMethodMappingTests handlerMethodMappingTests = new HandlerMethodMappingTests(); handlerMethodMappingTests.detectHandlerMethodsInAncestorContexts1(); } private static class MyHandlerMethodMapping extends AbstractHandlerMethodMapping<String> { private UrlPathHelper pathHelper = new UrlPathHelper(); private PathMatcher pathMatcher = new AntPathMatcher(); public MyHandlerMethodMapping() { setHandlerMethodMappingNamingStrategy(new SimpleMappingNamingStrategy()); } @Override protected boolean isHandler(Class<?> beanType) { return true; } @Override protected String getMappingForMethod(Method method, Class<?> handlerType) { String methodName = method.getName(); return methodName.startsWith("handler") ? methodName : null; } @Override protected Set<String> getMappingPathPatterns(String key) { return (this.pathMatcher.isPattern(key) ? Collections.<String>emptySet() : Collections.singleton(key)); } @Override protected CorsConfiguration initCorsConfiguration(Object handler, Method method, String mapping) { CorsConfiguration corsConfig = new CorsConfiguration(); corsConfig.setAllowedOrigins(Collections.singletonList("http://" + handler.hashCode() + method.getName())); return corsConfig; } @Override protected String getMatchingMapping(String pattern, HttpServletRequest request) { String lookupPath = this.pathHelper.getLookupPathForRequest(request); return this.pathMatcher.match(pattern, lookupPath) ? pattern : null; } @Override protected Comparator<String> getMappingComparator(HttpServletRequest request) { String lookupPath = this.pathHelper.getLookupPathForRequest(request); return this.pathMatcher.getPatternComparator(lookupPath); } } private static class SimpleMappingNamingStrategy implements HandlerMethodMappingNamingStrategy<String> { @Override public String getName(HandlerMethod handlerMethod, String mapping) { return handlerMethod.getMethod().getName(); } } }
新版本的Spring+SpringMVC对于父子容器是怎么处理的?
这个问题实际上在前面通过源码和测试类也验证了结论。早期版本SpringMVC中通过子容器查找Bean时,默认会尝试先去父容器查找。而在SpringMVC3.0版本后就默认关闭了父子容器,只用同一个容器也就是说都到子容器找。
好了,关于父子容器的知识在这里只做粗浅的认识,其它未涉及点可以自行了解。
- spring 父子容器 事务管理
- Spring和SpringMVC父子容器关系初窥
- [转]Spring IOC父子容器简介
- spring和springMVC父子容器的原理
- spring 父子容器的概念,入门博客推荐。必看
- Spring和SpringMVC父子容器关系初窥
- 关于Spring父子容器的理解
- Spring 父子容器使用实例
- spring的启动过程——spring和springMVC父子容器的原理
- Spring和springmvc父子容器注解扫描问题详解
- Spring父子容器加载过程描述
- Spring源码-父子容器
- spring 父子容器
- Spring父子容器关系引发的小问题 ——用@Value取值时失败
- Spring和SpringMVC父子容器关系初窥
- Spring和SpringMVC父子容器关系初窥
- Spring、SpringMVC父子容器关系浅析
- Spring和SpringMVC父子容器关系初窥
- Spring和SpringMVC父子容器关系初窥
- Springmvc整合Spring时,Spring配置文件扫描包与springmvc配置文件扫描包的不同(Spring父子容器)