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的容器了.....是不是簡潔(裝逼裝不下去了),呼叫鏈好長。
暫時只能分析到這裡!