搶先一步
VMware 提供訓練和認證,以加速您的進展。
了解更多在我之前的文章中,我介紹了內容協商的概念以及 Spring MVC 用於判斷所請求內容的三種策略。
在這篇文章中,我想將這個概念特別擴展到支援使用 ContentNegotiatingViewResolver
(或 CNVR)針對不同內容類型的多個視圖。
由於我們已經從之前的文章中了解如何設定內容協商,因此使用它在多個視圖之間進行選擇非常簡單。只需像這樣定義一個 CNVR
<!--
// View resolver that delegates to other view resolvers based on the
// content type
-->
<bean class="org.springframework.web.servlet.view.
ContentNegotiatingViewResolver">
<!-- All configuration now done by manager - since Spring V3.2 -->
<property name="contentNegotiationManager" ref="cnManager"/>
</bean>
<!--
// Setup a simple strategy:
// 1. Only path extension is taken into account, Accept headers
// are ignored.
// 2. Return HTML by default when not sure.
-->
<bean id="cnManager" class="org.springframework.web.accept.
ContentNegotiationManagerFactoryBean">
<property name="ignoreAcceptHeader" value="true"/>
<property name="defaultContentType" value="text/html" />
</bean>
對於每個請求,@Controller
通常會返回一個邏輯視圖名稱(或者 Spring MVC 會根據傳入的 URL 依慣例決定一個)。CNVR 將查詢配置中定義的所有其他視圖解析器,以查看 1) 它是否具有具有正確名稱的視圖,以及 2) 它是否具有也產生正確內容的視圖 - 所有視圖都「知道」它們返回的內容類型。所需的內容類型以與先前文章中討論的完全相同的方式確定。
有關等效的 Java 配置,請參閱這裡。有關擴展配置,請參閱這裡。Github 上有一個示範應用程式:https://github.com/paulc4/mvc-content-neg-views。
對於那些趕時間的人來說,這就是重點。
對於其餘的人,這篇文章展示了我們是如何達到這個目標的。它討論了 Spring MVC 中多個視圖的概念,並在此基礎上定義了 CNVR 是什麼、如何使用它以及它是如何運作的。它採用了先前文章中的相同 Accounts 應用程式,並將其建構成以 HTML、試算表、JSON 和 XML 格式傳回帳戶資訊。全部都僅使用視圖。
MVC 模式的優勢之一是能夠為相同的資料提供多個視圖。在 Spring MVC 中,我們使用「內容協商」來實現這一點。我之前的文章一般性地討論了內容協商,並展示了使用 HTTP 訊息轉換器的 RESTful 控制器的範例。但內容協商也可以與視圖一起使用。
例如,假設我希望不僅以網頁形式顯示帳戶資訊,而且還使其以試算表的形式提供。我可以為每個使用不同的 URL,在我的 Spring 控制器上放置兩個方法,並讓每個方法都傳回正確的視圖類型。(順便說一句,如果您不確定 Spring 如何建立試算表,稍後我會向您展示)。
@Controller
class AccountController {
@RequestMapping("/accounts.htm")
public String listAsHtml(Model model, Principal principal) {
// Duplicated logic
model.addAttribute( accountManager.getAccounts(principal) );
return ¨accounts/list¨; // View determined by view-resolution
}
@RequestMapping("/accounts.xls")
public AccountsExcelView listAsXls(Model model, Principal principal) {
// Duplicated logic
model.addAttribute( accountManager.getAccounts(principal) );
return new AccountsExcelView(); // Return view explicitly
}
}
使用多個方法是不優雅的,破壞了 MVC 模式,而且如果我也想支援其他資料格式(例如 PDF、CSV ...)會變得更醜陋。如果您還記得在之前的文章中,我們遇到了一個類似的問題,希望單一方法傳回 JSON 或 XML(我們透過傳回單一 @RequestBody
物件並選擇正確的 HTTP 訊息轉換器來解決這個問題)。
[caption id="attachment_13458" align="alignleft" width="380" caption="透過內容協商選擇正確的視圖。"][/caption]
現在我們需要一個「智慧型」視圖解析器,它可以從多個可能的視圖中選擇正確的視圖。
Spring MVC 長期以來一直支援多個視圖解析器,並依次訪問每個視圖解析器以尋找視圖。儘管可以指定諮詢視圖解析器的順序,但 Spring MVC 始終選擇第一個提供的視圖。「內容協商視圖解析器」(CNVR)在所有視圖解析器之間進行協商,以找到最適合所需格式的最佳匹配 - 這是我們的「智慧型」視圖解析器。
這是一個簡單的帳戶列表應用程式,我們將使用它作為我們的實作範例,以 HTML、試算表以及(稍後)JSON 和 XML 格式列出帳戶 - 僅使用視圖。
完整的程式碼可以在 Github 上找到:https://github.com/paulc4/mvc-content-neg-views。這是我上次向您展示的應用程式的變體,它僅使用視圖來產生輸出。注意:為了使下面的範例保持簡單,我直接使用了 JSP 和 InternalResourceViewResolver
。Github 專案使用 Tiles 和 JSP,因為它比原始 JSP 更容易。
帳戶列表 HTML 頁面的螢幕截圖顯示了目前登入使用者的所有帳戶。您稍後將看到試算表和 JSON 輸出的螢幕截圖。
產生我們頁面的 Spring MVC 控制器如下所示。請注意,HTML 輸出由邏輯視圖 accounts/list
產生。
@Controller
class AccountController {
@RequestMapping("/accounts")
public String list(Model model, Principal principal) {
model.addAttribute( accountManager.getAccounts(principal) );
return ¨accounts/list¨;
}
}
為了顯示兩種視圖類型,我們需要兩種視圖解析器 - 一種用於 HTML,另一種用於試算表(為了簡單起見,我將使用 JSP 作為 HTML 視圖)。這是 Java 配置
@Configuration
@EnableWebMvc
public class MvcConfiguration extends WebMvcConfigurerAdapter {
@Autowired
ServletContext servletContext;
// Will map to bean called "accounts/list" in "spreadsheet-views.xml"
@Bean(name="excelViewResolver")
public ViewResolver getXmlViewResolver() {
XmlViewResolver resolver = new XmlViewResolver();
resolver.setLocation(new ServletContextResource(servletContext,
"/WEB-INF/spring/spreadsheet-views.xml"));
resolver.setOrder(1);
return resolver;
}
// Will map to the JSP page: "WEB-INF/views/accounts/list.jsp"
@Bean(name="jspViewResolver")
public ViewResolver getJspViewResolver() {
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
resolver.setPrefix("WEB-INF/views");
resolver.setSuffix(".jsp");
resolver.setOrder(2);
return resolver;
}
}
或在 XML 中
<!-- Maps to a bean called "accounts/list" in "spreadsheet-views.xml" -->
<bean class="org.springframework.web.servlet.view.XmlViewResolver">
<property name="order" value="1"/>
<property name="location" value="WEB-INF/spring/spreadsheet-views.xml"/>
</bean>
<!-- Maps to "WEB-INF/views/accounts/list.jsp" -->
<bean class="org.springframework.web.servlet.view.
InternalResourceViewResolver">
<property name="order" value="2"/>
<property name="prefix" value="WEB-INF/views"/>
<property name="suffix" value=".jsp"/>
</bean>
在 WEB-INF/spring/spreadsheet-beans.xml
中,您會找到
<bean id="accounts/list" class="rewardsonline.accounts.AccountExcelView"/>
產生的試算表如下所示
以下是如何使用視圖建立試算表(這是一個簡化版本,完整的實作要長得多,但您會了解其概念)
class AccountExcelView extends AbstractExcelView {
@Override
protected void buildExcelDocument(Map<String, Object> model,
HSSFWorkbook workbook, HttpServletRequest request,
HttpServletResponse response) throws Exception {
List<Account> accounts = (List<Account>) model.get("accountList");
HSSFCellStyle dateStyle = workbook.createCellStyle();
dateStyle.setDataFormat(HSSFDataFormat.getBuiltinFormat("m/d/yy"));
HSSFSheet sheet = workbook.createSheet();
for (short i = 0; i < accounts.size(); i++) {
Account account = accounts.get(i);
HSSFRow row = sheet.createRow(i);
addStringCell(row, 0, account.getName());
addStringCell(row, 1, account.getNumber());
addDateCell(row, 2, account.getDateOfBirth(), dateStyle);
}
}
private HSSFCell addStringCell(HSSFRow row, int index, String value) {
HSSFCell cell = row.createCell((short) index);
cell.setCellValue(new HSSFRichTextString(value));
return cell;
}
private HSSFCell addDateCell(HSSFRow row, int index, Date date,
HSSFCellStyle dateStyle) {
HSSFCell cell = row.createCell((short) index);
cell.setCellValue(date);
cell.setCellStyle(dateStyle);
return cell;
}
}
就目前情況而言,此設定將始終傳回試算表,因為首先諮詢了 XmlViewResolver
(其 order
屬性為 1),並且它始終傳回 AccountExcelView
。InternalResourceViewResolver
永遠不會被諮詢(其 order
為 2,我們永遠不會到達那一步)。
這就是 CNVR 的用武之地。讓我們快速回顧一下我們在之前的文章中討論的內容選擇策略。請求的內容類型是透過依此順序檢查來確定的
http://...accounts.json
以指示 JSON 格式。format
,例如 http://...accounts?format=json
。Accept
標頭屬性(這實際上是 HTTP 定義的工作方式,但並不總是方便使用 - 特別是當用戶端是瀏覽器時)。在前兩種情況下,後綴或參數值(xml
、json
...)必須對應到正確的 mime 類型。可以使用 JavaBeans Activation Framework,也可以明確指定對應關係。對於 Accept
標頭屬性,其值是 mine 類型。
這是一個特殊的視圖解析器,我們的策略已插入其中。這是 Java 配置
@Configuration
@EnableWebMvc
public class MvcConfiguration extends WebMvcConfigurerAdapter {
/**
* Setup a simple strategy:
* 1. Only path extension taken into account, Accept headers ignored.
* 2. Return HTML by default when not sure.
*/
@Override
public void configureContentNegotiation
(ContentNegotiationConfigurer configurer) {
configurer.ignoreAcceptHeader(true)
.defaultContentType(MediaType.TEXT_HTML);
}
/**
* Create the CNVR. Get Spring to inject the ContentNegotiationManager
* created by the configurer (see previous method).
*/
@Bean
public ViewResolver contentNegotiatingViewResolver(
ContentNegotiationManager manager) {
ContentNegotiatingViewResolver resolver =
new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager(manager);
return resolver;
}
}
或在 XML 中
<!--
// View resolver that delegates to other view resolvers based on the
// content type
-->
<bean class="org.springframework.web.servlet.view.
ContentNegotiatingViewResolver">
<!-- All configuration now done by manager - since Spring V3.2 -->
<property name="contentNegotiationManager" ref="cnManager"/>
</bean>
<!--
// Setup a simple strategy:
// 1. Only path extension taken into account, Accept headers ignored.
// 2. Return HTML by default when not sure.
-->
<bean id="cnManager" class="org.springframework.web.accept.
ContentNegotiationManagerFactoryBean">
<property name="ignoreAcceptHeader" value="true"/>
<property name="defaultContentType" value="text/html" />
</bean>
ContentNegotiationManager
與我在之前的文章中討論的 bean 完全相同。
CNVR 自動轉到 Spring 定義的每個其他視圖解析器 bean,並要求它提供與控制器傳回的視圖名稱相對應的 View
實例 - 在這種情況下為 accounts/list
。每個 View
都「知道」它可以產生哪種內容,因為它上面有一個 getContentType()
方法(從 View
介面繼承)。JSP 頁面由 JstlView
(由 InternalResourceViewResolver
傳回)呈現,其內容類型為 text/html
,而 AccountExcelView
產生 application/vnd.ms-excel
。
CNVR 的實際配置方式委託給 ContentNegotiationManager
,後者又透過配置器(Java 配置)或 Spring 的許多工廠 bean(XML)之一建立。
最後一個謎題是:CNVR 如何知道請求了什麼內容類型?因為內容協商策略告訴它該怎麼做:要麼識別 URL 後綴,要麼識別 URL 參數或 Accept 標頭。與之前的文章中描述的策略設定完全相同,CNVR 重複使用。
請注意,當 Spring 3.0 引入內容協商策略時,它們僅適用於選擇視圖。自 3.2 以來,此功能已在所有領域可用(根據我之前的文章)。這篇文章中的範例使用 Spring 3.2,可能與您以前看過的舊範例不同。特別是,用於配置內容協商策略的大多數屬性現在位於ContentNegotiationManagerFactoryBean
上,而不是位於ContentNegotiatingViewResolver
上。CNVR 上的屬性現在已被棄用,以支持管理器上的屬性,但 CNVR 本身的工作方式與以往完全相同。
預設情況下,CNVR 自動偵測到 Spring 定義的所有 ViewResolvers
,並在它們之間進行協商。如果您願意,CNVR 本身具有 viewResolvers
屬性,因此您可以明確告訴它要使用哪些視圖解析器。這清楚地表明 CNVR 是主要解析器,而其他解析器是它的附屬。請注意,不再需要 order
屬性。
@Configuration
@EnableWebMvc
public class MvcConfiguration extends WebMvcConfigurerAdapter {
// .. Other methods/declarations
/**
* Create the CNVR. Specify the view resolvers to use explicitly.
* Get Spring to inject the ContentNegotiationManager created by the
* configurer (see previous method).
*/
@Bean
public ViewResolver contentNegotiatingViewResolver(
ContentNegotiationManager manager) {
// Define the view resolvers
List<ViewResolver> resolvers = new ArrayList<ViewResolver>();
XmlViewResolver r1 = new XmlViewResolver();
resolver.setLocation(new ServletContextResource(servletContext,
"/WEB-INF/spring/spreadsheet-views.xml"));
resolvers.add(r1);
InternalResourceViewResolver r2 = new InternalResourceViewResolver();
r2.setPrefix("WEB-INF/views");
r2.setSuffix(".jsp");
resolvers.add(r2);
// Create CNVR plugging in the resolvers & content-negotiation manager
ContentNegotiatingViewResolver resolver =
new ContentNegotiatingViewResolver();
resolver.setViewResolvers(resolvers);
resolver.setContentNegotiationManager(manager);
return resolver;
}
}
或在 XML 中
<bean class="org.springframework.web.servlet.view.
ContentNegotiatingViewResolver">
<property name="contentNegotiationManager" ref="cnManager"/>
<!-- Define the view resolvers explicitly -->
<property name="viewResolvers">
<list>
<bean class="org.springframework.web.servlet.view.XmlViewResolver">
<property name="location" value="spreadsheet-views.xml"/>
</bean>
<bean class="org.springframework.web.servlet.view.
InternalResourceViewResolver">
<property name="prefix" value="WEB-INF/views"/>
<property name="suffix" value=".jsp"/>
</bean>
</list>
</property>
</bean>
Github 示範專案使用 2 組 Spring 設定檔。在 web.xml
中,您可以為 XML 或 Java 配置分別指定 xml
或 javaconfig
。對於它們中的任何一個,指定 separate
或 combined
。separate
設定檔將所有視圖解析器定義為頂層 bean,並讓 CNVR 掃描內容以找到它們(如上一節所述)。在 combined
設定檔中,視圖解析器被明確定義,而不是作為 Spring bean,並透過其 viewResolvers
屬性傳遞給 CNVR(如本節所示)。
MappingJacksonJsonView
,它支援使用 Jackson Object to JSON 對應程式庫從 Java 物件產生 JSON 資料。MappingJacksonJsonView
自動將模型中找到的所有屬性轉換為 JSON。唯一的例外是它會忽略 BindingResult
物件,因為這些物件是 Spring MVC 表單處理的內部物件,不需要。需要一個合適的視圖解析器,而 Spring 沒有提供一個。幸運的是,編寫您自己的視圖解析器非常簡單
public class JsonViewResolver implements ViewResolver {
/**
* Get the view to use.
*
* @return Always returns an instance of {@link MappingJacksonJsonView}.
*/
@Override
public View resolveViewName(String viewName, Locale locale)
throws Exception {
MappingJacksonJsonView view = new MappingJacksonJsonView();
view.setPrettyPrint(true); // Lay JSON out to be nicely readable
return view;
}
}
只需將此視圖解析器宣告為 Spring bean,就表示可以傳回 JSON 格式的資料。JAF 已經將 json
對應到 application/json
,因此我們完成了。現在,像 http://myserver/myapp/accounts/list.json
這樣的 URL 可以傳回 JSON 格式的帳戶資訊。這是我們 Accounts 應用程式的輸出
有關此視圖的更多資訊,請參閱 Spring Javadoc。
有一個類似的類別用於產生 XML 輸出 - MarshallingView。它採用模型中可以封送處理的第一個物件並處理它。您可以選擇性地配置視圖,方法是告訴它要選擇哪個模型屬性(鍵)- 請參閱 setModelKey()
。
同樣,我們需要一個視圖解析器來處理它。Spring 透過 Spring 的 Object to XML Marshalling (OXM) 抽象支援多種封送處理技術。讓我們只使用 JAXB2,因為它內建於 JDK 中(自 JDK 6 以來)。這是解析器
/**
* View resolver for returning XML in a view-based system.
*/
public class MarshallingXmlViewResolver implements ViewResolver {
private Marshaller marshaller;
@Autowired
public MarshallingXmlViewResolver(Marshaller marshaller) {
this.marshaller = marshaller;
}
/**
* Get the view to use.
*
* @return Always returns an instance of {@link MappingJacksonJsonView}.
*/
@Override
public View resolveViewName(String viewName, Locale locale)
throws Exception {
MarshallingView view = new MarshallingView();
view.setMarshaller(marshaller);
return view;
}
}
同樣,我的類別需要註解才能與 JAXB 一起使用(為了回應評論,我在之前的文章的末尾新增了一個範例)。
使用 Java 配置將新的解析器配置為 Spring bean
@Bean(name = "marshallingXmlViewResolver")
public ViewResolver getMarshallingXmlViewResolver() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
// Define the classes to be marshalled - these must have
// @Xml... annotations on them
marshaller.setClassesToBeBound(Account.class,
Transaction.class, Customer.class);
return new MarshallingXmlViewResolver(marshaller);
}
或者我們可以在 XML 中做同樣的事情 - 請注意 oxm 命名空間的使用
<oxm:jaxb2-marshaller id="marshaller" >
<oxm:class-to-be-bound name="rewardsonline.accounts.Account"/>
<oxm:class-to-be-bound name="rewardsonline.accounts.Customer"/>
<oxm:class-to-be-bound name="rewardsonline.accounts.Transaction"/>
</oxm:jaxb2-marshaller>
<!-- View resolver that returns an XML Marshalling view. -->
<bean class="rewardsonline.accounts.MarshallingXmlViewResolver" >
<constructor-arg ref="marshaller"/>
</bean>
這是我們完成的系統
使用 @ResponseBody
、@ResponseStatus
和其他 REST 相關的 MVC 註解,可以完全支援 MVC 的 RESTful 方法。類似於這樣
@RequestMapping(value="/accounts",
produces={"application/json", "application/xml"})
@ResponseStatus(HttpStatus.OK)
public @ResponseBody List<Account> list(Principal principal) {
return accountManager.getAccounts(principal);
}
為了為我們的 @RequestMapping
方法啟用相同的內容協商,我們必須重複使用我們的內容協商管理器(這允許 produces
選項工作)。
<mvc:annotation-driven
content-negotiation-manager="contentNegotiationManager" />
然而,這產生了不同風格的控制器方法,優點是它也更強大。那麼該怎麼辦:視圖還是 @ResponseBody
?
對於已經使用 Spring MVC 和視圖的現有網站,MappingJacksonJsonView
和 MarshallingView
提供了一種簡單的方法來擴展 Web 應用程式,以同時傳回 JSON 和/或 XML。在許多情況下,這些是您唯一需要的資料格式,並且是一種簡單的方法來支援唯讀行動應用程式和/或 AJAX 啟用的網頁,其中 RESTful 請求僅用於 GET 資料。
完全支援 REST,包括修改資料的能力,涉及結合 HTTP 訊息轉換器使用帶註解的控制器方法。在這種情況下使用視圖沒有意義,只需傳回 @ResponseBody
物件並讓轉換器完成工作即可。
然而,如 http://blog.springsource.org/2013/05/11/content-negotiation-using-spring-mvc/#combined-controller"">here 在我之前的文章中所示,控制器完全可以在同一時間使用這兩種方法。現在,同一個控制器可以同時支援傳統的 Web 應用程式和實作完整的 RESTful 介面,從而增強可能已經建立和開發多年的 Web 應用程式。
Spring 一直以來都非常重視為開發人員提供彈性和選擇。這也不例外。