最近在使用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類可以刪除不用了。