1. 程式人生 > >ServletContext與Web應用以及Spring容器啟動

ServletContext與Web應用以及Spring容器啟動

一、ServletContext物件獲取Demo

 

Servlet容器在啟動時會載入Web應用,併為每個Web應用建立唯一的ServletContext物件。

 

可以把ServletContext看作一個Web應用的伺服器端元件的共享記憶體。在ServletContext中可以存放共享資料,有4個讀取或者設定共享資料的方法:

 

 

CounterServlet.java

 

package com.servletContext.demo;

import java.io.IOException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CounterServlet extends HttpServlet {

   @Override
   public void init(ServletConfig config) throws ServletException {
       super.init(config);
   }

   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       doPost(req, resp);
   }

   @Override
   protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

       // 獲得ServletContext的引用
       ServletContext context = getServletContext();

       // 從ServletContext讀取count屬性
       Integer count = (Integer) context.getAttribute("count");


       // 如果沒有讀到count屬性,那麼建立count屬性,並設定初始值為0
       if(count == null) {
           System.out.println("context中還沒有count屬性呢");
           count = new Integer(0);
           context.setAttribute("count", count);
       }
       count = count + 1;
       // count增加之後還要寫回去,引用為什麼還要重新存回去
       context.setAttribute("count", count);
       System.out.println("您是第" + count + "個訪問的!");

   }

   @Override
   public void destroy() {
       super.destroy();
   }

}

 

從上述程式碼中可見通過getServletContext()方法可以直接獲得ServletContext的引用。

 

二、Spring和ServletContext的關係

 

緣何這兩貨會扯上關係呢?

 

在使用Spring的時候想必對如下程式碼肯定熟悉:

 

// 獲取Spring容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean.xml");

// 從Spring容器中根據id獲得物件的引用        
User user = (User) ctx.getBean("user");

// 呼叫物件的方法        
user.add();

 

這樣做是最低階的,也就是通過載入配置檔案來獲得Spring容器,再來獲取物件的應用,在Web專案中,每次都通過載入配置檔案顯得效率低下,而且繁瑣,這裡介紹一種另外的方法。想在Web專案啟動的時候就把Spring容器也給啟動了,不用每次都手動去啟動。

 

這裡就用到了上面介紹的ServletContext了,每次Web專案啟動的時候都會建立ServletContext物件,而該物件又有一個ServletContextListener的介面,監視ServletContext的建立,這樣就可以呼叫這個介面的回撥方法來啟動Spring容器了。(但是這裡我有個疑問,隨著專案啟動的不止有ServletContext啊,過濾器好像也隨著專案啟動,為啥不在過濾器的init()方法裡面啟動Spring容器呢?)

 

先來看看這個介面是啥定義:

 

package javax.servlet;

import java.util.EventListener;

/**
* Implementations of this interface receive notifications about changes to the
* servlet context of the web application they are part of. To receive
* notification events, the implementation class must be configured in the
* deployment descriptor for the web application.
*/

public interface ServletContextListener extends EventListener {

   /**
    ** Notification that the web application initialization process is starting.
    * All ServletContextListeners are notified of context initialization before
    * any filter or servlet in the web application is initialized.
    */
   public void contextInitialized(ServletContextEvent sce);

   /**
    ** Notification that the servlet context is about to be shut down. All
    * servlets and filters have been destroy()ed before any
    * ServletContextListeners are notified of context destruction.
    */
   public void contextDestroyed(ServletContextEvent sce);
}

 

第一段註釋描述的是:這個介面的實現接受和Web應用關聯的servlet context的變更的通知。為了接受通知事件,這個類的實現必須在web應用的部署描述符配置。

 

第二段註釋的描述是:通知是在Web應用初始化的時候開始的。所有的ServletContextListeners都會在web應用中任何的filter和servlet初始話之前接收到context初始化的時候通知。

 

第三段註釋的描述是:servlet context將要被關閉的時候的通知。所有的filter和servlet會在任何ServletContextListeners收到context銷燬的通知之前就被銷燬了。

 

另外再來看看ServeletContextEvent.java

 

package javax.servlet;

/**
* This is the event class for notifications about changes to the servlet
* context of a web application.
*
* @see ServletContextListener
* @since v 2.3
*/
public class ServletContextEvent extends java.util.EventObject {

   private static final long serialVersionUID = 1L;

   /**
    * Construct a ServletContextEvent from the given context.
    *
    * @param source
    *            - the ServletContext that is sending the event.
    */
   public ServletContextEvent(ServletContext source) {
       super(source);
   }

   /**
    * Return the ServletContext that changed.
    *
    * @return the ServletContext that sent the event.
    */
   public ServletContext getServletContext() {
       return (ServletContext) super.getSource();
   }
}

 

public ServletContextEvent(ServletContext source);

 

這個方法是從一個給定的ServletContext構建一個ServletContextEvent。而public ServletContext getServletContext();則是返回已經改變的ServletContext,暫時不知道有啥用,是不是給監聽器塞ServletContext用的啊?

 

想自己也寫一個ServletContextListener呢!

 

package com.servletContext.demo;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class MyServletContextListener implements ServletContextListener {

   @Override
   public void contextInitialized(ServletContextEvent sce) {

       // 從web.xml中拿出新增的引數
       ServletContext ctx = sce.getServletContext();
       String initParam = ctx.getInitParameter("myContextListener");
       System.out.println("我配置的初始化引數為:" + initParam);

       // 利用初始化引數找到配置檔案機型初始化
       System.out.println("context初始化了咯");
       System.out.println("這裡假裝初始化Spring容器.....");

   }

   @Override
   public void contextDestroyed(ServletContextEvent sce) {

       // 在銷燬之前獲得ServletContext
       ServletContext ctx = sce.getServletContext();

       // 正好剛剛存了一個值進去了,銷燬之前拿出來瞅瞅
       Integer count = (Integer) ctx.getAttribute("count");

       System.out.println("在銷燬之前,count的值為:" + count);

   }

}

 

這他喵的居然真的可以!

 

web.xml為:

 

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
 <display-name>ServletContext</display-name>
 <welcome-file-list>
   <welcome-file>index.html</welcome-file>
   <welcome-file>index.htm</welcome-file>
   <welcome-file>index.jsp</welcome-file>
   <welcome-file>default.html</welcome-file>
   <welcome-file>default.htm</welcome-file>
   <welcome-file>default.jsp</welcome-file>
 </welcome-file-list>

   <!-- 假裝為Spring監聽器提供啟動引數,其實是給ServletContext提供的 -->
   <context-param>
       <param-name>myContextListener</param-name>
       <!-- 這裡如果bean.xml在包cn.ssh下,那麼就應該寫為:cn/ssh/bean.xml -->
       <param-value>這是我設定的值</param-value>
   </context-param>

   <!-- 配置Spring的監聽器 -->
   <listener>
       <listener-class>com.servletContext.demo.MyServletContextListener</listener-class>
   </listener>

 <servlet>
   <servlet-name>count</servlet-name>
   <servlet-class>com.servletContext.demo.CounterServlet</servlet-class>
 </servlet>
 <servlet-mapping>
   <servlet-name>count</servlet-name>
   <url-pattern>/counter</url-pattern>
 </servlet-mapping>

</web-app>

 

測試結果為:

 

 

看來真的是可以了,這裡關閉伺服器的時候Console中的內容也被清除了,暫時沒有看到ServletContext銷燬時的訊息。

 

Spring提供的是ContextLoaderListener,這個監聽器實現了ServletContextListener介面,可以作為Listener使用,它會在建立的時候自動查詢WEB-INF/下的applicationContext.xml檔案,因此,如果只有一個配置檔案,並且檔名為applicationContext.xml,則只需要在web.xml中加入對Listener的配置就可以。

 

如果有多個配置檔案需要載入,則要考慮使用<context-param.../>元素來確定配置檔案的檔名。ContextLoaderListener載入的時候,會查詢名為contextConfigLocation的初始化引數。因此<context-param.../>時應該指定引數名為contextConfigLocation。

 

<!-- 為Spring監聽器提供啟動引數 -->
<context-param>
   <param-name>contextConfigLocation</param-name>
   <!-- 這裡如果bean.xml在包cn.ssh下,那麼就應該寫為:cn/ssh/bean.xml -->
   <param-value>classpath:bean.xml</param-value>
</context-param>

<!-- 配置Spring的監聽器 -->
<listener>
   <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

 

如果沒有使用contextConfigLocation指定配置檔案,則Spring自動查詢applicationContext.xml配置檔案;如果有contextConfigLocation,則利用該引數確定配置檔案。如果無法找到適合的配置檔案,Spring將無法初始化。

 

Spring根據指定的檔案建立WebApplicationContext物件,並將其儲存在Web應用的ServletContext中。大部分情況下,應用中的Bean無需感受到ApplicationContext的存在,只要用ApplicationContext中的IoC即可。

 

這個監聽器所在的jar包為:

 

 

如果需要利用ApplicationContext的例項,可以通過如下程式碼獲取:

 

package com.ssh.domain;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.struts2.ServletActionContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import com.opensymphony.xwork2.ActionSupport;
import com.ssh.test.TestAdd;

public class TestAction extends ActionSupport {

   @Override
   public String execute() throws Exception {

       HttpServletRequest request = ServletActionContext.getRequest();
       ServletContext servletContext = request.getServletContext();
       // 這裡不是通過依賴注入,而是直接從容器中拿
       WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

       // 也可以是下面這樣的
       WebApplicationContext ctx1 = (WebApplicationContext)
               servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

       if(ctx == ctx1) {
           System.out.println("兩次獲得物件是一樣的");
       }

       TestAdd testAdd = (TestAdd) ctx.getBean("testAdd");
       testAdd.add();

       return NONE;
   }
}

 

TestAdd.java

 

package com.ssh.test;

public class TestAdd {

   public void add( ) {
       System.out.println("通過WebContext獲得的而列印....");
   }
}

 

測試結果為:

http://localhost:8080/spring_struts2/testAction

 

 

開啟原始碼,就蛋疼了,有封裝了一下:

 

package org.springframework.web.context;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
* Bootstrap listener to start up and shut down Spring's root {@link WebApplicationContext}.
* Simply delegates to {@link ContextLoader} as well as to {@link ContextCleanupListener}.
*
* <p>This listener should be registered after {@link org.springframework.web.util.Log4jConfigListener}
* in {@code web.xml}, if the latter is used.
*
* <p>As of Spring 3.1, {@code ContextLoaderListener} supports injecting the root web
* application context via the {@link #ContextLoaderListener(WebApplicationContext)}
* constructor, allowing for programmatic configuration in Servlet 3.0+ environments.
* See {@link org.springframework.web.WebApplicationInitializer} for usage examples.
*
* @author Juergen Hoeller
* @author Chris Beams
* @since 17.02.2003
* @see #setContextInitializers
* @see org.springframework.web.WebApplicationInitializer
* @see org.springframework.web.util.Log4jConfigListener
*/
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

   /**
    * Create a new {@code ContextLoaderListener} that will create a web application
    * context based on the "contextClass" and "contextConfigLocation" servlet
    * context-params. See {@link ContextLoader} superclass documentation for details on
    * default values for each.
    * <p>This constructor is typically used when declaring {@code ContextLoaderListener}
    * as a {@code <listener>} within {@code web.xml}, where a no-arg constructor is
    * required.
    * <p>The created application context will be registered into the ServletContext under
    * the attribute name {@link WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE}
    * and the Spring application context will be closed when the {@link #contextDestroyed}
    * lifecycle method is invoked on this listener.
    * @see ContextLoader
    * @see #ContextLoaderListener(WebApplicationContext)
    * @see #contextInitialized(ServletContextEvent)
    * @see #contextDestroyed(ServletContextEvent)
    */
   public ContextLoaderListener() {
   }

   /**
    * Create a new {@code ContextLoaderListener} with the given application context. This
    * constructor is useful in Servlet 3.0+ environments where instance-based
    * registration of listeners is possible through the {@link javax.servlet.ServletContext#addListener}
    * API.
    * <p>The context may or may not yet be {@linkplain
    * org.springframework.context.ConfigurableApplicationContext#refresh() refreshed}. If it
    * (a) is an implementation of {@link ConfigurableWebApplicationContext} and
    * (b) has <strong>not</strong> already been refreshed (the recommended approach),
    * then the following will occur:
    * <ul>
    * <li>If the given context has not already been assigned an {@linkplain
    * org.springframework.context.ConfigurableApplicationContext#setId id}, one will be assigned to it</li>
    * <li>{@code ServletContext} and {@code ServletConfig} objects will be delegated to
    * the application context</li>
    * <li>{@link #customizeContext} will be called</li>
    * <li>Any {@link org.springframework.context.ApplicationContextInitializer ApplicationContextInitializer}s
    * specified through the "contextInitializerClasses" init-param will be applied.</li>
    * <li>{@link org.springframework.context.ConfigurableApplicationContext#refresh refresh()} will be called</li>
    * </ul>
    * If the context has already been refreshed or does not implement
    * {@code ConfigurableWebApplicationContext}, none of the above will occur under the
    * assumption that the user has performed these actions (or not) per his or her
    * specific needs.
    * <p>See {@link org.springframework.web.WebApplicationInitializer} for usage examples.
    * <p>In any case, the given application context will be registered into the
    * ServletContext under the attribute name {@link
    * WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE} and the Spring
    * application context will be closed when the {@link #contextDestroyed} lifecycle
    * method is invoked on this listener.
    * @param context the application context to manage
    * @see #contextInitialized(ServletContextEvent)
    * @see #contextDestroyed(ServletContextEvent)
    */
   public ContextLoaderListener(WebApplicationContext context) {
       super(context);
   }


   /**
    * Initialize the root web application context.
    */
   @Override
   public void contextInitialized(ServletContextEvent event) {
       initWebApplicationContext(event.getServletContext());
   }


   /**
    * Close the root web application context.
    */
   @Override
   public void contextDestroyed(ServletContextEvent event) {
       closeWebApplicationContext(event.getServletContext());
       ContextCleanupListener.cleanupAttributes(event.getServletContext());
   }

}

 

原始碼果然是個好東西,平時敲程式碼那會注意到這麼多細節。這個類不復雜,兩個構造方法,外加一個初始化的時候建立Spring容器和服務關閉的時候對容器的清理,封裝了之後還要看其他的類,哎。

 

首先第一段註釋是對這個類的描述:

 

這個啟動監聽器是用開啟和關閉Spring的root的,這裡他用了root而不是容器。簡單的代理給了ContextLoader和ContextCleanupListener這兩個類來處理。如果這個org.springframework.web.util.Log4jConfigListener被用到了,那麼ContextLoaderListener應該在它之後註冊。

 

在Spring3.1中,ContextLoaderListener支援通過ContextLoaderListener(WebApplicationContext)這個構造方法嚮應用上下文中注入root(也就是Spring的容器),這樣可以以程式設計的方式來配置Servlet 3.0+的環境。

 

第二段註釋是,新建一個ContextLoaderListener的類將會基於Servlet的"contextClass"和"contextCofigLocation"這兩個引數來建立web應用的上下文。

 

翻譯的好累啊,反正意思差不多就是這樣5555....

 

來看這段程式碼:

 

/**
* Initialize the root web application context.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
   initWebApplicationContext(event.getServletContext());
}

 

這個initWebApplicationContext方法是ContextLoad.java這個類裡面的方法。

 

/**
* Initialize Spring's web application context for the given servlet context,
* using the application context provided at construction time, or creating a new one
* according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
* "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
* @param servletContext current servlet context
* @return the new WebApplicationContext
* @see #ContextLoader(WebApplicationContext)
* @see #CONTEXT_CLASS_PARAM
* @see #CONFIG_LOCATION_PARAM
*/
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;
   }
}

 

ContextLoad.initWebApplicationContext是為給定的servlet context來初始化web應用的上下文的。

 

業務邏輯解讀:

 

首先從ServletContext中看看有沒有Spring建立的這個容器;然後為ContextLoader存一份例項變數,使得在ServletContext關閉之後仍可以訪問;

 

this.context = createWebApplicationContext(servletContext);

 

這句就是建立一個WebApplicationContext相當於我們自己載入配置檔案的那個類。

 

configureAndRefreshWebApplicationContext(cwac, servletContext);

 

這句話也很明顯,就是配置並且重新整理WebAppCtx的。

 

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

 

這句將建立的Spring的context作為屬性放到servletContext中。

 

return this.context;

 

然後就返回了Spring的容器了.....是不是簡潔(裝逼裝不下去了),呼叫鏈好長。

 

暫時只能分析到這裡!