剛入手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的源碼剖析。