領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多使用 Spring MVC 生成輸出有兩種方式
@ResponseBody 方法和 HTTP 訊息轉換器,通常用於返回 JSON 或 XML 等資料格式。程式客戶端、移動應用程式和啟用 AJAX 的瀏覽器是常見的客戶端。無論哪種情況,您都需要處理控制器返回的相同資料的多種表示形式(或檢視)。確定要返回哪種資料格式稱為內容協商。
有三種情況我們需要知道要在 HTTP 響應中傳送哪種資料格式
確定使用者請求的格式依賴於 ContentNegotationStrategy。開箱即用提供了預設實現,但您也可以根據需要實現自己的。
在這篇文章中,我想討論如何使用Spring配置和使用內容協商,主要是在使用HTTP訊息轉換器的RESTful控制器方面。在稍後的一篇文章中,我將展示如何專門為使用Spring的ContentNegotiatingViewResolver檢視設定內容協商。
[caption id="attachment_13288" align="alignleft" width="200" caption="獲取正確內容"]
[/caption]
透過HTTP發出請求時,可以透過設定Accept頭屬性來指定您想要的響應型別。網路瀏覽器預設此屬性以請求HTML(以及其他內容)。事實上,如果您檢視,您會發現瀏覽器實際上傳送了非常令人困惑的Accept頭,這使得依賴它們不切實際。請參閱http://www.gethifi.com/blog/browser-rest-http-accept-headers以獲取關於此問題的一個很好的討論。底線:Accept頭是混亂的,通常您也無法更改它們(除非您使用JavaScript和AJAX)。
因此,對於Accept頭屬性不理想的情況,Spring提供了一些約定來替代使用。(這是Spring 3.2中一個很好的變化,它在所有Spring MVC中都提供了靈活的內容選擇策略,而不僅僅是在使用檢視時)。您可以一次集中配置內容協商策略,它將在需要確定不同格式(媒體型別)的任何地方應用。
Spring支援幾種用於選擇所需格式的約定:URL字尾和/或URL引數。它們與Accept頭的使用並行工作。因此,可以透過三種方式請求內容型別。預設情況下,它們按此順序檢查
http://myserver/myapp/accounts/list.html,則需要HTML。對於電子表格,URL應該是http://myserver/myapp/accounts/list.xls。字尾到媒體型別的對映是透過JavaBeans Activation Framework或JAF自動定義的(因此activation.jar必須在類路徑上)。http://myserver/myapp/accounts/list?format=xls。引數的名稱預設為format,但可以更改。預設情況下,使用引數是停用的,但啟用後,它會第二個被檢查。Accept HTTP頭屬性。這就是HTTP實際定義的工作方式,但是,如前所述,使用它可能會有問題。設定此功能的Java配置如下所示。只需透過其配置器自定義預定義的內容協商管理器即可。請注意,MediaType輔助類預定義了大多數知名媒體型別的常量。
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
/**
* Setup a simple strategy: use all the defaults and return XML by default when not sure.
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_XML);
}
}
使用XML配置時,內容協商策略最容易透過ContentNegotiationManagerFactoryBean進行設定。
<!--
Setup a simple strategy:
1. Take all the defaults.
2. Return XML by default when not sure.
-->
<bean id="contentNegotiationManager"
class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="defaultContentType" value="application/xml" />
</bean>
<!-- Make this available across all of Spring MVC -->
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" />
透過任何一種設定建立的ContentNegotiationManager都是ContentNegotationStrategy的實現,它實現了上面描述的PPA策略(路徑擴充套件,然後是引數,然後是Accept頭)。
在Java配置中,可以透過配置器上的方法完全自定義策略。
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
/**
* Total customization - see below for explanation.
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false).
favorParameter(true).
parameterName("mediaType").
ignoreAcceptHeader(true).
useJaf(false).
defaultContentType(MediaType.APPLICATION_JSON).
mediaType("xml", MediaType.APPLICATION_XML).
mediaType("json", MediaType.APPLICATION_JSON);
}
}
在XML中,可以使用工廠bean上的方法配置策略。
<!-- Total customization - see below for explanation. -->
<bean id="contentNegotiationManager"
class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="favorPathExtension" value="false" />
<property name="favorParameter" value="true" />
<property name="parameterName" value="mediaType" />
<property name="ignoreAcceptHeader" value="true"/>
<property name="useJaf" value="false"/>
<property name="defaultContentType" value="application/json" />
<property name="mediaTypes">
<map>
<entry key="json" value="application/json" />
<entry key="xml" value="application/xml" />
</map>
</property>
</bean>
在兩種情況下我們都做了什麼
format,而是使用mediaType。Accept頭。如果您的多數客戶端實際上是網路瀏覽器(通常透過AJAX進行REST呼叫),這通常是最好的方法。為了演示,我將一個簡單的賬戶列表應用程式作為我們的示例——截圖顯示了HTML格式的典型賬戶列表。完整程式碼可以在Github上找到:https://github.com/paulc4/mvc-content-neg。
要以 JSON 或 XML 格式返回賬戶列表,我需要一個像這樣的控制器。我們暫時忽略生成 HTML 的方法。
@Controller
class AccountController {
@RequestMapping(value="/accounts", method=RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
public @ResponseBody List<Account> list(Model model, Principal principal) {
return accountManager.getAccounts(principal) );
}
// Other methods ...
}
這是內容協商策略的設定
<!-- Simple strategy: only path extension is taken into account -->
<bean id="cnManager"
class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="favorPathExtension" value="true"/>
<property name="ignoreAcceptHeader" value="true" />
<property name="defaultContentType" value="text/html" />
<property name="useJaf" value="false"/>
<property name="mediaTypes">
<map>
<entry key="html" value="text/html" />
<entry key="json" value="application/json" />
<entry key="xml" value="application/xml" />
</map>
</property>
</bean>
或者,使用Java配置,程式碼看起來像這樣
@Override
public void configureContentNegotiation(
ContentNegotiationConfigurer configurer) {
// Simple strategy: only path extension is taken into account
configurer.favorPathExtension(true).
ignoreAcceptHeader(true).
useJaf(false).
defaultContentType(MediaType.TEXT_HTML).
mediaType("html", MediaType.TEXT_HTML).
mediaType("xml", MediaType.APPLICATION_XML).
mediaType("json", MediaType.APPLICATION_JSON);
}
只要我的類路徑上有JAXB2和Jackson,Spring MVC就會自動設定必要的HttpMessageConverters。我的領域類也必須用JAXB2和Jackson註解標記,以啟用轉換(否則訊息轉換器不知道該做什麼)。根據下面的評論,帶註解的Account類顯示在下方。
這是我們的Accounts應用程式的JSON輸出(請注意URL中的路徑擴充套件)。
系統如何知道是轉換為XML還是JSON?這是因為內容協商——根據ContentNegotiationManager的配置方式,上面討論的三種(PPA策略)選項中的任何一種都將被使用。在這種情況下,URL以accounts.json結尾,因為路徑擴充套件是唯一啟用的策略。
在示例程式碼中,您可以透過在web.xml中設定活動配置檔案來在MVC的XML或Java配置之間切換。配置檔案分別為“xml”和“javaconfig”。
Spring MVC的REST支援建立在現有MVC控制器框架之上。因此,同一個Web應用程式既可以返回原始資料(如JSON),也可以使用演示格式(如HTML)。
這兩種技術可以在同一個控制器中輕鬆地並行使用,就像這樣
@Controller
class AccountController {
// RESTful method
@RequestMapping(value="/accounts", produces={"application/xml", "application/json"})
@ResponseStatus(HttpStatus.OK)
public @ResponseBody List<Account> listWithMarshalling(Principal principal) {
return accountManager.getAccounts(principal);
}
// View-based method
@RequestMapping("/accounts")
public String listWithView(Model model, Principal principal) {
// Call RESTful method to avoid repeating account lookup logic
model.addAttribute( listWithMarshalling(principal) );
// Return the view to use for rendering the response
return ¨accounts/list¨;
}
}
這裡有一個簡單的模式:@ResponseBody方法處理所有資料訪問以及與底層服務層(AccountManager)的整合。第二種方法呼叫第一種方法,並在Model中設定響應以供View使用。這避免了重複的邏輯。
為了確定選擇哪種@RequestMapping方法,我們再次使用PPA內容協商策略。它允許produces選項工作。以accounts.xml或accounts.json結尾的URL對映到第一個方法,任何其他以accounts.anything結尾的URL對映到第二個方法。
ContentNegotiatingViewResolver發揮作用的地方,這將是我下一篇文章的主題。我要感謝Rossen Stoyanchev在撰寫此文時提供的幫助。如有任何錯誤,均由我本人承擔。
2013年6月2日增補.
由於有一些關於如何為JAXB註解類的問題,這裡是Account類的一部分。為簡潔起見,我省略了資料成員和除了帶註解的getter之外的所有方法。如果願意,我可以直接註解資料成員(就像JPA註解一樣)。請記住,Jackson可以使用這些相同的註解將物件編組為JSON。
/**
* Represents an account for a member of a financial institution. An account has
* zero or more {@link Transaction}s and belongs to a {@link Customer}. An aggregate entity.
*/
@Entity
@Table(name = "T_ACCOUNT")
@XmlRootElement
public class Account {
// data-members omitted ...
public Account(Customer owner, String number, String type) {
this.owner = owner;
this.number = number;
this.type = type;
}
/**
* Returns the number used to uniquely identify this account.
*/
@XmlAttribute
public String getNumber() {
return number;
}
/**
* Get the account type.
*
* @return One of "CREDIT", "SAVINGS", "CHECK".
*/
@XmlAttribute
public String getType() {
return type;
}
/**
* Get the credit-card, if any, associated with this account.
*
* @return The credit-card number or null if there isn't one.
*/
@XmlAttribute
public String getCreditCardNumber() {
return StringUtils.hasText(creditCardNumber) ? creditCardNumber : null;
}
/**
* Get the balance of this account in local currency.
*
* @return Current account balance.
*/
@XmlAttribute
public MonetaryAmount getBalance() {
return balance;
}
/**
* Returns a single account transaction. Callers should not attempt to hold
* on or modify the returned object. This method should only be used
* transitively; for example, called to facilitate reporting or testing.
*
* @param name
* the name of the transaction account e.g "Fred Smith"
* @return the beneficiary object
*/
@XmlElement // Make these a nested <transactions> element
public Set<Transaction> getTransactions() {
return transactions;
}
// Setters and other methods ...
}