源碼閱讀從最熟悉的SpringMVC開始--ContextLoaderListener的初始化

剛入手SpringMVC時,感覺很厲害,初學web開發時,我寫一個簡單web程序,要繼承HttpServlet重寫其init、doGet、doPost等方法,還要在web.xml文件中配置servlet-mapping,而當使用springMVC時就大大簡化我們的我們的開發,項目只要用一個註解或者簡單配置一下就可以。
雖然一直用它,卻從未仔細的閱讀過它的源碼,所以就打算認認真真的看下它的源碼,下文就開始SpringMVC源碼的閱讀和學習。

初體驗

程序每門語言都是"hello world"開始,SpringMVC源碼的學習也免不了俗套,首先來一個簡單的Demo。

1.首先配置web項目的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="springMvc" version="2.5">
    <display-name>spring mvc</display-name>
    <!--使用ContextLoaderListener配置時,需要告訴它Spring配置文件的位置-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <!--spring mvc的前端控制器-->
    <!--當DispatcherServlet載入後,它將從一個XML文件中載入Spring的應用七下文,XML文件的名字取決於<servletname>-->
    <!--這裏DispatcherServlet將試圖從一個叫作spring-mvc-servlet.xml的文件中載入應用下文,其默認位於WEBINF目錄下-->
    <servlet>
        <servlet-name>spring-mvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--也可以手動配置文件名稱如下-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring-mvc</servlet-name>
        <url-pattern>*.htm</url-pattern>
    </servlet-mapping>
    <!--配置上下文在載入器-->
    <!--上下文載入器除dispatcherServlet載入的配置文件之外-->
    <!--最常用的上下文載入器是一個servlet監聽器,其名稱爲ContextLoaderLister-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

創建spring配置文件applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

創建servlet配置文件spring-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="simpleUrlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/userlist.htm">userController</prop>
            </props>
        </property>
    </bean>
    <!--這裏的id=”userController”對應的是<bean id=”simpleUrlMapping">中的<prop>裏面的value-->
    <bean id="userController" class="UserController"/>
</beans>

創建model

public class User {
    private String userName;
    private Integer age;
    public User() {
        // do nothing
    }
    public User(String userName, Integer age) {
        this.userName = userName;
        this.age = age;
    }
    // 省略 setter getter
}

創建controller

控制器執行方法都必須返回一個ModelAndView,ModelAndView對象保存了視圖以及視圖顯示的模型數據。

public class UserController extends AbstractController {

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {

        List<User> userList = new ArrayList<>(3);
        userList.add(new User("tony", 18));
        userList.add(new User("mike", 38));
        userList.add(new User("liao", 28));
        return new ModelAndView("userlist", "users", userList);
    }
}

創建視圖文件userlist.jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>spring mvc</title>
</head>
<body>
<c:forEach items="${users}" var="u">
    <c:out value="${u.userName}"/>|| <c:out value="${u.age}"/> <br/>
</c:forEach>
</body>
</html>

至此,已經完成了SpringMVC的搭建,啓動服務器,輸入網址http://localhost/userlist測試結果如下:
在這裏插入圖片描述
SpringMVC或者其他比較成熟的MVC框架而言,解決的問題無外乎以下幾點。

  • 將Web頁面的請求傳給服務器。
  • 根據不同的請求處理不同的邏輯單元。
  • 返回處理結果數據並跳轉至響應的頁面。

下面就從源碼入手剖析SpringMVC如何是解決該問題的。

ContextLoaderListener的初始化

在上述的web.xml文件中,我們可以很明顯看到SpringMVC的類:

  • 1、“org.springframework.web.servlet.DispatcherServlet”
  • 2、“org.springframework.web.context.ContextLoaderListener”
    我們跟進源碼,使用ide工具可以看到如下類圖的。
    在這裏插入圖片描述

在這裏插入圖片描述
有點懵圈,我們可以回想一下我們在使用spring時,如何將參數傳給spring容器的?
我們編程時可以用下面的方式,將參數放在applicationContext.xml中從而實現參數的解析。

ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

但是在web項目就無法用上述方式實現了。我們就需要考慮與web結合,通常的辦法是將路徑以context-param的方式註冊並使用ContextLoaderListener進行監聽讀取。ContextLoaderListener的作用就是啓動Web容器時,自動裝配ApplicationContext的配置信息。因爲它實現了ServletContextListener這個接口,在web.xml配置這個監昕器,啓動容器時,就會默認執行它實現的方法,使用ServletContextListener接口,開發者能夠在爲客戶端請求提供服務之前向ServletContext中添加任的對象。個對象在ServletContext啓動的時候被初始化,然後在ServletContext整個運行期間都是可見的。

ServletContextListener的使用

正式分析代碼前我們同樣還是首先了解ServletContextListener的使用。

1.創建自定義ServletContextlistener

首先我們創建SevrvletContextListener,目標是在系統啓動時添加自定義的屬性,以便於在全局範圍內可以隨時調用。系統啓動的時候會調用ServletContextListener實現類的contextInitialized方法,所以需要在這個方法中實現我們的初始化邏輯。

public class MyContextLoaderListener implements ServletContextListener {
    private ServletContext context = null;
    /**
     * 該方法在ServletContext啓動之後被調用,並準備好處理客戶端請求
     */
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.println("程序啓動將會執行");
        context = servletContextEvent.getServletContext();
        // 通過實現自己的邏輯並將結果記錄在屬性中
        context.setAttribute("myData", "this is myContext");
    }
    /**
     * 這個方法在ServletContext將要關閉的時候用
     */
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        context = null;
    }
}

2.註冊監聽器

在web.xml文件中需要註冊自定義的監聽。

 <listener>
        <listener-class>MyContextLoaderListener</listener-class>
 </listener>

3.測試

一旦Web應用啓動的時候,可以看到啓動日誌中輸出“程序啓動將會執行”

在這裏插入圖片描述

我們就能在任意的Servlet或者JSP中通過下面的方獲取我們初始化的參數,如下:

myListener.jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<c:out value="${myData}"/>
</body>
</html>

瀏覽器訪問,查看結果
在這裏插入圖片描述

Spring中的ContextloaderListener

分析了ServletContextListener的使用方式後再來分析Spring中的ContextLoaderListener的實現就容易理解得多,雖然ContextLoaderListener實現的邏輯要複雜得多,但是大致的套路還是萬變不離其宗。

ServletContext啓動之後會調用ServletContextListener的contextlnitialized方法,那麼,我們就從這個函數開始進行分析。

	@Override
	public void contextInitialized(ServletContextEvent event) {
        // 該處調用了父類contextLoader的方法
        // 初始化WebApplicationContext
		initWebApplicationContext(event.getServletContext());
	}

這裏涉及了一個常用類WebApplicationContext:Web應用中,我們會用到WebApplicationContext,WebApplicationContext繼承自ApplicationContext,在ApplicationContext的基礎上又追加一些特定於Web的操作及屬性,非常類於我們通過編程方式使用Spring時使用的ClassPathXmlApplicationContext類提供的功能。繼續跟蹤代碼:

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    	// web.xml中存在多次ContextLoader定義
		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) {
                //初始化context
				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中
            // String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
			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;
		}
	}

initWebApplicationContext函數主要是體現了創建WebApplicationContext實例的一個功能架構,從函數中我們到了初始化的大致步驟。

1.WebApplicationContext存在性的驗證。

在配置中只允許明一次ServletContextListener,多次聲明會擾亂Spring的執行邏輯,所以這裏首先做的就是對此驗證,在Spring中如果創建WebApplicationContext實例會記錄在ServletContext中以方便全局調用,而使用的key就是WebApplicationContext.ROOTWEB_APPLICATIONCONTEXTATTRIBUTE,所以驗證的方式就是查看ServIetContext實例中是否對應key的屬性。

2.創建WebApplicationContext實例。

如果通過驗證,則Spring將創建WebApplicationContext實例的工作委託給了createWebApplicationContext函數。

	protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
        // 跟蹤determineContextClass->代碼1
		Class<?> contextClass = determineContextClass(sc);
		if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
			throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
					"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
		}
		return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
	}

	// 繼續跟蹤-->代碼1.
	protected Class<?> determineContextClass(ServletContext servletContext) {
        // String CONTEXT_CLASS_PARAM = "contextClass";
		String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
		if (contextClassName != null) {
			try {
				return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
			}
			catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load custom context class [" + contextClassName + "]", ex);
			}
		}
		else {
            // 屬性文件 contextLoader.properties 繼續跟蹤代碼->代碼2
			contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
			try {
				return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
			}
			catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load default context class [" + contextClassName + "]", ex);
			}
		}
	}
    // 繼續跟蹤-->代碼2
	String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
	private static final Properties defaultStrategies;

	static {
		// Load default strategy implementations from properties file.
		// This is currently strictly internal and not meant to be customized
		// by application developers.
		try {
			ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
			defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
		}
		catch (IOException ex) {
			throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
		}
	}

根據以上靜態代碼塊的內容,我們推斷在當前類ContextLoader同樣錄下必定會存在屬性文件ContextLoader.properties,查看後果然存在,內容如下:

org.springframework.web.context.WebApplicationContext = org.springframework.web.context.support.XmlWebApplicationContext

綜合以上代碼分析,在初始化的過程中,程序首先會讀取ContextLoader的同目錄下的屬性文件ContextLoader.properties,並根據其中的配置提取將要實現WebApplicationContext口的實現類,並根據這個實現類通過反射的方式進行實例的創建。

3.將實例記錄在servletContext中。

4.映射當前的類加載器與創建的實例到全局變量currentContextPerThread中。

在Spring中,ContextLoaderListener只是助功能,用於創建WebApplicationContext類型實例,而真正的邏輯實現實是在DispatcherServlet中進行的,DispatcherServlet是實現servlet接口的實現類。後面將繼續記錄DispatcherServlet的源碼剖析。

發佈了7 篇原創文章 · 獲贊 0 · 訪問量 420
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章