Статьи

REST с Spring — ContentNegotiatingViewResolver против HttpMessageConverter + ResponseBody Annotation

Отказ от ответственности: эта публикация в блоге отражает опыт, накопленный при обучении внедрению сервисов RESTful. Таким образом, некоторые элементы моего блога могут показаться нам неправильными. Поэтому, если вы столкнетесь с ошибками, пожалуйста, дайте мне знать, и я опубликую исправления как можно скорее. Пожалуйста, продолжайте с осторожностью …

Как я уже писал в своем предыдущем блоге «Как сделать DevNexus.com более спокойным «, я нахожусь в процессе создания большего количества данных с DevNexus.com, используемых другими службами, путем предоставления конечных точек на основе JSon и XML , Веб-сайт / приложение реализовано с использованием Spring MVC 3.0 в слое представления с использованием поддержки REST Spring.

Вот несколько хороших чтений, с которыми я столкнулся, которые предоставляют полезную информацию:

  1. http://blog.springsource.com/2009/03/08/rest-in-spring-3-mvc/
  2. http://blog.springsource.com/2009/03/16/adding-an-atom-view-to-an-application-using-springs-rest-support/
  3. http://rwehner.wordpress.com/2010/06/09/2-ways-to-create-json-response-for-ajax-request-in-spring3/
  4. http://www.ibm.com/developerworks/web/library/wa-restful/
  5. http://forum.springframework.org/showthread.php?p=337206
  6. http://static.springsource.org/spring/docs/3.0.x/reference/mvc.html

Оказывается, есть два способа реализации конечных точек REST:

Использование ContentNegotiatingViewResolver

Когда вы используете ContentNegotiatingViewResolver, ваши веб-контроллеры возвращают ModelAndViews или имена представлений, а ContentNegotiatingViewResolver на основе различных критериев выбирает правильную стратегию представления данных.

При этом наивысший приоритет имеет расширение файла, которое используется, если доступно в запросе. Затем ViewResolver будет искать (определяемый) параметр запроса, который идентифицирует представление. Если это не помогает, ViewResolver использует платформу Java Activation Framework для определения типа контента. Если все не удается, используйте заголовок HTTP Accept. Конечно шаги могут быть индивидуально отключены.

Тем не менее, ключевым моментом является то, что ваши контроллеры будут возвращать одно ModelAndView / Viewname, которое будет преобразовано в конкретное представление, такое как:

  • org.springframework.web.servlet.view.json.MappingJacksonJsonView
  • org.springframework.web.servlet.view.xml.MarshallingView 
  • org.springframework.web.servlet.view.documentClass.AbstractPdfView
  • etc...

Однако это может показаться немного неестественным для определенных представлений данных, таких как XML (с использованием аннотаций Jaxb) или Json (с использованием Джексона), где отдельное представление может не потребоваться. К счастью, вы можете настроить ContentNegotiatingViewResolver на использование представлений по умолчанию, что решает проблему.

Использование HttpMessageConverters

Вот где HttpMessageConverters потенциально может помочь. Каждый раз, когда вы используете аннотацию @ResponseBody, вы будете использовать HttpMessageConverter (см. Также справочную документацию Spring, глава «15.3.2.5 Отображение тела запроса с аннотацией @RequestBody» и «18.3.2 Преобразование сообщений HTTP»).

Это означает, что вместо возврата ModelAndView или имени представления вы фактически вернете данные, например, коллекцию объектов.

Если вы используете Spring Context  <mvc: annotation-driven />, тогда поддержка XML и JSON-маршаллинга активируется по умолчанию, при условии, что соответствующие библиотеки классов (Jaxb и / или Jackson) присутствуют в вашем пути к классам (см. Документацию Spring). подробности см. в главе «15.12.1 mvc: на основе аннотаций»)

Вот интересная проблема: если вы используете <mvc: annotation-driven />, вы можете установить дополнительные службы преобразования, но вы не можете установить дополнительные HttpMessageConverters. Следовательно, если вы хотите сделать это, вы должны использовать явные объявления bean-компонентов и удалить тег <mvc: annotation-driven /> из вашего контекста. Это было не слишком хорошо освещено в статье [6] http://www.ibm.com/developerworks/web/library/wa-restful/

В конечном итоге я использую следующее объявление bean-компонента вместо тега <mvc: annotation-driven />   :

     <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="webBindingInitializer">
            <bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
                <property name="conversionService" ref="conversionService"/>
                <property name="validator" ref="validator"/>
            </bean>
        </property>
       <property name="messageConverters">
           <list>
               <ref bean="jsonConverter" />
               <ref bean="marshallingConverter" />
               <ref bean="atomConverter" />
           </list>
       </property>
    </bean>

Мой выбор

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

Одна проблема, с которой я столкнулся, заключалась в том, что для моего приложения я могу возвращать как чистые представления данных (Json, XML), так и ответы Html / Jsp. Каким-то образом я не смог легко + аккуратно настроить свой контроллер, чтобы ответить на один и тот же URL-адрес несколькими методами контроллера (один из них использовал @ResponseBody, а другой возвращал представление JSP)

Во-вторых, я хотел также поддерживать расширения файлов, чтобы использовать правильный вид или конвертер. Однако оказывается, что HttpMessageConverters не поддерживают это — хотя где-то был пример использования параметров запроса. Но такой подход потребовал бы от меня создания дополнительных пользовательских классов …

В любом случае, я выбрал подход ContentNegotiatingViewResolver для реализации моих услуг Restful.

Дальнейшие проблемы

Как только я принял решение, казалось, все прошло гладко. Я быстро внедрил свои сервисы, и они отлично работали в Firefox. Вот пример предполагаемой структуры URL:

http://www.devnexus.com/s/speakers

http://www.devnexus.com/s/speakers.html

http://www.devnexus.com/s/speakers.xml

http://www.devnexus.com/s/speakers.json

После этого я решил охватить все основы: поддержка расширений файлов, а также разрешить клиентам подключаться к http://www.devnexus.com/s/speakers и извлекать все представления данных, используя соответствующий заголовок Http Accept.

Я развернул приложение в производство, но на следующий день на ежемесячном собрании группы пользователей в Атланте — люди сообщили мне, что сайт DevNexus не работает. Это было странно, так как я зашел на сайт за несколько минут до встречи.

Ну, как оказалось, Google Chrome, Safari и Internet Explorer передают дикую смесь заголовков Http Accept. Следовательно, когда пользователи обращались к http://www.devnexus.com/s/index, сервер пытался вернуть представление XML, поскольку Chrome и Safari запрашивали данные XML, а не данные HTML.

Для получения более подробной информации о заголовках Accept, пожалуйста, смотрите следующее увлекательное:

http://www.gethifi.com/blog/browser-rest-http-accept-headers

Что также меня оттолкнуло, так это то, что довольно много источников, в т.ч. Документация Spring подразумевает, что использование заголовка Http Accept может быть действительно эффективным способом определения правильного представления для возврата клиентам.

Чтобы проиллюстрировать хаос — Вот интересная публикация из списка рассылки webkit:

https://lists.webkit.org/pipermail/webkit-dev/2010-January/011188.html

В конечном счете, я настроил свой контекст сервлета так, как мне кажется, лучше всего подходит для меня. Url без расширения теперь всегда будет возвращать Html, а для других представлений данных расширение файла обязательно. Я также настроил ContentNegotiatingViewResolver на игнорирование заголовка Http Accept, установив:

<property name="ignoreAcceptHeader" value="true" />

Большое спасибо за эту публикацию в блоге Рика Херрика. Таким образом, теперь мой файл servlet-context.xml содержит:

    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="order" value="1" />
        <property name="ignoreAcceptHeader" value="true" />
        <property name="mediaTypes">
            <map>
                <entry key="xml"  value="application/xml"/>
                <entry key="json" value="application/json"/>
            </map>
        </property>
        <property name="defaultViews">
            <list>
                <bean class="org.springframework.web.servlet.view.xml.MarshallingView">
                    <property name="marshaller" ref="jaxbMarshaller"/>
                </bean>
                <bean
                    class="org.springframework.web.servlet.view.json.MappingJacksonJsonView">
                    <property name="objectMapper" ref="jaxbJacksonObjectMapper"/>
                </bean>
            </list>
        </property>
    </bean>

    <bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
        <property name="order" value="2"/>
    </bean> 
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
        <property name="order"  value="3"/>
    </bean>

Это был довольно долгий день, чтобы все заработало как задумано, но, тем не менее, это был хороший учебный опыт. Конечно, в чистых URL-адресах есть что-то интригующее.

 

От http://hillert.blogspot.com/2011/01/rest-with-spring-contentnegotiatingview.html