FrameworkServlet初始化

FrameworkServlet是Spring web框架的基本servlet實現類,通過JavaBean的方式集成了Application context,所有新實現的servlet最好都繼承於該類。該類提供了HttpServlet的所有接口實現,自帶了一個web容器,它實現了WebApplicationContextAware接口,所以能根據指定的容器配置文件,來初始化自己管理的容器。
FrameworkServlet提供兩個功能:

  1. 爲每個servlet管理一個WebApplicationContext實例(即每個servlet會自己的一個web容器),每個servlet都有自己的配置空間,相互獨立。(每一個<servlet> tag之間的配置屬於一個namespace, 配置一個application context。)
  2. 對每一個請求,無論是否成功處理,都會在每個請求上發佈事件。(這裏是不是可以做一些事情?)

FrameworkServlet初始化流程

FrameworkServlet初始化的過程其實就是WebApplication 配置文件加載(即容器初始化)的過程。

入口方法是initServletBean(),這個方法比較簡單,就是打印一些初始化必要信息,然後調用initWebApplicationContext()方法來初始化配置。
實現代碼如下:

	protected final void initServletBean() throws ServletException {
		getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
		if (logger.isInfoEnabled()) {
			logger.info("Initializing Servlet '" + getServletName() + "'");
		}
		long startTime = System.currentTimeMillis();

		try {
			this.webApplicationContext = initWebApplicationContext();   // 這個是真正初始化的方法
			initFrameworkServlet();    // 初始化完成後再做一些其他初始化
		}
		catch (ServletException | RuntimeException ex) {
			logger.error("Context initialization failed", ex);
			throw ex;
		}

		if (logger.isDebugEnabled()) {
			String value = this.enableLoggingRequestDetails ?
					"shown which may lead to unsafe logging of potentially sensitive data" :
					"masked to prevent unsafe logging of potentially sensitive data";
			logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
					"': request parameters and headers will be " + value);
		}

		if (logger.isInfoEnabled()) {
		    // 當容器初始化完畢後,我們會在日誌文件裏看到這一行日誌信息
			logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
		}
	}

initWebApplicationContext()方法在初始化applicationContext時,會分幾步來執行:

  1. 獲取root applicationContext, 一般是web.xml文件對應的初始化配置。
  2. 如果當前servlet已經初始化了一個applicationContext, 那麼設置parent applicationContext爲第一步設置的context, 然後讀取配置文件。
  3. 如果applicationContext爲空,那麼根據配置文件中設置的contextAttribute值從ServletContext中查找;
  4. 如果沒有設置contextAttribute,那麼就調用createWebApplicationContext()方法創建一個新的applicationContext,並將root applicationContext作爲父容器。

實現代碼如下:

	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.
			synchronized (this.onRefreshMonitor) {
				onRefresh(wac);
			}
		}

		if (this.publishContext) {
			// Publish the context as a servlet context attribute.
			String attrName = getServletContextAttributeName();
			getServletContext().setAttribute(attrName, wac);
		}

		return wac;
	}

createWebApplicationContext()方法是最核心的代碼,創建XmlWebApplicationContext實例,並從xml配置文件中加載配置。

	protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
		Class<?> contextClass = getContextClass();
		if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
			throw new ApplicationContextException(
					"Fatal initialization error in servlet with name '" + getServletName() +
					"': custom WebApplicationContext class [" + contextClass.getName() +
					"] is not of type ConfigurableWebApplicationContext");
		}
		// 真正初始化實現類,這個類就是通過contextClass來指定的。
		ConfigurableWebApplicationContext wac =
				(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

		wac.setEnvironment(getEnvironment());
		wac.setParent(parent);
		// 如果有配置文件的路徑,那麼就設置路徑
		String configLocation = getContextConfigLocation();
		if (configLocation != null) {
			wac.setConfigLocation(configLocation);
		}
		// 真正加載配置的地方
		configureAndRefreshWebApplicationContext(wac);

		return wac;
	}

我們來看看核心的代碼,它會設置各種屬性參數,最後會調用refresh()方法,讀取配置。

	protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
	    // 設置contextId屬性
		if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
			// The application context id is still set to its original default value
			// -> assign a more useful id based on available information
			if (this.contextId != null) {
				wac.setId(this.contextId);
			}
			else {
				// Generate default id...
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
			}
		}

	    // 設置當前webApplicationContext所屬於的ServletContext
		wac.setServletContext(getServletContext());
		// 設置ServletConfig
		wac.setServletConfig(getServletConfig());
		// 設置命名空間
		wac.setNamespace(getNamespace());
	    // 設置監聽器
		wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

		// The wac environment's #initPropertySources will be called in any case when the context
		// is refreshed; do it eagerly here to ensure servlet property sources are in place for
		// use in any post-processing or initialization that occurs below prior to #refresh
		ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment) {
			((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
		}

		// 這是一個hook, 子類可以實現該方法
		postProcessWebApplicationContext(wac);
		applyInitializers(wac);
		// 最終配置加載的地方
		wac.refresh();
	}

爲什麼FrameworkServlet的初始化入口是initServletBean()方法?

這就需要從FrameworkServlet的繼承關係來講解了。首先,必須明確的是,FrameworkServlet也是一個普普通通的servlet, 所以它自身也會有servlet定義的方法的實現:

public void init(ServletConfig config) throws ServletException;
public ServletConfig getServletConfig();
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
public String getServletInfo();
public void destroy();

下圖是類繼承關係鏈:
在這裏插入圖片描述
根據上圖繼承關係,我們可以理解初始化順序實際上是這樣的:

  1. web容器在實例化servlet時會調用Servlet的init(ServletConfig)方法,由於其子類GenericServlet實現了該方法,因而最終會調用GenericServlet.init(ServletConfig)方法。
  2. GenericServlet.init(ServletConfig)方法內部會調用其init()方法,由於該方法又是由其子類HttpServletBean實現,實際上是執行了HttpServletBean.init()方法。
  3. HttpServletBean.init()方法內部會調用其自定義的initServletBean()方法,但該方法實際上是由FrameworkServlet實現,實際上是執行了FrameworkServlet.initServletBean()方法。
  4. FrameworkServlet.initServletBean()方法內部真正初始化了applicationContext,並加載配置文件,最終調用其自定義的initFrameworkServlet()方法。

至此,當容器初始化該servlet時,通過調用init()方法,最終達到了初始化該servlet的容器的目的。
其實Servlet定義的接口很簡單,子類通過不斷繼承,利用Java的繼承和多態特性,完成了很多自定義的邏輯實現。簡單的接口,完成了重要的任務。真是佩服至極!!!

如何使用FrameworkServlet類

從上面的代碼分析,我們可以看到,其實只需要實現FrameworkServlet中的幾個方法即可:

  • initFrameworkServlet()方法
  • doService()方法
  • postProcessWebApplicationContext()方法

其中,子類必須實現FrameworkServlet.doService()方法,該方法用來處理所有的請求。另外兩個方法可以不必實現。具體實現可以參考DispatcherSerlvet的實現。

Servlet init-param有哪些?分別是什麼作用?

  • contextConfigLocation
  • contextClass
  • contextInitializerClasses
  • contextAttribute
  • namespace
  • publishContext

其實還有一些其他屬性,採用默認值就行,在此就j不一一羅列了。
contextConfigLocation屬性用來告訴FrameworkServlet容器配置的完整路徑,該路徑是基於WEB-INF/目錄。但是如果不指定該屬性,默認的路徑是什麼呢?

  1. 首先看當前servlet有沒有定義namespace屬性,如果有則用namespace.xml作爲文件名。
  2. 如果沒有,則用<servlet-name>-servlet作爲namespace,比如namespace是spring-mvc-test-servlet。
  3. 那麼完整的配置文件名就是spring-mvc-test-servlet.xml.完整路徑就是/WEB-INF/spring-mvc-test-servlet.xml.

contextConfigLocation支持同時指定多個配置文件,文件之間用逗號或空格隔開。如果多個文件中定義有相同的bean, 後面的會覆蓋前面的定義。如果想覆蓋前面文件定義的bean, 這是一個方法。

contextClass用來制定用哪個實現類來加載這個容器資源,這些容器管理類的基類是ConfigurableWebApplicationContext, 子類必須繼承實現它。通常使用比較多的是XmlWebApplicationContext,這也是spring mvc默認的實現類。

contextInitializerClasses用來指定多個ApplicationContextInitializer類,初始化這個容器,對其中的屬性會做一些加工。

namespace就是當前servlet的命名空間,用來標明容器的配置文件名稱和配置屬性所在的空間範圍。

contextAttribute用來標明當前的ApplicationContext在ServletContext中的名稱,因爲ServletContext中維護的對象實際上存在了Map<String, Object>的對象裏,所以必須知道對應的是哪個key, 這樣就可以通過servletContext.get()或servletContext.set()獲取會設置該容器了。該值是SERVLET_CONTEXT_PREFIX + servlet-name兩部分拼接而成,完整名是:org.springframework.web.servlet.FrameworkServlet.CONTEXT.servlet-name。

publishContext用來在初始化時告訴FrameworkServlet是否將applicationContext放到ServletContext裏,以便後續可以在其他地方訪問。

看源碼我們的印象會更深一些。

	/**
	 * Suffix for WebApplicationContext namespaces. If a servlet of this class is
	 * given the name "test" in a context, the namespace used by the servlet will
	 * resolve to "test-servlet".
	 */
	public static final String DEFAULT_NAMESPACE_SUFFIX = "-servlet";  // 如果沒有顯式地指定namespace, 那麼namespace是以-sevlet做後綴。

	/**
	 * Default context class for FrameworkServlet.
	 * @see org.springframework.web.context.support.XmlWebApplicationContext
	 */
	public static final Class<?> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class; // 缺省的容器配置類

	/**
	 * Prefix for the ServletContext attribute for the WebApplicationContext.
	 * The completion is the servlet name.
	 */
	public static final String SERVLET_CONTEXT_PREFIX = FrameworkServlet.class.getName() + ".CONTEXT."; // WebpplicationContext在ServletContext中的屬性名。

	/**
	 * Any number of these characters are considered delimiters between
	 * multiple values in a single init-param String value.
	 */
	private static final String INIT_PARAM_DELIMITERS = ",; \t\n"; // 多個值的分隔符


	/** ServletContext attribute to find the WebApplicationContext in. */
	@Nullable
	private String contextAttribute;

	/** WebApplicationContext implementation class to create. */
	private Class<?> contextClass = DEFAULT_CONTEXT_CLASS;  // 缺省的WebApplicationContext實現類

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章