使用 Spring MVC 進行內容協商

工程 | Paul Chapman | 2013 年 5 月 11 日 | ...

使用 Spring MVC 有兩種產生輸出的方式

  • 您可以使用 RESTful @ResponseBody 方法和 HTTP 訊息轉換器,通常是為了回傳像 JSON 或 XML 的資料格式。程式化的客戶端、行動應用程式和啟用 AJAX 的瀏覽器是常見的客戶端。
  • 或者,您可以使用視圖解析。雖然視圖完全可以產生 JSON 和 XML(我將在下一篇文章中詳細介紹),但視圖通常用於為傳統的 Web 應用程式產生像 HTML 這樣的展示格式。
  • 實際上,還有第三種可能性 - 一些應用程式需要兩者,而 Spring MVC 可以輕鬆地支援這樣的組合。我們將在最後回到這一點。

在任何一種情況下,您都需要處理控制器傳回的相同資料的多種表示形式(或視圖)。找出要回傳的資料格式稱為內容協商

在三種情況下,我們需要知道要在 HTTP 回應中傳送哪種類型的資料格式

  • HttpMessageConverters: 確定要使用的正確轉換器。
  • Request Mappings: 將傳入的 HTTP 請求對應到傳回不同格式的不同方法。
  • View Resolution: 選擇要使用的正確視圖。

確定使用者請求的格式依賴於 ContentNegotationStrategy。預設情況下有一些可用的預設實作,但如果您願意,也可以實作自己的實作。

在這篇文章中,我想討論如何使用 Spring 配置和使用內容協商,主要是在使用 HTTP 訊息轉換器的 RESTful 控制器方面。在稍後的文章中,我將展示如何專門為使用 Spring 的 ContentNegotiatingViewResolver 的視圖設定內容協商。

內容協商如何運作?

[caption id="attachment_13288" align="alignleft" width="200" caption="取得正確的內容"]Negotiation[/caption]

通過 HTTP 發出請求時,可以通過設定 Accept 標頭屬性來指定您想要的響應類型。 Web 瀏覽器已預先設定為請求 HTML(以及其他內容)。實際上,如果您查看,您會發現瀏覽器實際上會發送非常令人困惑的 Accept 標頭,這使得依賴它們變得不切實際。請參閱 http://www.gethifi.com/blog/browser-rest-http-accept-headers 以獲得對此問題的良好討論。底線:Accept 標頭搞砸了,通常你也不能更改它們(除非你使用 JavaScript 和 AJAX)。

因此,對於那些 Accept 標頭屬性不理想的情況,Spring 提供了一些慣例來代替使用。(這是 Spring 3.2 中的一個很好的變化,它使靈活的內容選擇策略在所有 Spring MVC 中可用,而不僅僅是在使用視圖時)。您可以集中配置一次內容協商策略,它將應用於需要確定不同格式(媒體類型)的任何地方。

在 Spring MVC 中啟用內容協商

Spring 支援幾種用於選擇所需格式的約定:URL 後綴和/或 URL 參數。 這些與使用 Accept 標頭一起使用。 因此,可以通過以下三種方式中的任何一種請求內容類型。 預設情況下,會按此順序檢查它們

  • 在 URL 中添加路徑擴展名(後綴)。 因此,如果傳入的 URL 類似於 http://myserver/myapp/accounts/list.html,則需要 HTML。 對於試算表,URL 應為 http://myserver/myapp/accounts/list.xls。 後綴到媒體類型映射是通過 *JavaBeans Activation Framework* 或 JAF 自動定義的(因此 activation.jar 必須在類別路徑上)。
  • 像這樣的 URL 參數: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>

我們在兩種情況下都做了什麼

  • 禁用了路徑擴展名。 請注意,favor 並不意味著優先使用一種方法而不是另一種方法,它只是啟用或禁用它。 檢查順序始終為路徑擴展名、參數、Accept 標頭。
  • 啟用 URL 參數的使用,但我們將使用 mediaType 而不是使用預設參數 format
  • 完全忽略 Accept 標頭。 如果您的大多數客戶端實際上是 Web 瀏覽器(通常通過 AJAX 進行 REST 調用),這通常是最佳方法。
  • 不要使用 JAF,而是手動指定媒體類型映射 - 我們只希望支援 JSON 和 XML。

列出使用者帳戶示例

為了演示,我已經編寫了一個簡單的帳戶清單應用程式作為我們的工作示例 - 螢幕截圖顯示了 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? 由於內容協商 - 上面討論的三個(*PPA 策略*)選項中的任何一個都將根據 ContentNegotiationManager 的配置方式使用。 在這種情況下,URL 以 accounts.json 結尾,因為路徑擴展名是唯一啟用的策略。

在示例程式碼中,您可以通過在 web.xml 中設定活動配置文件來在 XML 或 Java 配置的 MVC 之間切換。 配置文件分別為“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.xmlaccounts.json 結尾的 URL 對應於第一種方法,而任何以 accounts.anything 結尾的其他 URL 都對應於第二種方法。

另一種方法

或者,如果我們使用視圖來生成所有可能的內容類型,我們可以用一種方法完成所有操作。 這就是 ContentNegotiatingViewResolver 的用武之地,這將是我下一篇文章的主題。

致謝

我要感謝 Rossen Stoyanchev 在撰寫這篇文章時提供的幫助。 任何錯誤都是我自己的。

附錄:帶註釋的 Account 類別

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 ...

}

取得 Spring 電子報

透過 Spring 電子報保持聯繫

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進展。

瞭解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

瞭解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

查看全部