您的位置:首页 > 编程语言 > Java开发

Spring+SpringMVC父子容器?

2020-07-12 17:24 537 查看

这篇文章分享一下关于早期版本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版本后就默认关闭了父子容器,只用同一个容器也就是说都到子容器找。

好了,关于父子容器的知识在这里只做粗浅的认识,其它未涉及点可以自行了解。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: