領先一步
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 頭部屬性,其值 就是 MIME 型別。
這是一個特殊的檢視解析器,我們的策略就內建其中。這是 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,並請求它們提供一個與控制器返回的檢視名稱(在本例中為 accounts/list)相對應的 View 例項。每個 View 都“知道”它能生成什麼型別的內容,因為它上面有一個 getContentType() 方法(繼承自 View 介面)。JSP 頁面由 JstlView 渲染(由 InternalResourceViewResolver 返回),其內容型別為 text/html,而 AccountExcelView 生成 application/vnd.ms-excel。
CNVR 實際的配置方式委託給 ContentNegotiationManager,後者又透過配置器(Java Configuration)或 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 演示專案使用兩組 Spring 配置檔案。web.xml 中可以分別指定 xml 或 javaconfig 用於 XML 或 Java 配置。對於它們中的任何一個,都可以指定 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 的 物件到 XML 編組 (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" />
然而,這會產生一種不同風格的 Controller 方法,其優點是功能更強大。那麼,該選擇哪種方式:Views 還是 @ResponseBody?
對於已經使用 Spring MVC 和檢視的現有網站,MappingJacksonJsonView 和 MarshallingView 提供了一種簡單的方法來擴充套件 Web 應用程式,以同時返回 JSON 和/或 XML。在許多情況下,這些是您唯一需要的資料格式,並且是支援只讀移動應用程式和/或啟用 AJAX 的網頁的簡單方法,其中 RESTful 請求僅用於 獲取 資料。
對 REST 的全面支援,包括修改資料的能力,涉及結合 HTTP 訊息轉換器使用帶註解的控制器方法。在這種情況下,使用檢視沒有意義,只需返回一個 @ResponseBody 物件,讓轉換器完成工作。