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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM