Первоначально написано Полом Чепменом
В моем предыдущем посте я представил концепцию согласования контента и три стратегии, которые 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
ним никогда не консультируются (его order
2, и мы никогда не добираемся так далеко)
Вот тут и вступает 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 всегда был силен предлагать разработчикам гибкость и выбор. Это не исключение.