上文分析了 IOC 容器本身實作,本文探討典型 Web 環境中 Spring IOC 容器如何載入與發揮作用。
在 Web 容器中,透過 ServletContext 為 Spring IOC 容器提供宿主環境,建立起一個 IOC 容器體系。首先建立根上下文,持有業務物件、資料存取物件、資源、事務管理器等中間層物件。在此基礎上,與 Web MVC 相關會有另一上下文保存控制器等 MVC 物件,構成層次化上下文結構。Web 容器啟動 Spring 應用程式即建立此上下文體系的過程。
Spring 提供 Web 應用上下文擴充介面 WebApplicationContext:
public interface WebApplicationContext extends ApplicationContext {
String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
ServletContext getServletContext();
}
一般啟動過程使用預設實作 XmlWebApplicationContext 作為根上下文容器建立。其 loadBeanDefinitions 使用 XmlBeanDefinitionReader 解析 Bean 定義,預設配置位置為 /WEB-INF/applicationContext.xml。
Spring 提供 ContextLoader 類別,可宣告式指定 Web 應用啟動時載入 WebApplicationContext。使用 ContextLoaderServlet 或 ContextLoaderListener 實例化 Spring IOC 容器——兩者區別在於支援的 Servlet 版本不同,但皆使用 ContextLoader 完成實際初始化工作。ContextLoader 如同 Spring Web 應用在 Web 容器中的啟動器。具體使用需在 web.xml 部署描述符中定義。
此 Servlet 監聽器是根上下文載入處,也是整個 Spring Web 應用載入上下文的第一個地方。以 Tomcat 為例:
web.xml 啟動參數,配置 ContextLoaderListener。Web 容器中維護一系列 IOC 容器,ContextLoader 載入的作為根上下文存在於 ServletContext 中。contextInitialized 方法:
public void contextInitialized(ServletContextEvent event) {
this.contextLoader = createContextLoader();
this.contextLoader.initWebApplicationContext(event.getServletContext());
}
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 檢查是否已存在根上下文
if (servletContext.getAttribute(ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(...);
}
try {
// 載入根上下文的父上下文
ApplicationContext parent = loadParentContext(servletContext);
// 建立根上下文並存入 ServletContext
this.context = createWebApplicationContext(servletContext, parent);
servletContext.setAttribute(ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
return this.context;
} catch (...) { ... }
}
取決於 web.xml 定義的 locatorFactorySelector 參數(可選):
protected ApplicationContext loadParentContext(ServletContext servletContext) {
String locatorFactorySelector = servletContext.getInitParameter(LOCATOR_FACTORY_SELECTOR_PARAM);
String parentContextKey = servletContext.getInitParameter(LOCATOR_FACTORY_KEY_PARAM);
if (locatorFactorySelector != null) {
BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance(locatorFactorySelector);
this.parentContextRef = locator.useBeanFactory(parentContextKey);
return (ApplicationContext) this.parentContextRef.getFactory();
}
return null;
}
protected WebApplicationContext createWebApplicationContext(ServletContext servletContext, ApplicationContext parent) {
Class contextClass = determineContextClass(servletContext); // 由 web.xml contextClass 參數決定,預設 XmlWebApplicationContext
ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setParent(parent);
wac.setServletContext(servletContext);
String configLocation = servletContext.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocation != null) {
wac.setConfigLocations(StringUtils.tokenizeToStringArray(configLocation, CONFIG_LOCATION_DELIMITERS));
}
wac.refresh(); // 熟悉的 refresh 調用
return wac;
}
初始化根 ApplicationContext 並存入 ServletContext 後,建立全域應用上下文。此根上下文稍後會被 DispatcherServlet 初始化時作為自身 ApplicationContext 的父上下文。
完成 ContextLoaderListener 初始化後,Tomcat 開始初始化 DispatcherServlet(依 web.xml 載入順序)。DispatcherServlet 建立自身 ApplicationContext,從 ServletContext 取得根上下文作為父上下文,初始化後存入 ServletContext 供後續檢索使用。
從父類 FrameworkServlet 可見初始化流程:
protected final void initServletBean() {
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet(); // 根據 Bean 配置建立 MVC 框架主要元素
} catch (...) { ... }
}
protected WebApplicationContext initWebApplicationContext() {
// 取得根上下文
WebApplicationContext parent = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
// 建立 DispatcherServlet 自身上下文,預設 XmlWebApplicationContext
WebApplicationContext wac = createWebApplicationContext(parent);
if (isPublishContext()) {
String attrName = getServletContextAttributeName(); // 屬性名與 Servlet 名稱相關
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
WebApplicationContextUtils.getWebApplicationContext 直接從 ServletContext 透過屬性名取得根上下文。createWebApplicationContext 設定父上下文、ServletContext、ServletConfig、命名空間、配置檔位置,最後呼叫 refresh() 初始化。
initFrameworkServlet 初始化 MVC 核心組件:
protected void initFrameworkServlet() {
initMultipartResolver();
initLocaleResolver();
initThemeResolver();
initHandlerMappings();
initHandlerAdapters();
initHandlerExceptionResolvers();
initRequestToViewNameTranslator();
initViewResolvers();
}
MVC 框架建立完成後,DispatcherServlet 對接收到的 HTTP Request 進行分發處理,由 doService() 完成,具體 MVC 處理在 doDispatch():
protected void doService(HttpServletRequest request, HttpServletResponse response) {
try { doDispatch(request, response); } catch (...) { ... }
}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
HandlerExecutionChain mappedHandler = null;
try {
ModelAndView mv = null;
processedRequest = checkMultipart(request);
mappedHandler = getHandler(processedRequest, false); // 依請求參數與映射關係決定 Handler
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // Command 模式執行
if (mv != null && !mv.wasCleared()) {
render(mv, processedRequest, response); // 透過視圖展現模型資料
}
} catch (...) { ... }
}
具體 MVC 實現由 Bean 配置檔定義的 ViewResolver、Handler 等類別實現使用者程式碼功能。
Web 容器中 ServletContext 可持有多個 Web 上下文,存在一個根上下文作為其他 Servlet 上下文的父上下文。根上下文由 ContextLoader 載入初始化。DispatcherServlet 載入初始化自身上下文,父上下文為根上下文。可從 ServletContext 根據 Servlet 名稱檢索對應上下文,根上下文名稱由 Spring 唯一確定。此 DispatcherServlet 建立的上下文即開發 Spring MVC 應用的 IOC 容器。具體 Web 請求處理在上下文體系建立完成後由 DispatcherServlet 完成。下文將詳細分析 Spring MVC 框架實作。