1. 程式人生 > >Spring原始碼深度解析之Spring MVC

Spring原始碼深度解析之Spring MVC

Spring原始碼深度解析之Spring MVC

       Spring框架提供了構建Web應用程式的全功能MVC模組。通過策略介面,Spring框架是高度可配置的,而且支援多種檢視技術,例如JavaServer pages(JSP)技術、Velocity、Tiles、iText和POI。Spring MVC框架並不知道使用的檢視,所以不會強迫您只使用JSP技術。Spring MVC分離了控制器、模型物件、分派器以及處理程式物件的角色,這種分離讓它們更容易進行定製。

       Spring的MVC是基於Servlet功能實現的。通過實現Servlet介面的DispatcherServlet來封裝其核心功能實現,通過將請求分派給處理程式,同時帶有可配置的處理程式對映、檢視解析、本地語言、主題解析以及上載檔案的支援。預設的處理程式是非常簡單的Controller介面,只有一個方法ModelAndView handleRequest(request, response)。Spring提供了一個控制器層次結構,可以派生子類。如果應用程式需要處理使用者輸入表單,那麼可以繼承AbstractFormController。如果需要把多頁輸入處理到一個表單,那麼可以繼承AbstractWizardFormController。

       Spring MVC或者其他比較成熟的MVC框架而言,解決的問題無外乎以下幾點:

  (1)將Web頁面的請求傳給伺服器。

  (2)根據不同的請求處理不同的邏輯單元。

  (3)返回處理結果資料並跳轉到響應的頁面。

  我們先通過一個簡單示例來快速回顧Spring MVC的使用。

 

一、 Spring MVC快速體驗

(一)配置web.xml

       一個Web中可以沒有web.xml檔案,也就是說,web.xml檔案並不是Web工廠必須的。web.xml檔案用來初始化配置資訊:比如Welcome頁面、servlet、servlet-mapping、filter、listener、啟動載入級別等。但是,Spring MVC的實現原理就是通過Servlet攔截所有URL來達到控制的目的,所以web.xml的配置是必須的。

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <web-app id="WebApp_ID" version="2.5" xmlns="http://java.sun.com/xml/ns/j2ee"
 3          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4          xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
 5     <display-name>Springmvc</display-name>
 6 
 7     <!--使用ContextLoaderListener配置時,需要告訴他Spring配置檔案的位置-->
 8     <context-param>
 9         <param-name>contextConfigLocation</param-name>
10         <param-value>classpath:applicationContext.xml</param-value>
11     </context-param>
12 
13     <!--Spring MVC的前端控制器-->
14     <!--當DispatcherServlet載入後,它將從一個XML檔案中載入Spring的應用上下文,該XMl檔案的名字取決於<servlet-name>-->
15     <!--這裡DispatcherServlet將試圖從一個叫做Springmvc-servlet.xml的檔案中載入應用上下文,其預設位於WEB-INF目錄下-->
16     <servlet>
17         <servlet-name>Springmvc</servlet-name>
18         <servlet-class>org.Springframework.web.servlet.DispatcherServlet</servlet-class>
19         <load-on-startup>1</load-on-startup>
20     </servlet>
21     <servlet-mapping>
22         <servlet-name>Springmvc</servlet-name>
23         <url-pattern>*.htm</url-pattern>
24     </servlet-mapping>
25 
26     <!--配置上下文載入器-->
27     <!--上下文載入器載入除DispatcherServlet載入的配置檔案之外的其它上下文配置檔案-->
28     <!--最常用的上下文載入器是一個Servlet監聽器,其名稱為ContextLoaderListener-->
29     <listener>
30         <listener-class>org.Springframework.web.context.ContextLoaderListener</listener-class>
31     </listener>
32 
33 </web-app>

  Spring的MVC之所以必須要配置web.xml,其實最關鍵的是要配置兩個地方。

  (1)contextConfigLocation:Spring的核心就是配置檔案,可以說配置檔案是Spring中必不可少的東西,而這個引數就是使Web與Spring的配置檔案相結合的一個關鍵配置。

  (2)DispatcherServlet:包含了SpringMVC的請求邏輯,Spring使用此類攔截Web請求並進行相應的邏輯處理。

(二)建立Spring配置檔案applicationContext.xml

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmls:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4        xmlns:tx="http://www.springframework.org/schema/tx"
 5        xsi:schemaLocation="http://www.springframework.org/schema/beans
 6                http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
 7                http://www.springframework.org/schema/tx
 8                http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
 9 
10     <bean id="viewResolver" class="org.springframework.web.servlet.view.Internal Resource ViewResolver">
11         <property name="prefix" value="/WEB-INF/jsp/"/>
12         <property name="suffix" value=".jsp"/>
13     </bean>
14 
15 </beans>

       InternalResourceViewResolver是一個輔助Bean,會在ModelAndView返回的檢視名前加上prefix指定的字首,再在最後加上suffix指定的字尾,例如:由於XXController返回的ModelAndView中的檢視名師testview。故該檢視解析器將在/WEB-INF/jsp/testview.jsp處查詢檢視。

(三)建立model

       模型對於Spring MVC來說是必不可少,如果處理程式非常簡單,完全可以忽略。模型建立的主要目的就是承載資料,使資料傳輸更加方便。

 1 public class User{
 2     private String username;
 3     private Integer age;
 4 
 5     public String getUsername(){
 6         return username;
 7     }
 8     public void setUsername(String username){
 9         this.username = username;
10     }
11 
12     public Integer getAge() {
13         return age;
14     }
15     public void setAge(Integer age){
16         this.age = age;
17     }
18 }

(四)建立Controller

       控制器用於處理Web請求,每個控制器都對應著一個邏輯處理。

 1 public class UserController extends AbstractController{
 2     @Override
 3     protected ModelAndView handleRequestInternal(HttpServletRequest arg0, HttpServletResponse arg1) throws Exception{
 4         List<User> userList = new ArrayList<User>();
 5         User userA = new User();
 6         User userB = new User();
 7         userA.setUsername("張三");
 8         userA.setAge(27);
 9         userB.setUsername("李四");
10         userB.setAge(37);
11         userList.add(userA);
12         userList.add(userB);
13 
14         return new ModelAndView("userlist", "users", userList);
15     }
16 }

       在請求的最後返回了ModelAndView型別的例項。ModelAndView類在Spring MVC中佔有很重要的地位。控制器執行方法都必須返回一個ModelAndView,ModelAndView物件儲存了檢視以及檢視顯示的模型資料,例如其中的引數如下:

       第一個引數userlist:檢視元件的邏輯名稱。這裡檢視的邏輯名稱就是userlist,檢視解析器會使用該名稱查詢實際的View物件。

       第二個引數users:傳遞給檢視的,模型物件的名稱。

       第三個引數userList:傳遞給檢視的,模型物件的值。

(五)建立檢視檔案userlist.jsp

1 <%@ page language="java" pageEncoding="UTF-8" %>
2 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
3 <h2>This is SpringMVC demo page</h2>
4 <c:forEach item="${users}" var="user">
5     <c:out value="${user.username}"/><br/>
6     <c:out value="${usr.age}"/><br/>
7 </c:forEach>

       檢視檔案用於展現請求處理結果,通過對JSTL的支援,可以很方便地展現在控制器中放入ModelAndView中處理結果資料。

(六)建立Servlet配置檔案Spring-servlet.xml

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:xsi="http//www.w3.org/2001/XMLSchema-instance"
 4        xmlns:tx="http://www.springframework.org/schema/tx"
 5        xsi:schemaLocation="http://www.springframework.org/schema/beans
 6                http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
 7                http://www.springframework.org/schema/tx
 8                http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
 9 
10     <bean id="simpleUrlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
11         <property name="mapping">
12             <props>
13                 <prop key="/usrerlist.htm">userController</prop>
14             </props>
15         </property>
16     </bean>
17 
18     <!--這裡的 id="userController對應的是<bean id="simpleUrlMapping">中的<prop>裡面的value"-->
19     <bean id="userController" class="test.controller.UserController"/>
20 
21 </beans>

       因為Spring MVC是基於Servlet的實現,所以在Web啟動的時候,伺服器會首先嚐試載入對應於Servlet的配置檔案,而為了讓專案更加模組化,通常我們講Web部分的配置都存放於此配置檔案中。

       至此,已經完成了Spring MVC的搭建,啟動伺服器,輸入網址http://localhost:8080/Springmvc/userlist.htm。會看到伺服器器的返回介面。

 

二 、ContextLoaderListener

       對於Spring MVC功能實現的分析,我們首先從web.xml開始,在web.xml檔案中我們首先配置的就是contextLoaderListener,那麼它所提供的功能有哪些又是如何實現的呢?

  當使用程式設計方式的時候我們可以直接將Spring配置資訊作為引數傳入Spring容器中,如:

  ApplicationContext ac = new ClassPathXmlApplicationContext(“applicationContext.xml”);

       但是在Web下,我們需要更多的是與Web環境相互結合,通常的辦法是將路徑以context-param的方式註冊並使用ContextLoaderListener進行監聽讀取。

       ContextLoaderListener的作用就是啟動Web容器時,自動裝配ApplicationContext的配置資訊。因為它實現了ServletContextListener這個介面,在web.xml配置這個監聽器,啟動容器時,就會預設執行它實現的方法,使用ServletContextListener介面,開發者能夠在為客戶端請求提供服務之前向ServletContext中新增任意的物件。這個物件在ServletContext啟動的時候被初始化,然後在ServletContext整個執行期間都是可見的。

       每一個Web應用都有一個ServletContext與之相關聯。ServletContext物件在應用啟動時被建立,在應用關閉的時候被銷燬。ServletContext在全域性範圍內有效,類似於應用中一個全域性變數。

       在ServletContextListener中的核心邏輯便是初始化WebApplicationContext例項並存放至ServletContext中。

(一)ServletContextListener的使用

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

  (1)建立自定義ServletContextListener。

       首先我們建立ServletContextListener,目標是在系統啟動時新增自定義的屬性,以便於在全域性範圍內可以隨時呼叫。系統啟動的時候會呼叫ServletContextListener實現類的contextInitialized方法,所以需要在這個方法中實現我們的初始化邏輯。

 1 public class MyDataContextListener implements ServletContextListener{
 2 
 3     private ServletContext context = null;
 4 
 5     public MyDataContextListener(){
 6 
 7     }
 8 
 9     //該方法在ServletContext啟動之後被呼叫,並準備好處理客戶端的請求
10     public void contextInitialized(ServletContextEvent event){
11         this.context = event.getServletContext();
12         //通過你可以實現自己的邏輯並將結果記錄在屬性中
13         context = setAttribut("myData", "this is myData");
14     }
15 
16     //這個方法在ServletContext將要關閉的時候呼叫
17     public void contextDestroyed(ServletContextEvent event){
18         this.context = null;
19     }
20 }

  (2)註冊監聽器。

  在web.xml檔案中需要註冊自定義的監聽器。

1 <listener>
2    com.test.MyDataContextListener
3 </listener>

  (3)測試。

  一旦Web應用啟動的時候,我們就能在任意的Servlet或者JSP中通過下面的方式獲取我們初始化引數,如下:String myData = (String) getServletContext().getAttribut(“myData”);

(二)Spring中的ContextLoaderListener

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

       ServletContext啟動後會呼叫ServletContextListener的contextInitialized方法,那麼,我們就從這個函式開始進行分析。

1     @Override
2     public void contextInitialized(ServletContextEvent event) {
3         //該方法在其父類ContextLoader中定義
4         initWebApplicationContext(event.getServletContext());
5     }

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

 1     public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
 2         if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
 3             //web.xml中存在多次ContextLoader的定義
 4             throw new IllegalStateException(
 5                     "Cannot initialize context because there is already a root application context present - " +
 6                     "check whether you have multiple ContextLoader* definitions in your web.xml!");
 7         }
 8 
 9         servletContext.log("Initializing Spring root WebApplicationContext");
10         Log logger = LogFactory.getLog(ContextLoader.class);
11         if (logger.isInfoEnabled()) {
12             logger.info("Root WebApplicationContext: initialization started");
13         }
14         long startTime = System.currentTimeMillis();
15 
16         try {
17             // Store context in local instance variable, to guarantee that
18             // it is available on ServletContext shutdown.
19             if (this.context == null) {
20                 //初始化context
21                 this.context = createWebApplicationContext(servletContext);
22             }
23             if (this.context instanceof ConfigurableWebApplicationContext) {
24                 ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
25                 if (!cwac.isActive()) {
26                     // The context has not yet been refreshed -> provide services such as
27                     // setting the parent context, setting the application context id, etc
28                     if (cwac.getParent() == null) {
29                         // The context instance was injected without an explicit parent ->
30                         // determine parent for root web application context, if any.
31                         ApplicationContext parent = loadParentContext(servletContext);
32                         cwac.setParent(parent);
33                     }
34                     configureAndRefreshWebApplicationContext(cwac, servletContext);
35                 }
36             }
37             //記錄在servletContext中
38             servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
39 
40             ClassLoader ccl = Thread.currentThread().getContextClassLoader();
41             if (ccl == ContextLoader.class.getClassLoader()) {
42                 currentContext = this.context;
43             }
44             else if (ccl != null) {
45                 currentContextPerThread.put(ccl, this.context);
46             }
47 
48             if (logger.isInfoEnabled()) {
49                 long elapsedTime = System.currentTimeMillis() - startTime;
50                 logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
51             }
52 
53             return this.context;
54         }
55         catch (RuntimeException | Error ex) {
56             logger.error("Context initialization failed", ex);
57             servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
58             throw ex;
59         }
60     }

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

  (1)WebApplicationContext存在性的驗證。

       在配置中只允許申明一次ServletContextListener,多次申明會擾亂Spring的執行邏輯,所以這裡首先要做的就是對比驗證,在Spring中如果建立WebApplicationContext;例項會記錄在ServletContext中以方便全域性呼叫,而使用的key就是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,所以驗證的方式就是檢視ServletContext例項中是否有對應key的屬性。

  (2)建立WebApplicationContext例項。

       如果通過驗證,則Spring將建立WebApplicationContext例項的工作委託給了createWebApplicationContext函式。

1     protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
2         Class<?> contextClass = determineContextClass(sc);
3         if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
4             throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
5                     "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
6         }
7         return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
8     }

  createWebApplicationContext函式又呼叫了determineContextClass方法,繼續:

 1     protected Class<?> determineContextClass(ServletContext servletContext) {
 2         String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
 3         if (contextClassName != null) {
 4             try {
 5                 return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
 6             }
 7             catch (ClassNotFoundException ex) {
 8                 throw new ApplicationContextException(
 9                         "Failed to load custom context class [" + contextClassName + "]", ex);
10             }
11         }
12         else {
13             contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
14             try {
15                 return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
16             }
17             catch (ClassNotFoundException ex) {
18                 throw new ApplicationContextException(
19                         "Failed to load default context class [" + contextClassName + "]", ex);
20             }
21         }
22     }

       其中,在ContextLoader類中有這樣的靜態程式碼塊:

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

       根據以上靜態的程式碼塊內容,我們推斷在當前類ContextLoader同樣目錄下必定會存在屬性檔案ContextLoader.properties(在..\spring-web\src\main\resources\org\springframework\web\context目錄下),檢視後果然存在,內容如下:

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

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

  (3)將例項記錄在servletContext中。

  (4)對映當前的類載入器與建立的例項到全域性變數currentContextPerThread中。

 

三、 DispatcherServlet

       在Spring中,ContextLoaderListener只是輔助功能,用於建立WebApplicationContext型別例項,而真正的邏輯其實是在DispatcherServlet(package org.springframework.web.servlet)中進行的,DispatcherServlet是實現servlet的介面的實現類。

       servlet是一個Java編寫的程式,此程式是基於HTTP協議的,在伺服器端執行的(如Tomcat),是按照servlet規範編寫的一個Java類。主要是處理客戶端的請求並將其結果傳送到客戶端。servlet的生命週期是由servlet的容器來控制的,它可以分為3個階段:初始化、執行和銷燬。

       (1)初始化階段

       servlet容器載入servlet類,把servlet類的.class檔案中的資料讀到記憶體中。

       servlet容器建立一個ServletConfig物件,ServletConfig物件包含了servlet的初始化配置資訊。

       servlet容器建立一個servlet物件。

       servlet容器呼叫servlet物件的init方法機芯初始化化。

       (2)執行階段

       當servelt容器接收到一個請求是,servelt容器會針對這個請求建立serveltRequest和ServletResponse物件,然後呼叫service方法。並把這兩個引數傳遞給service方法。service方法通過servletRequest物件獲得請求的資訊。並處理請求。再通過servletResponse物件生成這個請求的響應結果。然後銷燬servletRequest和ServletResponse物件。我們不管這個請求是post提交的還是get提交的,最終這個請求都會由service方法來處理。

       (3)銷燬階段。

       當Web應用被終止時,servlet容器會先呼叫servlet物件的destroy方法,然後在銷燬servlet物件,同時也會銷燬與servlet物件相關聯的servletConfig物件。我們可以在destroy方法的實現中,釋放servlet所佔用的資源,如關閉資料庫連線,關閉檔案輸入輸出流等。

       servlet的框架是由兩個Java包組成:javax.servlet和javax.servlet.http。在javax.servlet包中定義了所有的servlet類都必須實現或拓展的通用介面和類。在javax.servlet.http包中定義了採用HTTP通訊協議的HttpServlet類。

       servlet類被設計成請求驅動,servlet的請求可能包含多個數據項,當Web容器接收到某個servlet請求是,servelt把請求封裝成一個HTTPServletRequest物件,然後把物件傳給servlet的物件的服務方法。

       HTTP的請求方式包括delete、get、options、post、put和trace。在HttpServlet類中分別提供了相應的服務方法,它們是doDelete()、doGet()、doOptions()、doPost()、doPut()和doTrace()。

(一)servlet的使用

       我們同樣還是以最簡單的servlet來快速體驗其用法。

  (1)建立servlet

 1 public class Myservlet extends HttpServlet{
 2     public void init(){
 3         System.out.println("this is a method");
 4     }
 5 
 6     public void doGet(HttpServletRequest request, HttpServletResponse response){
 7         handleLogic(request, response);
 8     }
 9 
10     public void doPost(HttpServletRequest request, HttpServletResponse response){
11         handleLogic(request, response);
12     }
13 
14     public void handleLogic(HttpServletRequest request, HttpServletResponse response){
15         System.out.println("handle myLogic");
16         ServletContext sc = getServletContext();
17 
18         RequestDispatcher rd = null;
19 
20         rd = sc.getRequestDispatcher("/index.jsp");
21         try {
22             rd.forward(request, response);
23         } catch (ServletException | IOException e) {
24             e.printStackTrace();
25         }
26     }
27 }

       麻雀雖小,五臟俱全。例項中包含了對init方法和get/post方法的處理,init方法包裝在servlet載入的時候能做一些邏輯操作,而HttpServlet類則會幫助我們根據方法型別的不同而將邏輯引入不同的函式。在子類中我們只需要重寫對應的函式邏輯便可,如以上程式碼重寫了doGet和doPost方法並將邏輯處理部分引導至handleLogic函式中,最後,又將頁面跳轉至index.jsp。

  (2)新增配置

  為了是servlet能夠正常使用,需要在web.xml檔案中新增以下配置:

 1 <servlet>
 2     <servlet-name>myservlet</servlet-name>
 3     <servlet-name>test.servlet.MyServlet</servlet-name>
 4     <load-on-startup>1</load-on-startup>
 5 </servlet>
 6 
 7 <servlet-mapping>
 8     <servlet-name>myservlet</servlet-name>
 9     <url-pattern>*.htm</url-pattern>
10 </servlet-mapping>

       配置後便可以根據對應的配置訪問響應的路徑了。

(二)DispatcherServlet的初始化

       通過上面的例項我們瞭解到,在servlet初始化階段會呼叫器init方法,所以我們首先要檢視在DispatcherServlet中是否重寫了init方法。我們再其父類HttpServletBean(package org.springframework.web.servlet)中找到了該方法。

 1     @Override
 2     public final void init() throws ServletException {
 3 
 4         // Set bean properties from init parameters.
 5         //解析init-param並封裝在pvs中
 6         PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
 7         if (!pvs.isEmpty()) {
 8             try {
 9                 //將當前的這個Servlet類轉化成一個BeanWrapper,從而能夠以Spring的方式來對init-param的值進行注
10                 BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
11                 ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
12                 //註冊自定義屬性編輯器,一旦遇到Resource型別的屬性將會使用ResourceEditor進行解析
13                 bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
14                 //空實現,留給子類覆蓋
15                 initBeanWrapper(bw);
16                 //屬性注入
17                 bw.setPropertyValues(pvs, true);
18             }
19             catch (BeansException ex) {
20                 if (logger.isErrorEnabled()) {
21                     logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
22                 }
23                 throw ex;
24             }
25         }
26 
27         // Let subclasses do whatever initialization they like.
28         //留給子類擴充套件
29         initServletBean();
30     }

       DispatchServlet的初始化過程主要是通過將當前的servlet型別例項轉換成BeanWrapper型別例項,以便使用Spring中提供的注入功能進行對應屬性的注入。這些屬性如contextAttribut、contextClass、nameSpace、contextConfigLocation等,都可以在web.xml檔案中以初始化引數的方式配置在servelt的宣告中。DispatcherServlet繼承自FrameworkServlet,FrameServlet類上包含對應的同名屬性,Spring會保證這些引數被注入到對應的值中。屬性注入主要包含以下幾個步驟:

  (1)封裝及驗證初始化引數

  ServletConfigPropertyValues除了封裝屬性外還有對屬性驗證的功能。

 1         public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties)
 2                 throws ServletException {
 3 
 4             Set<String> missingProps = (!CollectionUtils.isEmpty(requiredProperties) ?
 5                     new HashSet<>(requiredProperties) : null);
 6 
 7             Enumeration<String> paramNames = config.getInitParameterNames();
 8             while (paramNames.hasMoreElements()) {
 9                 String property = paramNames.nextElement();
10                 Object value = config.getInitParameter(property);
11                 addPropertyValue(new PropertyValue(property, value));
12                 if (missingProps != null) {
13                     missingProps.remove(property);
14                 }
15             }
16 
17             // Fail if we are still missing properties.
18             if (!CollectionUtils.isEmpty(missingProps)) {
19                 throw new ServletException(
20                         "Initialization from ServletConfig for servlet '" + config.getServletName() +
21                         "' failed; the following required properties were missing: " +
22                         StringUtils.collectionToDelimitedString(missingProps, ", "));
23             }
24         }

       從程式碼中得知,封裝屬性主要是對初始化的引數進行封裝,也就是servlet中配置的<init-param>中配置的封裝。當然,使用者可以同對requiredProperties引數的初始化來強制驗證某些屬性的必要性。這樣,在屬性封裝的過程中,一旦檢測到requiredProperties中的屬性沒有指定初始值,就會丟擲異常。

  (2)將當前servlet例項轉化成BeanWrapper例項

       PropertyAccessorFactory.forBeanPropertyAccess是Spring中提供的工具方法,主要用於將指定例項轉化成Spring中可以處理的BeanWrapper型別的例項。

  (3)註冊相對於Resource的屬性編輯器

       屬性編輯器,我們再上文中已經介紹並分析過其原理。這裡使用屬性編輯器的目的是在對當前例項(DispatcherServlet)屬性注入過程中一旦遇到Resource型別的屬性就會使用ResourceEditor去解析。

  (4)屬性注入

       BeanWrapper為Spring中的方法,支援Spring的自動注入。其實我們最常用的屬性注入無非是contextAttribut、contextClass、nameSpace、contextConfigLocation等屬性。

  (5)servletBean的初始化

       在ContextLoaderListener載入的時候已經建立了WebApplicationContext例項,而在這個函式中最重要的就是對這個例項進行進一步的補充初始化。

       繼續檢視initServletBean()。父類FrameworkServlet覆蓋了HttpServletBean中的initServletBean函式,如下:

 1     protected final void initServletBean() throws ServletException {
 2         getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
 3         if (logger.isInfoEnabled()) {
 4             logger.info("Initializing Servlet '" + getServletName() + "'");
 5         }
 6         long startTime = System.currentTimeMillis();
 7 
 8         try {
 9             this.webApplicationContext = initWebApplicationContext();
10             //設為子類覆蓋
11             initFrameworkServlet();
12         }
13         catch (ServletException | RuntimeException ex) {
14             logger.error("Context initialization failed", ex);
15             throw ex;
16         }
17 
18         if (logger.isDebugEnabled()) {
19             String value = this.enableLoggingRequestDetails ?
20                     "shown which may lead to unsafe logging of potentially sensitive data" :
21                     "masked to prevent unsafe logging of potentially sensitive data";
22             logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
23                     "': request parameters and headers will be " + value);
24         }
25 
26         if (logger.isInfoEnabled()) {
27             logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
28         }
29     }

       上面的函式設計了計時器來統計初始化的執行時間,而且提供了一個擴充套件方法initFrameworkServlet()用於子類的覆蓋操作。而作為關鍵的初始化邏輯實現委託給了initWebApplicationContext()。

(三)WebApplicationContext的初始化

       initWebApplicationContext函式的主要工作就是建立或者重新整理WebApplicationContext例項並對servlet功能所使用額變數進行初始化。

 1     protected WebApplicationContext initWebApplicationContext() {
 2         WebApplicationContext rootContext =
 3                 WebApplicationContextUtils.getWebApplicationContext(getServletContext());
 4         WebApplicationContext wac = null;
 5 
 6         if (this.webApplicationContext != null) {
 7             // A context instance was injected at construction time -> use it
 8             //context例項在建構函式中被注入
 9             wac = this.webApplicationContext;
10             if (wac instanceof ConfigurableWebApplicationContext) {
11                 ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
12                 if (!cwac.isActive()) {
13                     // The context has not yet been refreshed -> provide services such as
14                     // setting the parent context, setting the application context id, etc
15                     if (cwac.getParent() == null) {
16                         // The context instance was injected without an explicit parent -> set
17                         // the root application context (if any; may be null) as the parent
18                         cwac.setParent(rootContext);
19                     }
20                     //重新整理上下文環境
21                     configureAndRefreshWebApplicationContext(cwac);
22                 }
23             }
24         }
25         if (wac == null) {
26             // No context instance was injected at construction time -> see if one
27             // has been registered in the servlet context. If one exists, it is assumed
28             // that the parent context (if any) has already been set and that the
29             // user has performed any initialization such as setting the context id
30             //根據contextAttribute屬性載入WebApplicationContext
31             wac = findWebApplicationContext();
32         }
33         if (wac == null) {
34             // No context instance is defined for this servlet -> create a local one
35             wac = createWebApplicationContext(rootContext);
36         }
37 
38         if (!this.refreshEventReceived) {
39             // Either the context is not a ConfigurableApplicationContext with refresh
40             // support or the context injected at construction time had already been
41             // refreshed -> trigger initial onRefresh manually here.
42             synchronized (this.onRefreshMonitor) {
43                 onRefresh(wac);
44             }
45         }
46 
47         if (this.publishContext) {
48             // Publish the context as a servlet context attribute.
49             String attrName = getServletContextAttributeName();
50             getServletContext().setAttribute(attrName, wac);
51         }
52 
53         return wac;
54     }

  對應本函式中的初始化主要包含幾個部分。

  (1)尋找或建立對應的WebApplicationContext例項

  WebApplicationContext的尋找和建立包括以下幾個步驟。

  (1.1)通過建構函式的注入進行初始化。

       當進入initWebApplicationContext函式後通過判斷this.webApplicationContext != null後,便可以確定this.webApplicationContext是否是通過建構函式來初始化的。可是有讀者可能會有疑問,在initServletBean函式中明明是把建立好的例項記錄在了this.webApplicationContext中:

this.webApplicationContext = initWebApplicationContext();

       何以判斷這個引數就是通過建構函式初始化,而不是通過上一次的函式返回值初始化的呢?如果存在這個問題,那麼就是讀者忽略了一個問題:在Web中包含SpringWeb的核心邏輯DispatcherServlet只可以被宣告一次,在Spring中已經存在驗證,所以這就確保瞭如果this.webApplicationContext != null,則可以直接判定this.webApplicationContext已經通過建構函式初始化了。

  (1.2)通過contextAttribute進行初始化。

       通過在web.xml檔案中配置的servlet引數contextAttribut來 查詢ServletContext中對應的屬性,預設為WebApplicationContext.class.getName()+”.ROOT”,也就是在ContextLoaderListener載入時會建立WebApplicationContext例項,並將例項以WebApplicationContext.class.getName()+”.ROOT”為key放入ServletContext中,當然讀者可以重寫初始化邏輯使用自己建立的WebApplicationContext,並在servlet的配置中通過初始化引數contextAttribut指定可以key。

 1     protected WebApplicationContext findWebApplicationContext() {
 2         String attrName = getContextAttribute();
 3         if (attrName == null) {
 4             return null;
 5         }
 6         WebApplicationContext wac =
 7                 WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
 8         if (wac == null) {
 9             throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
10         }
11         return wac;
12     }

  (1.3)重新建立WebApplicationContext例項。

       如果通過以上兩種方式並沒有找到任何突破,那就沒有辦法了,只能在這裡重新建立新的例項了。

1     protected WebApplicationContext createWebApplicationContext(@Nullable WebApplicationContext parent) {
2         return createWebApplicationContext((ApplicationContext) parent);
3     }
 1     protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
 2         //獲取servlet的初始化引數contextClass,如果沒有配置預設為XMLWebApplication.class
 3         Class<?> contextClass = getContextClass();
 4         if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
 5             throw new ApplicationContextException(
 6                     "Fatal initialization error in servlet with name '" + getServletName() +
 7                     "': custom WebApplicationContext class [" + contextClass.getName() +
 8                     "] is not of type ConfigurableWebApplicationContext");
 9         }
10         //通過反射方式例項化contextClass
11         ConfigurableWebApplicationContext wac =
12                 (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
13 
14         wac.setEnvironment(getEnvironment());
15         //parent為在ContextloaderListener中建立的例項,在ContextLoaderListener載入的時候初始化的WebApplicationContext型別例項
16         wac.setParent(parent);
17         String configLocation = getContextConfigLocation();
18         if (configLocation != null) {
19             //獲取contextConfigLocation屬性,配置在servlet初始化引數中
20             wac.setConfigLocation(configLocation);
21         }
22         //初始化Spring環境包括載入配置檔案等
23         configureAndRefreshWebApplicationContext(wac);
24 
25         return wac;
26     }

  (2)configureAndRefreshWebApplicationContext

       無論是通過建構函式注入還是單獨建立,都免不了會呼叫configureAndRefreshWebApplicationContext方法來對已經建立的WebApplicationContext例項進行配置及重新整理,那麼這個步驟又做了哪些工作呢?

 1     protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
 2         if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
 3             // The application context id is still set to its original default value
 4             // -> assign a more useful id based on available information
 5             if (this.contextId != null) {
 6                 wac.setId(this.contextId);
 7             }
 8             else {
 9                 // Generate default id...
10                 wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
11                         ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
12             }
13         }
14 
15         wac.setServletContext(getServletContext());
16         wac.setServletConfig(getServletConfig());
17         wac.setNamespace(getNamespace());
18         wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
19 
20         // The wac environment's #initPropertySources will be called in any case when the context
21         // is refreshed; do it eagerly here to ensure servlet property sources are in place for
22         // use in any post-processing or initialization that occurs below prior to #refresh
23         ConfigurableEnvironment env = wac.getEnvironment();
24         if (env instanceof ConfigurableWebEnvironment) {
25             ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
26         }
27 
28         postProcessWebApplicationContext(wac);
29         applyInitializers(wac);
30         //載入配置檔案及整合parent到wac
31         wac.refresh();
32     }

  (3)重新整理

       onRefresh是FrameworkServlet類中提供的模板方法,在其子類DispatcherServlet中進行了重寫,主要用於重新整理Spring在Web功能實現中所必須使用的全域性變數。下面我們會介紹它們的初始化過程以及使用場景,而至於具體的使用細節會在稍後的章節中再做詳細的介紹。

 1     /**
 2      * This implementation calls {@link #initStrategies}.
 3      */
 4     @Override
 5     protected void onRefresh(ApplicationContext context) {
 6         initStrategies(context);
 7     }
 8 
 9     /**
10      * Initialize the strategy objects that this servlet uses.
11      * <p>May be overridden in subclasses in order to initialize further strategy objects.
12      */
13     protected void initStrategies(ApplicationContext context) {
14         //(1)初始化MultipartResolver
15         initMultipartResolver(context);
16         //(2)初始化LocaleResolver
17         initLocaleResolver(context);
18         //(3)初始化ThemeResolver
19         initThemeResolver(context);
20         //(4)初始化HandlerMappings
21         initHandlerMappings(context);
22         //(5)初始化HandlerAdapters
23         initHandlerAdapters(context);
24         //(6)初始化HandlerExceptionResolvers
25         initHandlerExceptionResolvers(context);
26         //(7)初始化RequestToViewNameTranslator
27         initRequestToViewNameTranslator(context);
28         //(8)初始化ViewResolvers
29         initViewResolvers(context);
30         //(9)初始化FlashMapManager
31         initFlashMapManager(context);
32     }

  (3.1)初始化MultipartResolver

       在Spring中,MultipartResolver主要用來處理檔案上傳。預設情況下,Spring是沒有Multipart處理的,因為一些開發者想要自己處理它們。如果想使用Spring的multipart,則需要在Web應用的上下文中新增multipart解析器。這樣,每個請求就會被檢查是否包含multipart。然而,如果請求中包含multipart,那麼上下文中定義的MultipartResolver就會解析它,這樣請求的multipart屬性就會像其他屬性一樣被處理。常用配置如下:

1 <bean id="multipartResolver" class="org.Springframework.web.multipart.commons.CommonsMultipartResolver">
2    <!--該屬性用來配置可上傳檔案的最大byte數-->
3    <proterty name="maximumFileSize"><value>100000</value></proterty>
4 </bean>

  當然,CommonsMultipartResolver還提供了其他功能用於幫助使用者完成上傳功能,有興趣的讀者可以進一步檢視。

       那麼MultipartResolver就是在initMultipartResolver中被加入到DispatcherServlet中的。

 1     private void initMultipartResolver(ApplicationContext context) {
 2         try {
 3             this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
 4             if (logger.isTraceEnabled()) {
 5                 logger.trace("Detected " + this.multipartResolver);
 6             }
 7             else if (logger.isDebugEnabled()) {
 8                 logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
 9             }
10         }
11         catch (NoSuchBeanDefinitionException ex) {
12             // Default is no multipart resolver.
13             this.multipartResolver = null;
14             if (logger.isTraceEnabled()) {
15                 logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared");
16             }
17         }
18     }

       因為之前的步驟已經完成了Spring中配置檔案的解析,所以在這裡只要在配置檔案註冊過都可以通過ApplicationContext提供的getBean方法來直接獲取對應的bean,進而初始化MultipartResolver中的MultipartResolver變數。

  (3.2)初始化LocalResolver

       在Spring國際化配置中一共有3種使用方式。

  1. 基於URL引數的配置

       通過URL引數來控制國際化,比如你在頁面上加一句<a href=”?locale=zh_CN”>簡體中文</a>來控制專案中使用的國際化引數。而提供這個功能的就是AcceptHeaderLocalResolver,預設的引數名為local,注意大小寫。裡面放的就是你的提交引數,比如en_US、zh_CN之類的,具體配置如下:

<bean id=”localResolver” class=”org.Springframework.web.servlet.i18n.AcceptHeaderLocalResolver”/>

  2. 基於session的配置

       它通過檢驗使用者會話中國預置的屬性來解析區域。最常用的是根據使用者本次會話過程中的語言設定決定語言種類(例如,使用者登入時選擇語言種類,則此次登入週期內統一使用此語言設定),如果會話屬性不存在,它會根據accept-language HTTP頭部確定預設區域。

<bean id=”localResolver” class=”org.Springframework.web.servlet.i18n.SessionLocaleResolver”>

  3. 基於Cookie的國際化配置。

       CookieLocalResolver用於通過瀏覽器的cookie設定取得Locale物件。這種策略再應用程式不支援會話或者狀態必須儲存在客戶端時有用,配置如下:

<bean id=”localResolver” class=”org.Springframework.web.servlet.i18n.CookieLocalResolver”>

       這3種方式都可以解決國際化的問題,但是,對於LocalResolver的使用基礎是在DispatcherServlet中初始化的。

 1     private void initLocaleResolver(ApplicationContext context) {
 2         try {
 3             this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
 4             if (logger.isTraceEnabled()) {
 5                 logger.trace("Detected " + this.localeResolver);
 6             }
 7             else if (logger.isDebugEnabled()) {
 8                 logger.debug("Detected " + this.localeResolver.getClass().getSimpleName());
 9             }
10         }
11         catch (NoSuchBeanDefinitionException ex) {
12             // We need to use the default.
13             this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
14             if (logger.isTraceEnabled()) {
15                 logger.trace("No LocaleResolver '" + LOCALE_RESOLVER_BEAN_NAME +
16                         "': using default [" + this.localeResolver.getClass().getSimpleName() + "]");
17             }
18         }
19     }

       提取配置檔案中設定的LocalResolver來初始化DispatcherServlet中的localeResolver屬性。

  (3.3)初始化ThemeResolver。

       在Web開發中經常會遇到通過主題Theme來控制網頁風格。這將進一步改善使用者體驗。簡單的說,一個主題就是一組靜態資源(比如樣式表和圖片)。它們可以影響程式的視覺效果。Spring中的主題功能和國際化功能非常類似。構成Spring主題功能主要包括如下內容。

  1. 主題資源

       org.Springframework.ui.context.ThemeSource是Spring中主題資源的介面。Spring的主題需要通過ThemeSource介面來實現存放主題資訊的資源。

       org.Springframework.ui.context.support.ResourceBundleThemeSource是ThemeSource介面預設實現類(也就是通過ResourceBundle資源的方式定義主題。)在Spring中的配置如下:

1 <bean id="themeSource" class="org.Springframework.ui.context.support.ResourceBundleThemeSource">
2    <proterty name="basenamePrefix" value="com.text."></proterty>
3 </bean>

       預設狀態下是在類路徑根目錄下查詢相應的資原始檔。也可以通過basenamePrefix來制定。這樣,DispatcherServlet就會在com.test包下查詢資原始檔。

  2. 主題解析器。

       ThemeSource定義了一些主題資源,那麼不同的使用者使用什麼主題資源由誰定義呢?org.Springframework.web.servlet.ThemeResolver是主題解析器的介面,主題解析器的工作便是由它的子類來完成的。

       對於主題解析器的子類主要有3個比較常用的實現。以主題檔案summer.properties為例。

  ①FixedThemeResolver用於選擇一個固定的主題。

1 <bean id="themeSource" class="org.Springframework.web.servlet.theme.FixedThemeResolver">
2    <proterty name="defaultThemeName" value="summer"/>
3 </bean>

  以上配置的作用是設定主題檔案為summer.properties,在整個專案內固定不變。

  ②CookieThemeResolver用於實現使用者所選的主題,以cookie的形式存放在客戶端的機器上,配置如下:

1 <bean id="themeSource" class="org.Springframework.web.servlet.theme. CookieThemeResolver">
2    <proterty name="defaultThemeName" value="summer"/>
3 </bean>

  ③SessionThemeResolver用於主題儲存在使用者的HTTP session中。

1 <bean id="themeSource" class="org.Springframework.web.servlet.theme. SessionThemeResolver">
2    <proterty name="defaultThemeName" value="summer"/>
3 </bean>

  以上配置用於設定主題名稱,並且將該名稱儲存在使用者的HttpSession中。

  ④AbstractThemeResolver是一個抽象類被SessionThemeResolver和FixedThemeResolver繼承,使用者也可以繼承它來自定義主題解析器。

  3. 攔截器。

       如果需要根據使用者請求來改變主題,那麼Spring提供了一個已經實現的攔截器ThemeChangeInterceptor攔截器了,配置如下:

1 <bean id="themeChangeInterceptor" class="org.Springframework.web.servlet.theme.ThemeChangeInterceptor">
2    <proterty name="paramName" value="themeName"/>
3 </bean>

       其中設定使用者請求引數名為themeName,即URL為?themeName=具體的主題名稱。此外,還需要在handlerMapping中配置攔截器。當然需要在handleMapping中新增攔截器。

1 <property name="interceptors">
2    <list>
3       <ref local="themeChangeInterceptor"/>
4    </list>
5 </property>

       瞭解了主題檔案的簡單使用方式後,再來檢視解析器的初始化工作。與其他變數的初始化工作相同,主題檔案解析器的初始化工作並沒有任何特別需要說明的地方。

 1     private void initThemeResolver(ApplicationContext context) {
 2         try {
 3             this.themeResolver = context.getBean(THEME_RESOLVER_BEAN_NAME, ThemeResolver.class);
 4             if (logger.isTraceEnabled()) {
 5                 logger.trace("Detected " + this.themeResolver);
 6             }
 7             else if (logger.isDebugEnabled()) {
 8                 logger.debug("Detected " + this.themeResolver.getClass().getSimpleName());
 9             }
10         }
11         catch (NoSuchBeanDefinitionException ex) {
12             // We need to use the default.
13             this.themeResolver = getDefaultStrategy(context, ThemeResolver.class);
14             if (logger.isTraceEnabled()) {
15                 logger.trace("No ThemeResolver '" + THEME_RESOLVER_BEAN_NAME +
16                         "': using default [" + this.themeResolver.getClass().getSimpleName() + "]");
17             }
18         }
19     }

  (3.4)初始化HandlerMapping

       當客戶端發出Request時DispatcherServlet會將Request提交給HandlerMapping,然後HandlerMapping根據WebApplicationContext的配置來回傳給DispatcherServlet響應的Controller。

       在基於SpringMVC的Web應用程式中,我們可以為DispatcherServlet提供多個HandlerMapping供其使用。DispatcherServlet在選用HandlerMapping的過程中,將根據我們所指定的一系列HandlerMapping的優先順序進行排序,然後優先使用優先順序在簽名的HandlerMapping。如果當前的HandlerMapping能夠返回可用的Handler,DispatcherServlet則使用當前返回的Handler進行Web請求的處理,而不再繼續詢問其他的HandlerMapping。否則,DispatcherServlet將繼續按照各個HandlerMapping的優先順序進行詢問,直到獲取一個可用的Handler為止。初始化配置如下:

 1     private void initHandlerMappings(ApplicationContext context) {
 2         this.handlerMappings = null;
 3 
 4         if (this.detectAllHandlerMappings) {
 5             // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
 6             Map<String, HandlerMapping> matchingBeans =
 7                     BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
 8             if (!matchingBeans.isEmpty()) {
 9                 this.handlerMappings = new ArrayList<>(matchingBeans.values());
10                 // We keep HandlerMappings in sorted order.
11                 AnnotationAwareOrderComparator.sort(this.handlerMappings);
12             }
13         }
14         else {
15             try {
16                 HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
17                 this.handlerMappings = Collections.singletonList(hm);
18             }
19             catch (NoSuchBeanDefinitionException ex) {
20                 // Ignore, we'll add a default HandlerMapping later.
21             }
22         }
23 
24         // Ensure we have at least one HandlerMapping, by registering
25         // a default HandlerMapping if no other mappings are found.
26         if (this.handlerMappings == null) {
27             this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
28             if (logger.isTraceEnabled()) {
29                 logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
30                         "': using default strategies from DispatcherServlet.properties");
31             }
32         }
33     }

       預設情況下,SpringMVC將載入當前系統中所有實現了HandlerMapping介面的bean。如果只期望SpringMVC載入指定的handlerMapping時,可用修改web.xml中的DispatcherServlet的初始引數,將detectAllHandlerMappings的值設定為false:

1 <init-param>
2    <param-name>detectAllHandlerMappings</param-name>
3    <param-value>false</param-value>
4 </init-param>

       此時,SpringMVC將查詢名為“HandlerMapping”的bean,並作為當前系統中唯一的handlerMapping。如果沒有定義HandlerMapping的話,則SpringMVC將按照org.Springframework.web.servlet.DispatcherServlet所在目錄下的DispatcherServlet.properties中所定義的org.Springframework.web.servlet.HandlerMapping的內容來載入預設的handlerMapping(使用者沒有自定義Strategies的情況下)。

  (3.5)初始化HandlerAdapters

       從名字也能聯想到這是一個典型的介面卡模式的使用,在計算機程式設計中,介面卡模式將一個類的介面適配成使用者所期待的。使用介面卡,可以使介面不相容而無法在一起工作的類協同工作。做飯是將類自己的介面包裹在一個已存在的類中。那麼在處理handler時為什麼會使用介面卡模式呢?回答這個問題我們首先要分析它的初始化邏輯。

 1     private void initHandlerAdapters(ApplicationContext context) {
 2         this.handlerAdapters = null;
 3 
 4         if (this.detectAllHandlerAdapters) {
 5             // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts.
 6             Map<String, HandlerAdapter> matchingBeans =
 7                     BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
 8             if (!matchingBeans.isEmpty()) {
 9                 this.handlerAdapters = new ArrayList<>(matchingBeans.values());
10                 // We keep HandlerAdapters in sorted order.
11                 AnnotationAwareOrderComparator.sort(this.handlerAdapters);
12             }
13         }
14         else {
15             try {
16                 HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
17                 this.handlerAdapters = Collections.singletonList(ha);
18             }
19             catch (NoSuchBeanDefinitionException ex) {
20                 // Ignore, we'll add a default HandlerAdapter later.
21             }
22         }
23 
24         // Ensure we have at least some HandlerAdapters, by registering
25         // default HandlerAdapters if no other adapters are found.
26         if (this.handlerAdapters == null) {
27             this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
28             if (logger.isTraceEnabled()) {
29                 logger.trace("No HandlerAdapters declared for servlet '" + getServletName() +
30                         "': using default strategies from DispatcherServlet.properties");
31             }
32         }
33     }

       同樣在初始化的過程中涉及了一個變數detectAllHandlerAdapters,detectAllHandlerAdapters的作用和detectAllHandlerMapping類似,只不過作用物件為handlerAdapter。亦可通過如下配置來強制系統只加載bean name為“handlerAdapter”的handlerAdapter。

1 <init-param>
2    <param-name>detectAllHandlerAdapters</param-name>
3    <param-value>false</param-value>
4 </init-param>

       如果無法找到對應的bean,那麼系統會嘗試載入預設的介面卡。

 1     @SuppressWarnings("unchecked")
 2     protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
 3         String key = strategyInterface.getName();
 4         String value = defaultStrategies.getProperty(key);
 5         if (value != null) {
 6             String[] classNames = StringUtils.commaDelimite