使用SpringMVC集成SpringSession的问题
最近在使用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的问题的更多相关文章
- EhCache WebCache 与 SpringMVC集成时 CacheManager冲突的问题
转自:点击打开链接 http://www.cnblogs.com/daxin/p/3560989.html EhCache WebCache 与 SpringMVC集成时 CacheManager冲突 ...
- spring+springMVC集成(annotation方式)
spring+springMVC集成(annotation方式) SpringMVC+Spring4.0+Hibernate 简单的整合 MyBatis3整合Spring3.SpringMVC3
- SpringMVC 集成velocity
前言 没有美工的时代自然少不了对应的模板视图开发,jsp时代我们用起来也很爽,物极必反,项目大了,数据模型复杂了jsp则无法胜任. 开发环境 idea2016.jdk1.8.tomcat8.0.35 ...
- SpringMVC集成rabbitmq:优化秒杀下单环节
前言 上一篇在springboot中基于自动配置集成了rabbitmq.那么回到最初的话题中就是想在秒杀下单环节增加排队机制,从而达到限流的目的. 优化秒杀下单流程 之前是在控制器里拿到客户端请求后直 ...
- SpringMVC集成springfox-swagger2自动生成接口文档
本节内容: 什么是Swaggger Springfox与Swagger的关系 SpringMVC集成springfox-swagger2 一.什么是Swaggger Swagger是一个流行的API开 ...
- MP实战系列(十)之SpringMVC集成SpringFox+Swagger2
该示例基于之前的实战系列,如果公司框架是使用JDK7以上及其Spring+MyBatis+SpringMVC/Spring+MyBatis Plus+SpringMVC可直接参考该实例. 不过建议最好 ...
- spring-mvc集成 swagger
问题1:spring-mvc集成 swagger, 配置好后界面 404, 原因: dispatcher-servlet.xml 文件中, 要在这上面 <!-- 启用spring mvc 注解 ...
- Spring学习之旅(六)--SpringMVC集成
对大多数 Java 开发来说,基于 web 的应用程序是我们主要的关注点. Spring 也提供了对于 web 的支持,基于 MVC 模式的 Spring MVC 能够帮助我们灵活和松耦合的完成 we ...
- webService学习之路(三):springMVC集成CXF后调用已知的wsdl接口
webService学习之路一:讲解了通过传统方式怎么发布及调用webservice webService学习之路二:讲解了SpringMVC和CXF的集成及快速发布webservice 本篇文章将讲 ...
随机推荐
- js调试--查找dom对象绑定的函数
点击最右侧的js文件. 选中函数upload_pic_box,右击,选择在控制台中调试,或者在控制台直接输入该函数 点击最后一行代码会打开该函数所在的js文件
- 00 MySQL
1数据库 1.1名词解释 DB:数据库Database,用于存放数据仓库 DBMS:数据库管理系统 DataBase Management System,管理数据库 ta ...
- Xcode UUID查询
打开终端,输入: defaults read /Applications/Xcode.app/Contents/Info DVTPlugInCompatibilityUUID
- JS 之Blob 对象类型
原文 http://blog.csdn.net/oscar999/article/details/36373183 什么是Blob? Blob 是什么? 这里说的是一种Javascript的对象类型. ...
- LightOJ1126 Building Twin Towers(DP)
题目 Source http://www.lightoj.com/volume_showproblem.php?problem=1126 Description Professor Sofdor Al ...
- Web开发流程
知乎上关于Web开发流程豪情给予的回答 web前端开发流程是什么?进行操作会用到哪些便捷的小工具?是先用模板做好,然后在基础上改吗??正常大家说的改框架是不是指的用模板做的网站原文件?前端开发做的文件 ...
- Learn ZYNQ (9)
创建zybo cluster的spark集群(计算层面): 1.每个节点都是同样的filesystem,mac地址冲突,故: vi ./etc/profile export PATH=/usr/loc ...
- iOS CoreAnimation 核心动画
一 介绍 一组非常强大的动画处理API 直接作用在CALAyer上,并非UIView(UIView动画) CoreAnimation是所有动画的父类,但是不能直接使用,应该使用其子类 属性: dura ...
- 基于Jquery的页面过渡效果(原创)
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat=&qu ...
- 关于linux开机进入grub问题的解决方法
用还是用ls (hd0,X)/grub命令查看每个盘里面的内容, 情况一 :如果你是在/boot/grub这个目录下找到的 grub rescue>root=(hd0,9) gr ...