最近在使用SpringSession时遇到一个问题,错误日志如下:

Exception sending context initialized event to listener instance of class org.springframework.web.context.ContextLoaderListener
java.lang.IllegalStateException: Cannot initialize context because there is already a root application context present - check whether you have multiple ContextLoader* definitions in your web.xml!

先说下项目的配置情况:

SpringSession完全按照官方文档配置如下,

@EnableRedisHttpSession
public class Config { @Bean
public JedisConnectionFactory connectionFactory(@RedisServerPort int port) {
JedisConnectionFactory connection = new JedisConnectionFactory();
connection.setPort(port);
return connection;
}
}
public class Initializer
extends AbstractHttpSessionApplicationInitializer { public Initializer() {
super(Config.class);
}
}

web.xml也是标准的配置方法:

   <context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:config/spring/rpc-service.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener> <servlet>
<servlet-name>mvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:config/spring/spring-mvc-main.xml</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>

要想知道这个错误产生的原因,先要弄清楚Spring的容器(ApplicationContext)的继承原理及SpringMVC如何使用这一机制的。

Spring容器的继承关系



如上图那样,容器之间可以像对象的继承关系一样,子容器通过setParent方法来设置自己的父容器。在调用容器的getBean查找实例时,依次从当前容器往父容器查找,直到找到满足的对象即返回,如果一直没有找到则返回Null.

SpringMVC中的容器即它们的关系

在使用SpringMVC时,必需要配置org.springframework.web.servlet.DispatcherServlet这样的一个servlet。在初始化此实例时,会生成一个WebApplicationContext容器,生成容器后会检查当前ServletContext环境下是否已经存在"rootContext",如果存在,则通过setParent方法设置为父容器。源代码在这里(org.springframework.web.servlet.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) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!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);
} if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
onRefresh(wac);
} if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
"' as ServletContext attribute with name [" + attrName + "]");
}
} return wac;
}

那这个rootContext从哪里来的呢?答案是org.springframework.web.context.ContextLoaderListener,它是一个标准的javax.servlet.ServletContextListener实现,在容器启动的时候创建一个全局唯一的rootContext,代码在:org.springframework.web.context.ContextLoader#initWebApplicationContext下:

   public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
} Log logger = LogFactory.getLog(ContextLoader.class);
servletContext.log("Initializing Spring root WebApplicationContext");
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis(); try {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!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 ->
// determine parent for root web application context, if any.
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
} if (logger.isDebugEnabled()) {
logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
} return this.context;
}
catch (RuntimeException ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
catch (Error err) {
logger.error("Context initialization failed", err);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
throw err;
}
}

总结下结果:servlet容器通过ContextLoaderListener创建一个root容器,并设置为SpringMVC的父容器。 到止应该明白了文章最开始的报错信息的来源了,就是在这个方法里报错。出错的原因有且只有一个:就是给servlet容器注册了两个ContextLoaderListener。一个是在web.xml配置文件里配置的,那另一个在哪里注册的呢?接着分析。

SpringSession的加载机制

集成SpringSession是很简单的,只要实现一个"AbstractHttpSessionApplicationInitializer "的子类即可,然后在子类的构造器中传一个标注了EnableRedisHttpSession的注解类,此注解继承了Configuration,所以在类Initializer进行初始化时,会调用“org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer#onStartup”方法,代码如下:

    public void onStartup(ServletContext servletContext)
throws ServletException {
beforeSessionRepositoryFilter(servletContext);
if(configurationClasses != null) {
AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
rootAppContext.register(configurationClasses);
servletContext.addListener(new ContextLoaderListener(rootAppContext));
}
insertSessionRepositoryFilter(servletContext);
afterSessionRepositoryFilter(servletContext);
}

现在我们找到了另外一个往servlet容器中注册ContextLoaderListener的地方了,也就是在这个地方抛错了。找到了问题的根源,解决问题就很简单了。

解决问题

其实只要保证ContextLoaderListener只注册一次就不会有这个问题了,所以有两个选择做法:要么别在web.xml里配置ContextLoaderListener,要么在Initializer类的构造方法中,不要调用父类的有参数构造器,而是调用空参构造器。为了遵守SpringMVC官方的开发规范,最好还是要配置下ContextLoaderListener,把非web层而的对象单独配置,比如service层对象。而web层的东西配置在dispatcher容器中。但是这样即使这样做了,会报别外一个错误,说"org.springframework.data.redis.connection.jedis.JedisConnectionFactory"找不到。所以要在spring的配置文件中加入如下配置:

   <bean class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="xxxx"/>
<property name="port" value="xxxx"/>
<property name="password" value="xxx"/>
</bean>

如果你加入这个配置,也还是报相同错误的话,那么就要检查下这个配置是放在哪个spring的配置文件下,如果放在ContextLoaderListener的配置文件下就不会报错,而放在DispatcherServlet的配置下就会报错。原因还是从代码(org.springframework.web.filter.DelegatingFilterProxy#findWebApplicationContext)里看:

   protected WebApplicationContext findWebApplicationContext() {
if (this.webApplicationContext != null) {
// the user has injected a context at construction time -> use it
if (this.webApplicationContext instanceof ConfigurableApplicationContext) {
if (!((ConfigurableApplicationContext)this.webApplicationContext).isActive()) {
// the context has not yet been refreshed -> do so before returning it
((ConfigurableApplicationContext)this.webApplicationContext).refresh();
}
}
return this.webApplicationContext;
}
String attrName = getContextAttribute();
if (attrName != null) {
return WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
}
else {
return WebApplicationContextUtils.getWebApplicationContext(getServletContext());
}
}

这个方法的最后几行可以看出,SpringSession所需要的所有基础对象,比如Redis连接对象,Redis模板对象,都是从WebApplicationContext从获取。而WebApplicationContext根据getContextAttribute()的值不同先获取的方式也不同。如果getContextAttribute()返回为Null,则取的容器是rootContext,即ContextLoaderListener生成的容器。反之,获取的是DispatcherServlet容器。知道了原因,解决方式就清晰了。重写Initializer的getDispatcherWebApplicationContextSuffix方法。Initializer最终的代码如下:

@EnableRedisHttpSession
public class Initializer extends AbstractHttpSessionApplicationInitializer
{
@Override
protected String getDispatcherWebApplicationContextSuffix()
{
return "mvc"; # 这里返回的字符串就是你配置DispatcherServlet的名称

而本文前面提到的Config类可以删除不用了。

使用SpringMVC集成SpringSession的问题的更多相关文章

  1. EhCache WebCache 与 SpringMVC集成时 CacheManager冲突的问题

    转自:点击打开链接 http://www.cnblogs.com/daxin/p/3560989.html EhCache WebCache 与 SpringMVC集成时 CacheManager冲突 ...

  2. spring+springMVC集成(annotation方式)

    spring+springMVC集成(annotation方式) SpringMVC+Spring4.0+Hibernate 简单的整合 MyBatis3整合Spring3.SpringMVC3

  3. SpringMVC 集成velocity

    前言 没有美工的时代自然少不了对应的模板视图开发,jsp时代我们用起来也很爽,物极必反,项目大了,数据模型复杂了jsp则无法胜任. 开发环境 idea2016.jdk1.8.tomcat8.0.35 ...

  4. SpringMVC集成rabbitmq:优化秒杀下单环节

    前言 上一篇在springboot中基于自动配置集成了rabbitmq.那么回到最初的话题中就是想在秒杀下单环节增加排队机制,从而达到限流的目的. 优化秒杀下单流程 之前是在控制器里拿到客户端请求后直 ...

  5. SpringMVC集成springfox-swagger2自动生成接口文档

    本节内容: 什么是Swaggger Springfox与Swagger的关系 SpringMVC集成springfox-swagger2 一.什么是Swaggger Swagger是一个流行的API开 ...

  6. MP实战系列(十)之SpringMVC集成SpringFox+Swagger2

    该示例基于之前的实战系列,如果公司框架是使用JDK7以上及其Spring+MyBatis+SpringMVC/Spring+MyBatis Plus+SpringMVC可直接参考该实例. 不过建议最好 ...

  7. spring-mvc集成 swagger

    问题1:spring-mvc集成 swagger, 配置好后界面 404, 原因: dispatcher-servlet.xml 文件中, 要在这上面 <!-- 启用spring mvc 注解 ...

  8. Spring学习之旅(六)--SpringMVC集成

    对大多数 Java 开发来说,基于 web 的应用程序是我们主要的关注点. Spring 也提供了对于 web 的支持,基于 MVC 模式的 Spring MVC 能够帮助我们灵活和松耦合的完成 we ...

  9. webService学习之路(三):springMVC集成CXF后调用已知的wsdl接口

    webService学习之路一:讲解了通过传统方式怎么发布及调用webservice webService学习之路二:讲解了SpringMVC和CXF的集成及快速发布webservice 本篇文章将讲 ...

随机推荐

  1. [译]SSAS下玩转PowerShell

    操作SSAS数据库的方法有很多,是否有一种可以方法可以通过脚本自动去做这些事呢,比如处理分区,创建备份以及监视SSAS的运行状况. 原文地址: http://www.mssqltips.com/sql ...

  2. IBatis.Net使用总结(三)-- IBatis实现分页返回数据和总数

    IBatis 分页,这里没有使用其他插件,只使用最原始的方法. 输入参数: int currentPage 当前页 int  pageSize 每页大小 Hashtable findCondition ...

  3. [leetcode] 12. Integer to Roman

    关于罗马数字: I: 1V: 5X: 10L: 50C: 100D: 500M: 1000字母可以重复,但不超过三次,当需要超过三次时,用与下一位的组合表示:I: 1, II: 2, III: 3, ...

  4. 在visual studio2015中使用easyX画图

    配置:解压EasyX压缩包: 将文件内的include,lib,lib/amd64下的文件拷贝到visualstudio中VC文件夹内对应的地方: 然后再执行上图中的Setup.hta进行安装: 在v ...

  5. Angular过滤器

    angular中的过滤器有: currency 过滤货币 number  过滤数字, date  过滤日期 json 把js对象过滤为json字符串 limitTo  截取字符串,参数是正数则从左往右 ...

  6. 2016 Multi-University Training Contest 2

    8/13 2016 Multi-University Training Contest 2官方题解 数学 A Acperience(CYD)题意: 给定一个向量,求他减去一个  α(>=0)乘以 ...

  7. 【Oracle】去除表中重复的数据

    删除表重复数据 (t1表中有重复数据)1.使用distinct create table t2 as select * from t1;create table tmp_t2 as select di ...

  8. 【Cocos2d-x游戏开发】细数Cocos2d-x开发中那些常用的C++11知识

    自从Cocos2d-x3.0开始,Cocos2dx就正式的使用了C++11标准.C++11简洁方便的特性使程序的可拓展性和可维护性大大提高,也提高了代码的书写速度. 下面我们就来一起学习一下Cocos ...

  9. css 图片内容在不同分辨率下居中显示(演示的图片宽度是1920px,当图片宽度大于显示屏的宽度时)

    1.img 图片内容在不同分辨率下居中显示(如果隐藏多余,在img外面套一个div  设定overflow: hidden.div的大小就是img显示区域的大小) <!DOCTYPE html& ...

  10. ios7迎来完美越狱,果粉狂欢!

    [我要]最近一则iOS7可以完美越狱的消息,可是乐坏了期待已久的果粉们.据科技博客网站Gizmodo报道,越狱专家Evasi0n团队最近攻破苹果的 iOS7系统,赶在圣诞前发布了iOS7的越狱.消息一 ...