最近在使用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. 圣诞老人去哪?Power BI告诉你

    随着圣诞节的来临,微软的Power BI团队使用Power BI来回答大家一直以来所关心的问题:圣诞老人去哪? 要回答这个问题,来自社交网络的数据是最合适不过的了.于是Power BI团队用以下关键字 ...

  2. 数据的平面拟合 Plane Fitting

    数据的平面拟合 Plane Fitting 看到了一些利用Matlab的平面拟合程序 http://www.ilovematlab.cn/thread-220252-1-1.html

  3. XCode6 生成prefix.pch文件

    XCode6里, 新建工程默认是没有pch文件的,苹果取消pch文件这一点肯定有它的道理,刚开始很多人可能不适应,如果我们想使用pch文件,需要手动添加,添加步骤如下:(依旧直接上图)

  4. 二维树状数组 BZOJ 1452 [JSOI2009]Count

    题目链接 裸二维树状数组 #include <bits/stdc++.h> const int N = 305; struct BIT_2D { int c[105][N][N], n, ...

  5. 【原】iOS学习之UIStoryboardSegue解析

    在 Storyboard 的可视化编程中,跳转界面就是按住 Ctrl 使用鼠标头一条连线就可以解决,相当的简单!本篇博客主要就是介绍这条连线,在iOS中,这条连线也是一个对象,也有其自己的初始化方法和 ...

  6. 非常详细的/etc/passwd解释

    root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nol ...

  7. 【JAVA】FOR UPDATE 和 FOR UPDATE NOWAIT 区别 (转)

    1.for update 和 for update nowait 的区别:首先一点,如果只是select 的话,Oracle是不会加任何锁的,也就是Oracle对 select 读到的数据不会有任何限 ...

  8. IOS_ios逆向工程-静态分析

    返回博客列表 原 ios逆向工程-静态分析 余成海 发布时间: 2014/11/03 19:17 阅读: 11201 收藏: 17 点赞: 5 评论: 6 最近在学习IOS逆向工程,查看网络上的资料也 ...

  9. mysql错误汇总

    1,mysql报Fatal error encountered during command execution的解决办法 连接字符串里加上 Allow User Variables=True 解决. ...

  10. HDU1541 树状数组

    Stars Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submi ...