Spring MVC 解析之 DispatcherServlet

擺碼王子發表於2018-03-17

Spring MVC 是什麼

Spring Web MVC (Spring MVC) 是一套以 Servlet API 為基礎平臺的優雅的 Web 框架,一直是 Spring Framework 中重要的一個組成部分。 正式名稱 “Spring Web MVC” 來自其源模組 spring-webmvc 的名稱,但它通常被稱為“Spring MVC”。

與 Spring Web MVC 並行,Spring Framework 5.0 引入了一個 Reactive stack —— Web框架,其名稱 Spring WebFlux 也基於它的源模組 spring-webflux。

DispatcherServlet

與許多其他 Web 框架一樣,Spring MVC 同樣圍繞前端頁面的控制器模式 (Controller) 進行設計,其中最為核心的 Servlet —— DispatcherServlet 為來自客戶端的請求處理提供通用的方法,而實際的工作交由可自定義配置的元件來執行。 這種模型使用方式非常靈活,可以滿足多樣化的專案需求。

和任何普通的 Servlet 一樣,DispatcherServlet 需要根據 Servlet 規範使用 Java 程式碼配置或在 web.xml 檔案中宣告請求和 Servlet 的對映關係。 DispatcherServlet 通過讀取 Spring 的配置來發現它在請求對映,檢視解析,異常處理等方面所依賴的元件。

以下是註冊和初始化 DispatcherServlet 的 Java 程式碼配置示例。 該類將被 Servlet 容器自動檢測到:

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // 載入 Spring Web Application 的配置
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class);
        ac.refresh();

        // 建立並註冊 DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}
複製程式碼

以下是在 web.xml 中註冊和初始化 DispatcherServlet 的方法:

<web-app>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- Spring 上下文的配置 -->
        <param-value>/WEB-INF/app-context.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <!-- "/*" 表示將所有請求交由 DispatcherServlet 處理 -->
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>
複製程式碼

1. 應用上下文的層次結構

DispatcherServlet 依賴於一個 WebApplicationContext(對普通 ApplicationContext 的功能擴充套件)來實現自己的配置。 WebApplicationContext 中包含了一個指向它所關聯的 ServletContext 和 Servlet 的連結。 它同時還繫結到 Servlet 上下文中,以便應用程式可以使用 RequestContextUtils 中的靜態方法在 WebApplicationContext 進行查詢,來判斷是否需要呼叫 DispatcherServlet 中的方法。

對於只有一個 WebApplicationContext 應用程式來說,這已經可以滿足使用了。 同時也可以使用具有層次結構的上下文,其中有一個根上下文(或者叫基上下文) 被多個 DispatcherServlet(或其他普通 Servlet)例項所共享,每個例項都有屬於自己的子上下文配置。

根上下文通常包含被多個 Servlet 例項共享的公共 bean,例如資料倉儲和業務。 這些 bean 被繼承下來使用,還可以在特定的 Servlet 的子上下文中重寫(即重新宣告一個 bean 的配置),子上下文中擁有該 Servlet 所獨有的區域性 bean 例項:

Spring MVC 解析之 DispatcherServlet

以下是使用 WebApplicationContext 層次結構的示例配置:

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { App1Config.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/app1/*" };
    }
}
複製程式碼

在 web.xml 中的配置

<web-app>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- 根上下文的配置 -->
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app1</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!-- app1 專屬的的子上下文 -->
            <param-value>/WEB-INF/app1-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app1</servlet-name>
        <url-pattern>/app1/*</url-pattern>
    </servlet-mapping>

</web-app>
複製程式碼

2. 帶有特殊功能的 Bean

DispatcherServlet 依靠這些特殊的 bean 來處理請求並返回響應。 這些特殊的 bean 是指實現 WebFlux 框架協議的,同樣由 Spring 管理的物件。 這些物件都含有一套預設的配置,但也可以自定義各種屬性,從而進行靈活擴充套件或功能改寫。

bean 類名 功能
HandlerMapping 將請求對映到處理程式以及用於預處理和後續處理的一系列攔截器。 這種對映有著一套標準,具體的功能因 HandlerMapping 實現而異。 HandlerMapping 的兩個最主要實現是 RequestMappingHandlerMapping 和 SimpleUrlHandlerMapping ,前者支援 @RequestMapping 註釋方法,它為請求的處理進行 URI 對映的註冊。
HandlerAdapter 協助 DispatcherServlet 呼叫匹配請求對映的處理程式,且不需要關心如何呼叫處理程式以及處理程式的任何細節。 例如,呼叫帶註釋的控制器中的方法需要先對 @RequestMapping 等註釋進行解析。 HandlerAdapter 的主要功能是遮蔽 DispatcherServlet 的實現細節。
HandlerExceptionResolver 包含各種異常的解決方法,可以將不同的異常對映到響應的處理程式或頁面等。
ViewResolver 將處理程式中的方法返回值(字串)的邏輯檢視名稱解析為實際檢視,來將響應返回給客戶端。
LocaleResolver, LocaleContextResolver 識別客戶端的當前區域設定以估測大概的時區,從而能夠返回響應地區的國際化檢視。
ThemeResolver 解析當前 Web 應用程式可用的主題,例如提供個性化佈局。
MultipartResolver 在相應的解析庫的輔助下,對 multi-part 請求(比如瀏覽器的表單檔案上傳)進行解析。
FlashMapManager 儲存和檢索可將引數從一個請求傳遞到另一個請求的“輸入”和“輸出”的 FlashMap,通常通過重定向來實現。

3. Web MVC 的配置

應用程式可以單獨宣告上文中“帶有特殊功能的 Bean”中列出的基礎版的 bean。 DispatcherServlet 掃描這些 bean 所屬的 WebApplicationContext。 如果沒有匹配的 bean 型別,它將返回 DispatcherServlet.properties 中的預設型別。

在大多數情況下,MVC 預設配置是最好的實現。 它採用 Java 程式碼或 XML 檔案來配置所需的 bean,同時提供更高階別的配置回撥 API 用於改寫預設配置。

4. Servlet 的配置

在 Servlet 3.0 以上的版本中,可以用 Java程式碼或與 web.xml 檔案相結合來配置 Servlet 容器。 以下是用 Java 程式碼註冊 DispatcherServlet 的示例:

import org.springframework.web.WebApplicationInitializer;

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
        XmlWebApplicationContext appContext = new XmlWebApplicationContext();
        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");

        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
        registration.setLoadOnStartup(1);
        registration.addMapping("/");
    }
}
複製程式碼

WebApplicationInitializer 是 Spring MVC 提供的一個介面,其所有實現類都可以被掃描到,並自動用於初始化任何 Servlet 3 容器。 AbstractDispatcherServletInitializer (WebApplicationInitializer 的一個抽象父類)的實現類可以通過覆蓋方法來配置 Servlet 請求對映、DispatcherServlet 配置檔案的目錄,這樣很簡單的就實現了 DispatcherServlet 的配置。

如果通過 Java 程式碼來配置 Spring 的話,需要這樣做:

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { MyWebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}
複製程式碼

如果用 xml 檔案來配置 Spring,則只需定義一個 AbstractDispatcherServletInitializer 實現類即可:

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }

    @Override
    protected WebApplicationContext createServletApplicationContext() {
        XmlWebApplicationContext cxt = new XmlWebApplicationContext();
        cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
        return cxt;
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}
複製程式碼

AbstractDispatcherServletInitializer 還可以輕鬆新增 Filter 並自動對映到 DispatcherServlet 中:

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {
            new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
    }
}
複製程式碼

不同的 Filter 都會以不同的名稱註冊,同時對映到 DispatcherServlet 中。

The isAsyncSupported protected method of AbstractDispatcherServletInitializer provides a single place to enable async support on the DispatcherServlet and all filters mapped to it. By default this flag is set to true.

AbstractDispatcherServletInitializer 中的 isAsyncSupported() 方法可以設定各個 Filter 是否開啟非同步支援。

如果還想更加細化自定義配置,可以通過重寫 createDispatcherServlet() 方法來實現。

5. 處理請求

DispatcherServlet 處理請求的規則:

  • 在請求中查詢並繫結 WebApplicationContext,它可以作為引數被控制器中的方法使用。 預設繫結到 DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE 對應的值。
  • 區域解析器 (LocaleResolver) 也繫結到請求上,它可以在請求解析、呈現檢視、準備資料等過程中將資訊解析為當前的區域環境。如果無需解析這些資訊,可以不用管它。
  • 主題解析器用來決定使用哪個主題。 如果你不使用主題,可以忽略掉它。
  • 如果在應用中宣告瞭 multipart file resolver,則會對請求進行 multipart 檢查;如果發現了 multiparts,請求會被包裝成 MultipartHttpServlet 來進行處理。
  • 如果返回模型,則會解析並返回檢視。 如果沒有返回模型(由於其他處理程式攔截了請求,可能出於安全原因),則不會返回檢視,因為可能已經有響應返回給客戶端了。

WebApplicationContext 中宣告的 HandlerExceptionResolver bean 可以解析請求處理時丟擲的異常。 可以給異常解析器進行特定的配置來解決特定的異常。

DispatcherServlet 還支援返回最後修改日期。 DispatcherServlet 掃描註冊的對映關係並,判斷找到的處理程式是否實現了 LastModified 介面。 如果實現了,則將 LastModified 介面的 long getLastModified(request)方法的返回值返回給客戶端。

在 web.xml 中,可以通過配置 Servlet 的初始化引數(init-param)來自定義一個 DispatcherServlet 的例項。

DispatcherServlet 的初始化引數

引數 含義
contextClass WebApplicationContext 的實現類,初始化 Servlet 的上下文,預設為 XmlWebApplicationContext
contextConfigLocation 作為引數傳遞給在 contextClass 中指定上下文例項,用於標識上下文的位置,接受多個字串(以逗號分隔),如果同一個 bean 的配置在多個上下文中出現,則以最後一個為準。
namespace WebApplicationContext 的名稱空間,預設為 元素中的值加上“servlet”。比如,app,那麼,名稱空間為 appServlet。

6. 攔截器

所有 HandlerMapping 的實現類都支援使用攔截器,特別是將某些功能只應用到特定的請求上時(比如判斷許可權),攔截器就非常有用。 攔截器必須實現 org.springframework.web.servlet 包中的 HandlerInterceptor,它提供了三個方法,供不同時刻來呼叫:

  • preHandle(..):在請求處理前執行...
  • postHandle(..):在請求處理後執行...
  • afterCompletion(..):在請求處理完全結束後執行...

preHandle(..) 方法返回布林值,可以通過這一點選擇來切斷或繼續請求的處理鏈。當返回 true 的時候,處理器鏈會繼續執行;返回 false 的時候, DispatcherServlet 就會認為攔截器已經處理了請求或返回了檢視,並不會繼續被處理鏈中的其他處理器或攔截器所處理。

請注意,postHandle(..) 對使用 @ResponseBody 和 ResponseEntity 方法的用處不大,在 HandlerAdapter 中, postHandle(..) 呼叫之前就已經提交響應了。 所以這時再修改響應什麼用也沒有了。 這種情況下,可以實現 ResponseBodyAdvice,並將其宣告為 Controller Advice bean (在 Controller 類上新增 @ControllerAdvice 註解)或直接在 RequestMappingHandlerAdapter 中進行配置。

7. 異常處理

在對映或呼叫請求處理程式 (例如帶有 @Controller 註解的控制器) 處理請求時,如果丟擲了異常,則 DispatcherServlet 呼叫 HandlerExceptionResolver bean 來處理異常,然後將錯誤頁面或錯誤狀態碼等資訊返回個客戶端。

下面列出 HandlerExceptionResolver 的幾個實現類:

HandlerExceptionResolver 實現類

類名 功能
SimpleMappingExceptionResolver 將異常型別對映到異常頁面的檢視名,可以很容易實現錯誤頁面的返回
DefaultHandlerExceptionResolver 處理由 Spring MVC 丟擲的異常,可以直接對映到不同的 HTTP 狀態碼
ResponseStatusExceptionResolver 處理帶有 @ResponseStatus 註解的異常,並將其對映到 HTTP 狀態碼
ExceptionHandlerExceptionResolver 通過呼叫控制器(帶有 @Controller 註解或 @ControllerAdvice 註解)中的帶有 @ExceptionHandler 註解的方法,來處理異常

如果需要同時對映多個異常型別,需要設定不同異常的權重 (order 屬性),權重越高,處理時機越晚。

處理方式

HandlerExceptionResolver 中可以返回:

  • ModelAndView: 指向檢視名
  • 不帶屬性的 ModelAndView: 如果已經通過權重更低的方法處理過異常了
  • null: 這個異常無法被當前異常處理器識別,需要丟給接下來的處理器,如果所有處理器都不能處理這個異常,異常會傳到 Servlet 容器中,由容器來處理
  • 自定義異常處理請求也非常簡單,比如,在 xml 中配置一個 HandlerExceptionResolver 的 bean, Spring MVC 會自動使用內部的預設異常處理器來處理 Spring MVC 丟擲的異常(帶有 @ResponseStatus 註解的異常或帶有 @ExceptionHandler 註解的方法),可以修改這些預設配置或乾脆直接重寫新的配置。

Servlet 容器中定義的錯誤頁面 (error-page)

如果丟擲的異常沒能被任何 HandlerExceptionResolver 處理,就會傳到 Servlet 容器中。如果將 HTTP 響應碼設定為 4xx 或 5xx, Servlet 容器會直接返回預設的錯誤頁面。可以在 web.xml 中配置自定義的錯誤頁面:

<error-page>
    <location>/error</location>
</error-page>
複製程式碼

此時,如果想要對異常處理進一步自定義,可以這樣做:

@RestController
public class ErrorController {

    @RequestMapping(path = "/error")
    public Map<String, Object> handle(HttpServletRequest request) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", request.getAttribute("javax.servlet.error.status_code"));
        map.put("reason", request.getAttribute("javax.servlet.error.message"));
        return map;
    }
}
複製程式碼

這樣就能再次將 /error 請求交給 DispatcherServlet 來處理,這種方式解決了使用 RestController 返回 JSON,而不是返回檢視的情況。

8. 檢視名解析

Spring MVC 定義了 View 和 ViewResolver 這兩個介面,View 負責返回檢視前的資料準備, ViewResolver 則負責將邏輯檢視名對映到實際檢視。 遮蔽了具體的檢視實現細節。

ViewResolver 的幾個實現類

類名 功能
AbstractCachingViewResolver AbstractCachingViewResolver 的實現類會將解析過的檢視快取,快取可以提升特定檢視技術的效能。可以通過設定 cache 屬性的值為 false 來將快取功能關閉。如果想在執行時重新整理檢視快取,可以呼叫 removeFromCache(String viewName, Locale loc) 方法將已快取內容移除。
XmlViewResolver 可以使用 Spring bean DTD 定義,在 xml 中對其配置。預設的配置檔案為 /WEB-INF/views.xml
ResourceBundleViewResolver 通過解析定義的 ResourceBundle bean,將 viewname 解析為檢視名,url 解析為檢視路徑
UrlBasedViewResolver 可以直接將 url 對映到返回值中,無需再進行額外的對映配置,適用於檢視結構清晰,可以直接對應匹配的情況
InternalResourceViewResolver UrlBasedViewResolver 的子類,可以解析 InternalResourceView (比如 Servlet 或 JSP) 以及其子類,比如 JstlView 或 TilesView。可以通過 setViewClass(..) 方法來指定要解析的具體檢視型別
FreeMarkerViewResolver UrlBasedViewResolver 的子類,可以解析 FreeMarkerView 以及自定義的子類
ContentNegotiatingViewResolver 通過請求的 url 或請求頭資訊來解析檢視

處理方式

和上面提到的異常解析器一樣,通過配置 bean 來解析檢視,同樣可以指定不同解析器的權重,權重越高,呼叫時機越晚。

ViewResolver 可以通過返回 null 來表示檢視無法解析,如果是 JSP 或 InternalResourceViewResolver,可以判斷 JSP 是否存在的唯一方法就是通過執行 RequestDispatcher 的請求排程方法。 因此必須將 InternalResourceViewResolver 的權重配置為所有檢視解析器中最高的。

如果返回檢視的執行過程不需要處理任何的業務邏輯,可以使用檢視控制器來實現:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 將 url 為 "/" 的請求對映到名為 home 的檢視上
        registry.addViewController("/").setViewName("index");
    }
}
複製程式碼

在 xml 中這樣寫:

<mvc:view-controller path="/" view-name="index"/>
複製程式碼

重定向

如果返回的檢視名以 "redirect:" 開頭, UrlBasedViewResolver 就會將其解析為請求重定向,後面的檢視名就是將重定向的 url。

這樣和返回 RedirectView 的效果是一樣的,他可以將請求重定向到當前 Servlet 上下文的相對路徑,如果寫成這樣 redirect:http://myhost.com/some/arbitrary/path,就會重定向到這個絕對路徑上。

如果在控制器的方法上寫了 @ResponseStatus 註解,則註解中的狀態碼優先權高於 RedirectView 中的。

轉發

如果最終的檢視解析器為 UrlBasedViewResolver, 則可以使用 forward:,效果通過 forward() 方法建立一個 InternalResourceView 。這個字首不適用於 InternalResourceViewResolver 和 InternalResourceView (JSP),但對於使用其他的檢視技術且想執行請求轉發,就非常有用了。同樣,將多個檢視解析器組成處理鏈實現鏈式處理。

內容規約

ContentNegotiatingViewResolver 實際並不會解析檢視,而是呼叫其他檢視解析器,查詢與客戶端請求中有關資訊匹配的檢視。這些資訊可以從請求頭中的 Accept 中或 url 中引數獲得。

ContentNegotiatingViewResolver 將請求的媒體型別與 ViewResolvers 關聯的 View 支援的媒體型別(也稱為 Content-Type)進行比較,選擇最佳的 View 來處理請求。 與這個 Content-Type 相符的第一個檢視會被首先返回給客戶端。 如果 ViewResolver 鏈無法解析檢視,則會在 DefaultViews 的檢視列表進行查詢。 後者適用於無需進行業務邏輯處理的單例 View ,因此不用考慮邏輯檢視名。 Accept 頭中可以包含萬用字元,例如 text/ * ,則將匹配 content-type 為 text/xml的 View 。

9. 語言環境

Spring MVC 和 Spring 中的大多數模組一樣,也提供了內容國際化的功能。DispatcherServlet 可以根據客戶端的語言環境進行內容的自動國際化,這要歸功於 LocaleResolver。

DispatcherServlet 接受到請求後會去上下文中查詢 LocaleResolver bean,找到後就會使用它完成國際化的任務。可以呼叫 RequestContext.getLocale() 來獲得請求的語言環境。

如果不想使用預設的自動解析方式,還可以通過在處理程式對映中新增一個攔截器來實現,比如解析 url 中的引數。

有關國際化的解析器和攔截器都定義在 org.springframework.web.servlet.i18n 中,並且都需要在應用上下文中配置。Spring 可以國際化這些內容:

時區

除了語言環境,有時還需要獲得客戶端的時區資訊,LocaleContextResolver 對 LocaleResolver 中的功能進行了擴充套件,這樣可以在 LocaleContext 中獲得更為豐富的資訊。

通過 RequestContext.getTimeZone()方法獲取客戶端的時區資訊,在 Spring 的 ConversionService 中註冊的日期/時間轉換器和格式化器會自動獲取這些資訊。

請求頭

這個區域解析器會識別請求中的 accept-language 頭,這個欄位一般包含客戶作業系統的區域資訊,但是它無法獲得客戶端的時區資訊。

Cookie

這個區域解析器會檢查客戶端上的 Cookie 是否存在語言或時區資訊。這個區域解析器可以設定 cookie 的名字和失效時間。在 xml 檔案中定義 CookieLocaleResolver:

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">

    <!-- 根據具體情況自定義 -->
    <property name="cookieName" value="clientlanguage"/>

    <!-- 100000 為秒數,值為 -1 時, cookie 會在瀏覽器關閉時被銷燬 -->
    <property name="cookieMaxAge" value="100000"/>

</bean>
複製程式碼

CookieLocaleResolver 部分屬性的含義

屬性 預設值 含義
cookieName 類名+語言環境 cookie 的名稱
cookieMaxAge Servlet 容器中規定的值 cookie 的有效期,值為 -1 時, cookie 會在瀏覽器關閉時被銷燬
cookiePath / 將 cookie 的作用域限制到制定的相對路徑下,cookie 將只在這個路徑及其自路徑中有效

Session

SessionLocaleResolver 可以從會話上下文中獲取語言和時區資訊,它和 CookieLocaleResolver 的區別是:它是在 Servlet 容器的 session 中儲存語言和時區資訊,所有,這些資訊都是暫時的,會話結束後也就不復存在了。

語言環境攔截器

LocaleChangeInterceptor bean 可以通過自定義配置來修改請求中的引數,達到修改語言環境的目的。呼叫 LocaleResolver 中的 setLocal() 方法即可。下面的例子演示將所有請求中的 siteLanguage 引數修改為荷蘭語:

<bean id="localeChangeInterceptor"
        class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
    <property name="paramName" value="siteLanguage"/>
</bean>

<bean id="localeResolver"
        class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>

<bean id="urlMapping"
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="localeChangeInterceptor"/>
        </list>
    </property>
    <property name="mappings">
        <value>/**/*.view=someController</value>
    </property>
</bean>
複製程式碼

10. 主題

Spring Web MVC 框架支援自定義主題,應用程式的整體外觀可以實現統一修改。主題即靜態資源的集合,通常是 CSS 和圖片,兩者決定了應用的整體風格。

主題的定義

必須定義一個 org.springframework.ui.context.ThemeSource 介面的實現類才能使用主題功能。預設情況下,WebApplicationContext 通過 org.springframework.ui.context.support.ResourceBundleThemeSource 實現主題功能,它從 classpath 的根目錄讀取配置檔案。 要使用自定義的 ThemeSource,需要配置 ResourceBundleThemeSource 的主題名稱字首,在應用程式上下文中註冊一個 themeSource bean。

使用 ResourceBundleThemeSource 自定義主題,需要將配置寫在 properties 檔案中,這個檔案中包含了構成主題的所有資源。

styleSheet=/themes/cool/style.css
background=/themes/cool/img/coolBg.jpg
複製程式碼

在 JSP 中使用主題設定,要用到 spring:theme 標籤:

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
    <head>
        <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
    </head>
    <body style="background=<spring:theme code='background'/>">
        ...
    </body>
</html>
複製程式碼

預設情況下,ResourceBundleThemeSource 主題名字首是空的。properties 檔案是從 classpath 的根目錄讀取的。所以應當將 abc.properties 配置檔案放到根目錄下(/WEB-INF/classes)。 ResourceBundleThemeSource 可以實現主題的國際化。比如,/WEB-INF/classes/abc_nl.properties,結合上面對 background 屬性的配置,可以實現將背景切換為帶有荷蘭文字的背景圖片。

主題的解析

定義好主題配置後,當有請求發來時,在預處理階段,通過查詢上下文中叫 themeResolver 的 bean 來對決定用什麼解析器來解析主題,這裡的工作原理和上文中的 localeResolver 一樣。通過識別帶有不同引數的請求來對請求的主題進行切換。Spring 提供了這幾種 themeResolver:

類名 功能
FixedThemeResolver 通過讀取 defaultThemeName 屬性來選擇固定的主題
SessionThemeResolver 主題資訊由 session 儲存,每個 session 只需解析一次,不同的 session 之間無法共享
FixedThemeResolver 主題資訊儲存在客戶端的 cookie 中

Spring 還提供了一個 ThemeChangeInterceptor 攔截器,通過識別請求的引數來切換主題。

11. Multipart

org.springframework.web.multipart.MultipartResolver 提供了一種處理表單上傳檔案的解決方案,有兩種實現方式,一種是基於 Apache 的 Commons-Fileupload,另一種是基於 Servlet 3.0 的。

首先要在 Spring 的配置檔案中為 DispatcherServlet 宣告一個叫做 MultipartResolver 的 bean,DispatcherServlet 會自動識別並呼叫它來處理檔案上傳請求。它會將 content-type 為 multipart/form-data 的請求包裝成 MultipartHttpServletRequest,從而將這些 "part" 暴露為請求的一個引數。

Apache FileUpload

要想使用 Apache Commons-Fileupload,需要將 multipartResolver bean 配置為 CommonsMultipartResolver,另外不要忘了新增 commons-fileupload 的依賴。

Servlet 3.0

如果通過 Servlet 3.0 處理 multipart 請求,則同樣需要在 DispatcherServlet 中註冊。在 Java 程式碼中配置的話,需要新增一個 MultipartConfigElement;在 xml 檔案中配置的話,新增 <multipart-config> 節點。檔案大小限制和檔案儲存位置等選項同樣需要這樣配置,因為在 Servlet 3.0 以後,不允許 MultipartResolver 這麼幹了。

Servlet 3.0 配置好後,只需要在 xml 檔案中將 multipartResolver hean 配置為 StandartMultipartResolver 即可。

相關文章