Статьи

Согласование содержимого с помощью Spring MVC


Первоначально написано Полом Чепменом

Пол Чепмен

Существует два способа генерации вывода с использованием Spring MVC:

  • Вы можете использовать @ResponseBody подход RESTful  и конвертеры HTTP-сообщений, как правило, чтобы возвращать форматы данных, такие как JSON или XML. Программные клиенты, мобильные приложения и браузеры с поддержкой AJAX являются обычными клиентами.
  • В качестве альтернативы вы можете использовать  разрешение экрана . Хотя представления вполне способны генерировать JSON и XML, если хотите (подробнее об этом в моем следующем посте), представления обычно используются для создания форматов представления, таких как HTML, для традиционного веб-приложения.
  • На самом деле существует третья возможность — некоторые приложения требуют обоих, и Spring MVC легко поддерживает такие комбинации. Мы вернемся к этому прямо в конце.

В любом случае вам нужно иметь дело с несколькими представлениями (или представлениями) одних и тех же данных, возвращаемых контроллером. Определение формата данных для возврата называется  согласованием содержимого .

Существует три ситуации, когда нам нужно знать, какой тип формата данных отправлять в ответе HTTP:

  • HttpMessageConverters:  определить правильный конвертер для использования.
  • Сопоставление запросов:  сопоставьте входящий HTTP-запрос с разными методами, которые возвращают разные форматы.
  • Разрешение просмотра:  выберите правильный вид для использования.

Определение того, какой формат запрошен пользователем, зависит от  ContentNegotationStrategy. Существуют стандартные реализации, доступные из коробки, но вы также можете реализовать свою собственную, если хотите.

В этой статье я хочу обсудить, как настроить и использовать согласование содержимого со Spring, в основном с точки зрения контроллеров RESTful, использующих конвертеры сообщений HTTP. В следующем  посте  я покажу, как настроить согласование содержимого специально для использования с представлениями, использующими Spring ContentNegotiatingViewResolver.

КАК РАБОТАЕТ ПЕРЕГОВОР КОНТЕНТА?

Переговоры

Получение правильного контента

Делая запрос через HTTP, вы можете указать, какой тип ответа вы хотите получить, установив  Acceptсвойство заголовка. Веб-браузеры имеют эту предустановку для запроса HTML (среди прочего). На самом деле, если вы посмотрите, вы увидите, что браузеры на самом деле посылают очень запутанные заголовки Accept, что делает их использование непрактичным. См. Http://www.gethifi.com/blog/browser-rest-http-accept-headers  для хорошего обсуждения этой проблемы. Итог: Accept заголовки перепутаны, и вы также не можете их изменить (если вы не используете JavaScript и AJAX).

Таким образом, для тех ситуаций, когда  Accept свойство заголовка нежелательно, Spring предлагает вместо этого некоторые соглашения. (Это было одним из приятных изменений в Spring 3.2, сделавшим гибкую стратегию выбора контента доступной для всех Spring MVC, а не только при использовании представлений). Вы можете настроить стратегию согласования контента один раз, и она будет применяться везде, где необходимо определить различные форматы (типы мультимедиа).

ВКЛЮЧЕНИЕ ПЕРЕГОВОРНОГО КОНТЕНТА В ВЕСНА 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 которая реализует  РРА стратегии  (путь расширения, то параметр, то заголовок 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 стратегия может быть настроена с использованием методов фабричного компонента:

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

Что мы сделали, в обоих случаях:

  • Отключено расширение пути. Обратите внимание, что услуга не означает использование одного подхода в предпочтении другого, она просто включает или отключает его. Порядок проверки всегда — расширение пути, параметр, заголовок Accept.
  • Включите использование параметра URL, но вместо этого formatмы будем использовать  параметр по умолчанию mediaType .
  • Accept Полностью игнорировать  заголовок Часто это лучший подход, если большинство ваших клиентов на самом деле являются веб-браузерами (как правило, делают вызовы REST через AJAX).
  • Не используйте 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 и Джексон на моем пути к классам, Spring MVC автоматически настроит необходимое  HttpMessageConverters. Классы моего домена также должны быть помечены аннотациями JAXB2 и Jackson, чтобы разрешить преобразование (в противном случае преобразователи сообщений не знают, что делать). В ответ на комментарии (ниже) аннотированный  Account класс показан  ниже .

Вот вывод JSON из нашего приложения Аккаунтов (обратите внимание на расширение пути в URL).

Как система узнает, нужно ли конвертировать в XML или JSON? Из-за согласования содержимого — любой из трех вариантов ( Стратегия PPA ), описанных выше, будет использоваться в зависимости от того, как  ContentNegotiationManager настроен. В этом случае URL заканчивается,  accounts.json потому что расширение пути является единственной включенной стратегией.

В примере кода вы можете переключаться между XML или Java-конфигурацией MVC, устанавливая активный профиль в  web.xml. Профили «xml» и «javaconfig» соответственно.

КОМБИНИРОВАНИЕ ДАННЫХ И ФОРМАТЫ ПРЕЗЕНТАЦИИ

Поддержка REST в Spring MVC основана на существующей платформе MVC Controller. Таким образом, возможно, чтобы одни и те же веб-приложения возвращали информацию как в виде необработанных данных (например, 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 метод обрабатывает весь доступ к данным и интеграцию с нижележащим уровнем сервиса (the  AccountManager). Второй метод вызывает первый и устанавливает ответ в модели для использования представлением. Это позволяет избежать дублирования логики.

Чтобы определить, какой из двух  @RequestMapping методов выбрать, мы снова используем нашу стратегию согласования контента PPA. Это позволяет  produces опции работать. URL-адреса, оканчивающиеся на accounts.xml или  accounts.json отображающие первый метод, любые другие URL-адреса, оканчивающиеся accounts.anything на второй.

ДРУГОЙ ПОДХОД

В качестве альтернативы мы могли бы сделать все это одним способом, если бы использовали представления для генерации всех возможных типов контента. Это то, где  ContentNegotiatingViewResolver приходит, и это будет темой моего следующего  поста .

ACKNOWELDGEMENTS

Я хотел бы поблагодарить  Россена Стоянчева  за помощь в написании этого поста. Любые ошибки мои.

ДОБАВЛЕНИЕ: АННОТИРОВАННЫЙ УЧЕТНЫЙ КЛАСС

Добавлено 2 июня 2013 .

Поскольку возникли некоторые вопросы о том, как аннотировать класс для JAXB, здесь есть часть класса Account. Для краткости я опустил данные-члены и все методы, кроме аннотированных геттеров. Я мог бы аннотировать элементы данных напрямую, если предпочитал (точно так же, как аннотации JPA на самом деле). Помните, что Джексон может маршалировать объекты в 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 ...

}