Первоначально написано Полом Чепменом
Существует два способа генерации вывода с использованием 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 ... }