Первоначально написано Полом Чепменом
В моем предыдущем посте я представил концепцию согласования контента и три стратегии, которые 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 is now done by the 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>
Для каждого запроса a @Controller обычно возвращает логическое имя представления (или Spring MVC определит его по соглашению из входящего URL). CNVR сверится со всеми другими определителями представлений, определенными в конфигурации, чтобы увидеть 1), имеет ли он представление с правильным именем и 2) если он имеет представление о том, что он также генерирует правильное содержимое — все представления «знают», какое содержимое типа они возвращаются. Желаемый тип контента определяется точно так же, как обсуждалось в предыдущем посте.
Эквивалентную конфигурацию Java смотрите здесь . А для расширенной конфигурации смотрите здесь . На Github есть демонстрационное приложение: https://github.com/paulc4/mvc-content-neg-views .
Для тех из вас, кто спешит, это все в двух словах.
Для остальных из вас этот пост показывает, как мы к этому пришли. Он обсуждает концепцию множественных представлений в Spring MVC и основывается на этой идее, чтобы определить, что такое CNVR, как его использовать и как он работает. Он берет то же приложение «Учетные записи» из предыдущего поста и создает его для возврата информации об учетной записи в HTML, в виде электронной таблицы, в виде JSON и в XML. Все с использованием только просмотров.
ПОЧЕМУ НЕСКОЛЬКО ВИДОВ?
Одной из сильных сторон шаблона MVC является возможность иметь несколько представлений для одних и тех же данных. В Spring MVC мы достигаем этого, используя « Согласование контента» ». В моем предыдущем посте обсуждалось согласование контента в целом и показывались примеры контроллеров RESTful, использующих конвертеры сообщений HTTP. Но согласование контента также можно использовать и с представлениями.
Например, предположим, что я хочу отображать информацию об учетной записи не только как веб-страницу, но и сделать ее доступной в виде электронной таблицы. Я мог бы использовать разные URL для каждого, поместить два метода в мой контроллер Spring и каждый из них возвращал правильный тип представления. (Кстати, если вы не уверены, как Spring может создать электронную таблицу, я покажу вам это позже).
@Controller
class AccountController {
@RequestMapping("/accounts.htm")
public String listAsHtml(Model model, Principal principal) {
model.addAttribute( accountManager.getAccounts(principal) ); // Duplicated logic
return ¨accounts/list¨; // View determined by view-resolution
}
@RequestMapping("/accounts.xls")
public AccountsExcelView listAsXls(Model model, Principal principal) {
model.addAttribute( accountManager.getAccounts(principal) ); // Duplicated logic
return new AccountsExcelView(); // Return view explicitly
}
}
Использование нескольких методов неэлегатно, побеждает паттерн MVC и становится еще страшнее, если я хочу поддерживать и другие форматы данных — такие как PDF, CSV… Если вы помните в предыдущем посте, у нас была похожая проблема, когда один метод возвращал JSON или XML (который мы решили, вернув один @RequestBody объект и выбрав правильный HTTP Message Converter).
Теперь нам нужен интеллектуальный распознаватель видов, который выбирает правильный вид из нескольких возможных видов.
Spring MVC долгое время поддерживал несколько распознавателей представлений и обращается к каждому по очереди, чтобы найти представление. Хотя порядок обращения к распознавателям представлений можно указать, Spring MVC всегда выбирает первое предложенное представление. « Resolver View согласования содержимого » (CNVR) согласовывает все средства разрешения представления, чтобы найти наилучшее соответствие для желаемого формата.
ПРИМЕР ЛИСТИНГА ПОЛЬЗОВАТЕЛЯ
Вот простое приложение для составления списка учетных записей, которое мы будем использовать в качестве нашего рабочего примера для составления списка учетных записей в HTML, в электронной таблице и (позже) в форматах JSON и XML — просто используя представления.
Полный код можно найти на Github: https://github.com/paulc4/mvc-content-neg-views . Это вариант приложения, которое я показал вам в прошлый раз, который использует только представления для генерации вывода. Примечание : для простоты приведенных ниже примеров я использовал JSP напрямую и an 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 a 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 ним никогда не консультируются (его order2, и мы никогда не добираемся так далеко)
Вот тут и вступает CNVR. Давайте быстро рассмотрим, что мы знаем о стратегии выбора контента, обсуждавшейся в предыдущем посте . Запрошенный тип содержимого определяется путем проверки в следующем порядке:
- Суффикс URL (расширение пути) — например,
http://...accounts.jsonчтобы указать формат JSON. - Или параметр URL может быть использован. По умолчанию он называется
format, напримерhttp://...accounts?format=json. - Или
Acceptбудет использоваться свойство заголовка HTTP (что на самом деле определяет, как работает HTTP, но не всегда удобно использовать — особенно когда клиент является браузером).
В первых двух случаях суффикс или значение параметра ( xml, json …) должны быть сопоставлены с правильным MIME-типом. Либо можно использовать среду активации JavaBeans, либо сопоставления можно указать явно. Со Accept свойством header его значением является тип mine.
СОДЕРЖАНИЕ ПЕРЕГОВОРНЫЙ ВИД РЕЗОЛЬВЕР
Это специальный преобразователь представлений, к которому подключена наша стратегия. Вот Конфигурация Java :
@Configuration
@EnableWebMvc
public class MvcConfiguration extends WebMvcConfigurerAdapter {
/**
* Setup a simple strategy:
* 1. Only path extension is taken into account, Accept headers are 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 is now done by the 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>
Это ContentNegotiationManager точно такой же компонент, о котором я говорил в предыдущем посте .
CNVR автоматически переходит к любому другому компоненту средства разрешения представления, определенному для Spring, и запрашивает у него View экземпляр, соответствующий имени представления, возвращаемому контроллером — в этом случае accounts/list. Каждый View «знает», какой контент он может генерировать, потому что в нем есть getContentType() метод (унаследованный от View интерфейса). Страница JSP отрисовывается JstlView (возвращается InternalResourceViewResolver) и имеет тип содержимого — text/htmlпока AccountExcelView генерируется application/vnd.ms-excel.
То, как на самом деле сконфигурирован CNVR, делегируется тому, ContentNegotiationManagerкоторый создается в свою очередь через configurer (конфигурация Java) или один из множества фабричных компонентов Spring (XML).
Последний фрагмент головоломки: как CNVR узнает, какой тип контента был запрошен ? Потому что стратегия согласования контента говорит ей, что делать: либо распознан суффикс URL, либо параметр URL, либо заголовок Accept. Точно такая же настройка стратегии, описанная в предыдущем посте , повторно использованная CNVR.
Обратите внимание, что когда Spring 3.0 представил стратегии согласования контента, они применялись только к выбору Views. Начиная с 3.2, эта возможность доступна по всем направлениям (согласно моему предыдущему посту ). Примеры в этом посте используют Spring 3.2 и могут отличаться от предыдущих примеров, которые вы видели ранее. В частности, большинство свойств для настройки стратегии согласования контента теперь включены,
ContentNegotiationManagerFactoryBeanа не включеныContentNegotiatingViewResolver. Свойства на CNVR теперь устарели в пользу свойств на менеджере, но сама CNVR работает точно так же, как и всегда.
НАСТРОЙКА СОДЕРЖАНИЯ ПЕРЕГОВОРНОЙ ВИД РЕЗОЛЬВЕР
По умолчанию CNVR автоматически обнаруживает все ViewResolvers определенные для Spring и согласовывает между ними. Если вы предпочитаете, у самого 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 the CNVR plugging in the resolvers and the 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 использует 2 набора профилей Spring. В web.xml, вы можете указать xmlили javaconfig для конфигурации XML или Java соответственно. И для любого из них, укажите либо separate или combined. separate Профиль определяет все вид резольверы как бобы верхнего уровня и позволяет CNVR сканировать контекст , чтобы найти их (как описано в предыдущем разделе). В combined профиле преобразователи представления определяются явно, а не как компоненты Spring и передаются в CNVR через его viewResolvers свойство (как показано в этом разделе).
JSON ПОДДЕРЖКА
Spring предоставляет объект, MappingJacksonJsonView который поддерживает генерацию данных JSON из объектов Java с использованием библиотеки отображения объектов Джексона в JSON. MappingJacksonJsonView Автоматически преобразует все атрибуты , найденные в модели в формате 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 the JSON out to be nicely readable
return view;
}
}
Простое объявление этого преобразователя представления как bean-компонента Spring означает, что данные в формате JSON могут быть возвращены. JAF уже json соответствует, application/json так что мы сделали. Теперь URL-адрес http://myserver/myapp/accounts/list.json может возвращать информацию об учетной записи в формате JSON. Вот вывод из нашего приложения учетных записей:
Подробнее об этом View см. В Spring Javadoc .
ПОДДЕРЖКА XML
Существует аналогичный класс для генерации вывода XML — MarshallingView . Он берет первый объект в модели, который можно маршалировать, и обрабатывает его. При желании вы можете настроить представление, указав, какой атрибут (ключ) модели выбрать setModelKey().
Опять же, для этого нам нужен преобразователь представления. Spring поддерживает несколько технологий маршаллинга через абстракцию Spring to Object Marshalling (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 (в ответ на комментарии я добавил пример этого в конец моего предыдущего поста ).
Сконфигурируйте новый преобразователь как бин Spring с помощью Java Configuration:
@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>
Это наша законченная система:
СРАВНЕНИЕ ОТДЕЛЬНЫХ ПОДХОДОВ
Полная поддержка для RESTful подхода с MVC доступна при использовании @ResponseBody, @ResponseStatus и другой связанный с REST MVC аннотаций. Что-то вроде этого:
@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" />
Однако это создает другой стиль метода Controller, преимущество в том, что он также более мощный. Так какой путь: взгляды или @ResponseBody?
Для существующего веб-сайта, уже использующего Spring MVC и представления, MappingJacksonJsonView и MarshallingView предоставляют простой способ расширить веб-приложение для возврата также JSON и / или XML. Во многих случаях они являются единственными данными-форматов вам нужно , и это простой способ поддержки только для чтения мобильных приложений и / или AJAX включен веб-страницы , где RESTful запросы используются только для GET данных.
Полная поддержка REST, в том числе возможность изменения данных, предполагает использование аннотированных методов контроллера в сочетании с конвертерами сообщений HTTP. Использование представлений в этом случае не имеет смысла, просто верните @ResponseBody объект и позвольте конвертеру выполнить свою работу.
Однако, как показано здесь в моем предыдущем посте, для контроллера вполне возможно использовать оба подхода одновременно. Теперь один и тот же контроллер может поддерживать как традиционные веб-приложения, так и реализовывать полноценный интерфейс RESTful, улучшая веб-приложения, которые могли создаваться и развиваться в течение многих лет.
Spring всегда был силен предлагать разработчикам гибкость и выбор. Это не исключение.


