邁向成功
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 profile。在 web.xml
中,你可以分別指定 xml
或 javaconfig
用於 XML 或 Java 配置。對於它們中的任何一個,可以指定 separate
或 combined
。 separate
profile 將所有檢視解析器定義為頂級 bean,並讓 CNVR 掃描上下文來查詢它們(如前一節所述)。在 combined
profile 中,檢視解析器是顯式定義的,而不是作為 Spring bean,並透過其 viewResolvers
屬性傳遞給 CNVR(如本節所示)。
MappingJacksonJsonView
,它支援使用 Jackson 物件到 JSON 對映庫從 Java 物件生成 JSON 資料。 MappingJacksonJsonView
會自動將 Model 中找到的所有屬性轉換為 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。它獲取模型中第一個可以被序列化的物件並對其進行處理。你可以選擇透過指定要選取哪個 Model 屬性(鍵)來配置檢視 - 參見 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" />
然而,這會產生不同風格的控制器方法,其優點在於它也更強大。那麼該選擇哪種方式呢:檢視還是 @ResponseBody
?
對於已經使用 Spring MVC 和檢視的現有網站,MappingJacksonJsonView
和 MarshallingView
提供了一種簡便的方法來擴充套件 Web 應用程式,使其也能返回 JSON 和/或 XML。 在許多情況下,這些是你唯一需要的資料格式,並且是支援只讀移動應用程式和/或啟用 AJAX 的網頁的一種簡便方法,在這些應用中,RESTful 請求僅用於獲取資料。
對 REST 的全面支援,包括修改資料的能力,涉及使用帶註解的控制器方法並結合 HTTP 訊息轉換器。在這種情況下使用檢視沒有意義,只需返回一個 @ResponseBody
物件,讓轉換器完成工作即可。
然而,如我在上一篇文章中此處所示,一個控制器同時使用這兩種方法是完全可能的。現在,同一個控制器既可以支援傳統的 Web 應用程式,也可以實現完整的 RESTful 介面,從而增強那些可能已構建和開發多年的 Web 應用程式。
Spring 一直以來在為開發者提供靈活性和選擇方面表現出色。這也不例外。