我們已經知道了ViewResolver的主要職責是,根據Controller所返回的ModelAndView中的邏輯視圖名,為DispatcherServlet返回一個可用的View實例。現在是揭開ViewResolver如何“盡職”的時候了。
有ViewResolver的職責為前提,理解甚至於自己聲明一個ViewResolver接口變得不再困難。實際上ViewResolver接口定義確實很簡單,如下所示:
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
接口實現類只需要根據resolveViewName()方法中以參數形式傳入的邏輯視圖名(viewName)和當前Locale的值,返回相應的View實例即可[17]。至於每個ViewResolver實現類如何處理具體的邏輯視圖名與具體的View實例之間的對應關系,則因實現類的不同而存在差異。
大部分的ViewResolver實現類,除了org.springframework.web.servlet.view.BeanName- ViewResolver是直接實現ViewResolver接口,都直接或者間接繼承自org.springframe work. web.servlet.view.AbstractCachingViewResolver。因為針對每次請求都重新實例化View將可能為Web應用程序帶來性能上的損失,所以Spring MVC在AbstractCachingViewResolver這一繼承層次加入了View實例的緩存功能。AbstractCachingViewResolver默認啟用View的緩存功能。對於生產環境來說,這是合理的默認值。不過,如果在測試或者開發環境下,我們想即刻反映相應的修改結果,可以通過setCache(false)暫時關閉AbstractCachingViewResolver的緩存功能。
Spring MVC在AbstractCachingViewResolver的基礎上為我們提供了一系列的ViewResolver實現。下面讓我們來認識一下它們的廬山真面目。
為了便於理解,我們可以將Spring MVC提供的ViewResolver划分為兩類,一類稱為“面向單一視圖類型的ViewResolver,另一類則稱為面向多視圖類型的ViewResolver。下面是這兩類ViewResolver的詳細情況。
該類別ViewResolver的正宗名稱應該是UrlBasedViewResolver(它們都直接地或者間接地繼承自該類)。使用該類別的ViewResolver,我們不需要為它們配置具體的邏輯視圖名到具體View的映射關系。通常只要指定一下視圖模板所在的位置,這些ViewResolver就會按照邏輯視圖名,抓取相應的模板文件、構造對應的View實例並返回。之所有又將它們稱之為面向單一視圖類型的ViewResolver,是因為該類別中,每個具體的ViewResolver實現都只負責一種View類型的映射,ViewResolver與View之間的關系是一比一。比如,我們之前一直使用的InternalResourceView- Resolver,它通常就只負責到指定位置抓取JSP模板文件,並構造InternalResourceView類型的View實例並返回。而VelocityViewResolver則只關心指定位置的Velocity模板文件(.vm),並會將邏輯視圖名映射到視圖模板的文件名,然后構造VelocityView類型的View實例返回,諸如此類。
屬於該類別的主要ViewResolver實現類為如下幾個。
q InternalResourceViewResolver。它是我們使用最多的ViewResolver實現類型,它對應InternalResourceView[18]視圖類型的映射,說白了也就是處理JSP模板類型的視圖映射。如果DispatcherServlet在初始化的時候,不能在自己的WebApplicationContext中找到至少一個ViewResolver,那么,InternalResourceViewResolver將作為默認的ViewResolver被使用。
q FreeMarkerViewResolver/VelocityViewResolver。FreeMarkerViewResolver和Velo- cityViewResolver分別負責對應FreeMarkerView和VelocityView類型視圖的查找工作,它們將根據邏輯視圖名到指定的位置獲取對應的模板文件,並構造FreeMarkerView和VelocityView的實例返回給DispatcherServlet使用。
q JasperReportsViewResolver。JasperReportsViewResolver只關心根據邏輯視圖名到指定位置查找JasperReport類型模板文件,並返回AbstractJasperReportsView的具體子類型View實例,例如JasperReportsCsvView或者JasperReportsHtmlView等。
q XsltViewResolver。只負責根據邏輯視圖名查找並返回XsltView類型的View實例。
啟用以上這些ViewResolver,與使用InternalResourceViewResolver一樣簡單。最基本的方法是,使用prefix屬性指定模板所在路徑,使用suffix屬性指定模板文件的后綴名。這樣,在獲取邏輯視圖名之后,相應的ViewResolver內部就能夠根據[prefix]+viewName+[suffix]這樣的URL找到對應的模板文件,並構造對應的View實例而返回了。以VelocityViewResolver的使用為例,至於其他的幾個ViewResolver的使用,你基本上就可以“舉一反三”了,更加詳盡的配置項,可以參考對應類的Javadoc或者Professional Java Development with the Spring Framework一書中對應視圖章節的介紹內容。下面給出了針對VelocityViewResolver的配置代碼示例:
class="org.springframework.Web.servlet.view.velocity.VelocityViewResolver">
現在DispatcherServlet對視圖的請求將會由VelocityViewResolver接管,Velocity- ViewResolver將根據傳入的邏輯視圖名,到指定目錄下查找.vm類型的Velocity模板文件,並構造VelocityView實例返回給DispatcherServlet使用。就跟我們所說的那樣,它只負責到指定位置查找對應Velocity的單一視圖類型,而不會返回其他,比如Freemarker視圖對應的View實例。
注意 關於使用Velocity作為視圖技術需要附加的配置內容,可以參考稍后ResourceBundle- ViewResolver部分的附帶信息。
使用面向單一視圖類型的ViewResolver,我們不需要指定明確的邏輯視圖名與具體視圖之間的映射關系,對應的ViewResolver將自動到指定位置匹配自己所管轄的那種視圖模板,並構造具體的View實例。面向多視圖類型的ViewResolver則不然。使用面向多視圖類型的ViewResolver,我們需要通過某種配置方式明確指定邏輯視圖名與具體視圖之間的映射關系,這可能帶來配置上的煩瑣。不過,好處是,面向多視圖類型的ViewResolver可以顧及多種視圖類型的映射管理。如果你的邏輯視圖名想要映射到InternalResourceView,那么面向多視圖類型的ViewResolver可以做到。如果你的邏輯視圖名想要映射到VelocityView,那么,面向多視圖類型的ViewResolver也可以做到。相對於只支持單一視圖類型映射的情況,面向多視圖類型的ViewResolver更加靈活。
面向多視圖類型的ViewResolver的主要實現類有三個,它們分別是ResourceBundleView- Resolver、XmlViewResolver以及BeanNameViewResolver。以下是它們的詳細情況介紹。
ResourceBundleViewResolver。ResourceBundleViewResolver構建在ResourceBundle上,繼承了ResourceBundle國際化支持的能力,也是所有的ViewResolver實現類中唯一提供視圖國際化支持的ViewResolver。ResourceBundleViewResolver管理的視圖的邏輯名稱與具體視圖的映射關系保存在properties文件中,格式符合Spring的IoC容器的properties配置格式。ResourceBundleView- Resolver內部將通過PropertiesBeanDefinitionReader加載這些配置信息。之后,根據邏輯視圖名查找的操作,實際上也就簡化為beanfactory.getBean(viewName)的形式了(當然,實際上要做的事情會多一些)。
使用ResourceBundleViewResolver之前,我們得先將其添加到DispatcherServlet的WebApp- licationContext中,如下所示:
class="org.springframework.Web.servlet.view.ResourceBundleViewResolver">
如果我們沒有指定properties配置文件從何處加載的話,ResourceBundleViewResolver默認將從classpath的根路徑加載以views為basename的properties文件,比如views.properties、views_zh_CN.properties等。如果我們想改變這種默認加載行為,可以通過setBasename (String)或者setBasenames(String[])方法來進行變更。
以下是一個典型的ResourceBundleViewResolver使用的properties配置文件內容:
viewTemplate.class=org.springframework.Web.servlet.view.InternalResourceView
viewTemplate.(abstract)=true
help/HelpForSomething.(parent)=viewTemplate
help/HelpForSomething.url=/WEB-INF/jsp/help/HelpForSomething.jsp
hello.class=org.springframework.Web.servlet.view.velocity.VelocityView
hello.url=cn/spring21/simplefx/resources/velocity/hello.vm
# 其他視圖定義……
視圖的bean定義主要有兩個屬性:class和url。如果我們想要避免每次為同一類型的視圖指定某些共同的屬性,也可以定義一個模板聲明,然后通過parent引用該模板聲明。這些特性在第二部分中我們已經領教過了,不是嗎?
注意 如果要在ResourceBundleViewResolver中使用Velocity或者Freemarker之類的通用模板引擎渲染的視圖,那么需要在WebApplicationContext中添加相應的配置,使得視圖渲染階段能夠獲取模板引擎的支持。實際上,單獨使用VelocityViewResolver或者Freemarker- ViewResolver也需要同樣的配置。
我們以使用Velocity類型視圖的配置為例,(Freemarker類型視圖的配置與Velocity類型視圖的配置雷同)。在應用程序的WebApplicationContext中,我們添加org.springframework.web. servlet.view. velocity.VelocityConfigurer的配置如下:
class="org.springframework.Web.servlet.view.velocity.VelocityConfigurer">
value="/WEB-INF/velocity-config.properties"/>
/bean>
這樣,在視圖渲染階段就可以根據該配置獲取一個VelocityEngine進行視圖模板與數據的合並(Merge)操作,以便最終輸出視圖頁面。
velocity-config.properties的配置內容,完全就是特定於Velocity的內容了。你可以參考Velocity的相關文檔獲取配置參數,這里可以給出一個簡單的實例,如下所示:
resource.loader = classpath
classpath.resource.loader.description = Classpath Resource Loader
classpath.resource.loader.class =
org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
classpath.resource.loader.path=.
velocimacro.library =
最后,對於Velocity(或者Freemarker)的模板文件,最好像我們給出的配置內容所指定的那樣,將它們放入應用程序的classpath中進行加載,而不是依賴於默認的文件系統加載行為。
XmlViewResolver。XmlViewResolver與ResourceBundleViewResolver之間最主要的區別就是,它們所采用的配置文件格式不同。ResourceBundleViewResolver按照Spring IoC容器所接受的properties配置格式配置邏輯視圖名與具體視圖之間的映射關系,而XmlViewResolver則是按照Spring IoC容器接受的XML配置文件格式來加載映射信息。與ResourceBundleViewResolver同樣的配置信息,使用XmlViewResolver的話,內容如代碼清單24-28所示。
代碼清單24-28 XmlViewResolver使用配置代碼示例
class="org.springframework.Web.servlet.view.InternalResourceView"
abstract="true">
class="org.springframework.Web.servlet.view.velocity.VelocityView"
p:url="cn/spring21/simplefx/resources/velocity/hello.vm">
XmlViewResolver默認會加載/WEB-INF/views.xml作為配置文件。不過,我們可以在將XmlView- Resolver添加到WebApplicationContext的時候,根據情況改變這一默認行為,例如:
現在,XmlViewResolver將從Classpath的根路徑加載名為views.xml的配置文件。至於其他配置,比如Velocity需要的VelocityConfigurer,因為與使用何種ViewResolver沒有關系,只與是否使用Velocity作為視圖技術有關,所以依然需要根據情況添加到WebApplicationContext中。
注意 XmlViewResolver並不支持視圖的國際化(I18n)。如果必須對國際化視圖給予支持,需要使用ResourceBundleViewResolver。
BeanNameViewResolver。BeanNameViewResolver可以認為是XmlViewResolver的原型版或者簡化版。使用它,我們可以直接將View實例注冊到當前DispatcherServlet所使用的特定的WebAppli- cationContext中,而不用像XmlViewResolver那樣另辟一塊地。不過,BeanNameViewResolver更多地用於快速搭建應用框架原型,或者構建小型的Web應用程序。對於正常的基於Spring MVC的Web應用程序,應盡量避免將可以分離出來的視圖配置信息一並加入到DispatcherServlet的WebApp- licationContext中。
至於如何啟用BeanNameViewResolver作為ViewResolver,我想你現在要比我清楚,如下所示:
class="org.springframework.Web.servlet.view.BeanNameViewResolver"/>
至於具體視圖的配置,參照XmlViewResolver即可。
實際上,正如我們所看到的那樣,這三種ViewResolver在本質上是一樣的,只不過是配置的表現形式上存在差異而已。最終的配置信息都將轉換為Spring IoC容器中管理的View實例,BeanName- ViewResolver應該是最初的實現原型吧!
24.4.2 ViewResolver查找序列(Chain Of ViewResolver)
雖然我們在之前的示例中一直都是使用一個InternalResourceViewResolver進行視圖查找,但這並不意味着每個基於Spring MVC的Web應用程序只能使用一個ViewResolver。實際上,Dispatcher- Servlet不但可以接受多個HandlerMapping以處理Web請求到具體Handler的映射,也可以接受多個ViewResolver以處理視圖的查找。
DispatcherServlet初始化時,將根據類型掃描自己的WebApplicationContext中定義的ViewResolver。如果查找到存在多個ViewResolver的定義,DispatcherServlet將根據這些ViewResolver的優先級進行排序,然后當需要根據邏輯視圖名查找具體的View實例的時候,將按照排序后的順序遍歷這些ViewResolver,只要期間任何一個ViewResolver返回非空的View實例,當前查找即告結束。如果DispatcherServlet沒能在當前的WebApplicationContext中找到任何的ViewResolver定義,它將使用InternalResourceViewResolver作為默認的ViewResolver使用。
ViewResolver的優先級的指定使用Ordered接口作為標准,這已經成為Spring框架內設定優先級方式的慣例了。假設我們希望主要使用ResourceBundleViewResolver進行邏輯視圖名到具體View實例的查找,如果沒能找到,再尋求InternalResourceViewResolver的幫助。我們可以在Dispatcher- Servlet的WebApplicationContext中添加如下配置內容:
class="org.springframework.Web.servlet.view.ResourceBundleViewResolver">
class="org.springframework.Web.servlet.view.InternalResourceViewResolver">
相應ViewResolver的bean定義對應的id或者name屬性值是任意的,DispatcherServlet將按照類型來獲取ViewResolver。如果沒有為某個ViewResolver指定order值的話,默認值為Integer.MAX_ VALUE,對應的是最低優先級。
如果為DispatcherServlet指定多個ViewResolver的話,不要給予InternalResour- ceViewResolver以及其他UrlBasedViewResolver子類過高的優先級,因為這些ViewResolver即使找不到相應的視圖,也不會返回null以給我們輪詢下一個ViewResolver的機會,這樣,我們所指定的其他ViewResolver實際上就形同虛設。合理的處理方式是,給予ResourceBundleView- Resolver或者XmlViewResolver這種能夠通過返回null以表明無法找到相應視圖的ViewResolver較高的優先級,而只是將InternalResourceViewResolver(或者其他類似行為的ViewResolver)添加為最低優先級ViewResolver,以作為DispatcherServlet的后備查找對象。