1. 程式人生 > >SpringMVC中為什麼要配置Listener和Servlet

SpringMVC中為什麼要配置Listener和Servlet

一直以來,我們使用SpringMVC的時候習慣性都配置一個ContextLoaderListener,雖然曾經有過疑問,配置的這個監聽器和Servlet究竟做了什麼,但也沒深究。

要說任何Web框架都離不開Servlet,它是一個容器,也是一種規範,你要和Web搞上關係,無非就是那麼幾種,監聽器、過濾器和Servlet,最終都是為了切進ServletContext。

SpringMVC是基於Servlet的,直接配個Servlet不就行了嘛,但我沒有真正這麼做過。上次群裡有個人說根本不需要配置監聽器,他從來不配那玩意,也沒啥事,於是我也實驗了一把。

然後有了下面的總結:

ContextLoaderListener

啟動方式:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener
    </listener-class>
</listener>

在contextInitialized方法中呼叫initWebApplicationContext方法

/**
 * Initialize the root web application context.
 */
public void contextInitialized(ServletContextEvent event) {
    this
.contextLoader = createContextLoader(); if (this.contextLoader == null) { this.contextLoader = this; } this.contextLoader.initWebApplicationContext(event.getServletContext()); }

1. ServletContext中查詢指定key的例項

WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE的例項如果存在則丟擲異常,不存在則建立。

String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";

2. createWebApplicationContext(servletContext)

可以指定自己的contextClass 如果沒有 則使用預設的:

org.springframework.web.context.support.XmlWebApplicationContext

如果是自己的需要實現ConfigurableWebApplicationContext(通過isAssignableFrom方法判斷,注意和instanceof區別)

3. 配置和重新整理WebApplicationContext

configureAndRefreshWebApplicationContext(cwac, servletContext);

  • wac.setServletContext(sc); 把servletContext放到上下文中持有
  • wac.setConfigLocation(configLocationParam); 利用servletContext的getInitParameter方法獲取配置檔案的位置。
  • wac.refresh(); 呼叫繼承的refresh方法初始化IOC容器。

4. 把建立的根上下文放到servletContext中

servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
this.context);

把這個上下文放到servletContext中。

DispatcherServlet

GenericServlet中定義了一個

 public void init() throws ServletException {

 }

被HttpServlet繼承 HttpServletBean又繼承了HttpServlet

在init方法中呼叫initServletBean();

protected void initServletBean() throws ServletException {

}

利用模板方法模式,在子類FrameworkServlet中實現,然後在下面的方法中初始化WebApplicationContext:

initWebApplicationContext();

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);
}

1. 先取rootContext

WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());

在這裡會去servletContext中取出key為‘ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE’的WebApplicationContext作為一個rootContext

由於監聽器啟動建立了這個rootContext,現在可直接拿出來

2. findWebApplicationContext()

protected WebApplicationContext findWebApplicationContext() {
    String attrName = getContextAttribute();
    if (attrName == null) {
        return null;
    }
    WebApplicationContext wac =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
    if (wac == null) {
        throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
    }
    return wac;
}

由於attrName為null,直接返回null。

3. createWebApplicationContext(rootContext)

在沒有啟動監聽器的情況下,就沒有建立父上下文,然後依然是使用 :
org.springframework.web.context.support.XmlWebApplicationContext

ConfigurableWebApplicationContext wac =
                (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

wac.setEnvironment(getEnvironment());
wac.setParent(parent);
wac.setConfigLocation(getContextConfigLocation());

4. configureAndRefreshWebApplicationContext(wac);

wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
……
wac.refresh();

使用shiro報錯

如果只配置一個dispatcherServlet然後使用shiro

啟動時報錯:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named ‘lifecycleBeanPostProcessor’ is defined

注意:springMVC的配置檔案中包含了對lifecycleBeanPostProcessor的引用
這時候要將SpringMVC的配置檔案掃描擴大到所有spring的配置檔案

訪問時報錯:

java.lang.IllegalStateException: No WebApplicationContext found: no ContextLoaderListener registered?

關掉shiro配置後,不再報錯,說明是shiro引起的,跟著shiro的配置,找到了一個突破口:DelegatingFilterProxy

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy
    </filter-class>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
    throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?");
}

initFilterBean()的時候呼叫一次findWebApplicationContext()

doFilter()時還會呼叫

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    // Lazily initialize the delegate if necessary.
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
        synchronized (this.delegateMonitor) {
            if (this.delegate == null) {
                WebApplicationContext wac = findWebApplicationContext();
                if (wac == null) {
                    throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?");
                }
                this.delegate = initDelegate(wac);
            }
            delegateToUse = this.delegate;
        }
    }

    // Let the delegate perform the actual doFilter operation.
    invokeDelegate(delegateToUse, request, response, filterChain);
}

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());
    }
}

出問題的關鍵在此:

public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
    return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}

先取attrName,如果不為null,到servletContext中取一個指定名稱的WebApplicationContext。沒有配置這個屬性,預設都是取root webApplicationContext。

現在沒有配置監聽器,root webApplicationContext當然不存在,不過給它指定一個contextAttribute也許可以解決,於是我在web.xml中給shiro加了一個初始化引數:

<init-param>
    <param-name>contextAttribute</param-name>
    <param-value>org.springframework.web.servlet.FrameworkServlet.CONTEXT.springServlet</param-value>
</init-param>

這下竟然可以跑了,但是其它問題還沒驗證。反正我不推薦這種方式。

總結:

  1. 監聽器啟動和servlet啟動都會initWebApplicationContext,由各自的初始化方法來觸發。

  2. 兩者各自的上下文最終都會儲存到servletContext中,root WebApplicationContext使用固定的attrName,dispatcherServlet使用FrameworkServlet.class.getName() + “.CONTEXT.”+servletName。

  3. 最佳實踐還是監聽器加servlet的配置,各司其職,父上下文做核心容器,子上下文處理web相關。如果你要把springMVC換成其他框架如struts,也不會有什麼影響。

  4. 監聽器可以在整個webapp啟動時初始化IOC容器,並在關閉時做一些處理。

  5. 不管是基於xml和註解的Application還是WebApplication,最終都是那套,呼叫繼承自AbstractApplicationContext的refresh()方法。

引用下stackoverflow上關於是否要監聽器的回答:

In your case, no, there’s no reason to keep the ContextLoaderListener and applicationContext.xml. If your app works fine with just the servlet’s context, that stick with that, it’s simpler.

Yes, the generally-encouraged pattern is to keep non-web stuff in the webapp-level context, but it’s nothing more than a weak convention.

The only compelling reasons to use the webapp-level context are:

  • If you have multiple DispatcherServlet that need to share services
  • If you have legacy/non-Spring servlets that need access to Spring-wired services
  • If you have servlet filters that hook into the webapp-level context (e.g. Spring Security’s DelegatingFilterProxy, OpenEntityManagerInViewFilter, etc)
    None of these apply to you, so the extra complexity is unwarranted.

Just be careful when adding background tasks to the servlet’s context, like scheduled tasks, JMS connections, etc. If you forget to add to your web.xml, then these tasks won’t be started until the first access of the servlet.

You need to understand the difference between Web application context and root application context .

In the web MVC framework, each DispatcherServlet has its own WebApplicationContext, which inherits all the beans already defined in the root WebApplicationContext. These inherited beans defined can be overridden in the servlet-specific scope, and new scope-specific beans can be defined local to a given servlet instance.

相關連結: