Статьи

Согласование содержимого с использованием представлений


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


Пол Чепмен

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

В первых двух случаях суффикс или значение параметра ( xmljson …) должны быть сопоставлены с правильным 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 или  combinedseparate Профиль определяет все вид резольверы как бобы верхнего уровня и позволяет 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 всегда был силен предлагать разработчикам гибкость и выбор. Это не исключение.