19 августа 2011 г.

Несколько особенностей контекстов Spring MVC или история о невнимательности

Так уж получилось, что последний год я работаю на проектом, который построен с использованием фреймворка Spring MVC. Сейчас я совсем от этого не в восторге, год назад было еще терпимо, а два года назад я даже не видел достойных конкурентов для быстрой и архитектурно правильной разработки. С выходом третьей версии, конечно, стало гораздо комфортнее работать, конфигурировать, но сейчас существуют куда более эффективные и стандартизированные решения. Но сейчас не о них,будем считать эту статью очередной ретроспективой.

Так получилось, что занимаясь конфигурацией (по старой привычке в основном на уровне xml) я не особо задумывался о том, как Spring MVC организует работу с контекстами, а оказалось, что это весьма интересно и понимание этой работы могло бы избавить меня от ряда нудных ошибок...

Контекст задачи

Исторически сложилось, что текущий проект целиком поднимался с разворачиванием самого обычного DispatcherServlet:

<servlet>
    <servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
         <param-name>contextConfigLocation</param-name>
         <param-value>/WEB-INF/spring/*.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

Все было очень просто, все наши репозитории, компоненты, контроллеры, сервисы (@Repository, @Component, @Controller, @Service) поднимались в одном контексте. Из параметра contextConfigLocation также понятно, что каши не было и различные конфигурации были разнесены по разным файлам.

Все было хорошо до тех пор пока неожиданно не понадобилось сделать самый обычный фильтр, который должен был иметь доступ к сессионному бину. Понятно, что сам бин находится под управлением Spring и чтобы получить к нему доступ из фильтра нужно получить контекст диспетчера, из этого контекста получить уже существующий Resolver, а уже из него получить конкретный бин.

Реальность

При оценке задачи все казалось очень просто, но на практике получить контекст диспетчера в обычном фильтре (который не находится под управлением Spring) можно только кривым путем через копание в сессии и обладая сакральными знаниями о имени диспетчера в web.xml. Этот кривой путь был сразу отвергнут. Как самый наивный я решил просто передать фильтр под управление Spring, сделать это очень легко через предоставляемый прокси фильтр:

<filter>
    <filter-name>ExampleFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>ExampleFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Что будет происходить за кулисами при разворачивании этого фильтра? Благодаря такому проксированию наш реальный фильтр будет находится прод управлением Spring, только теперь наш настоящий фильтр должен подниматься в контексте Spring, что я и сделал указав в фильтре аннотацию @Component("ExampleFilter"), собственно по имени компонента произойдет состыковка DelegatingFilterProxy с нашим реальным фильтром:

...
@Component("ExampleFilter")
public class ExampleFilter implements Filter {
...

После таких простых манипуляций фильтр становиться полноценным spring-овым бином, в котором можно использовать инжектинг (@Autowired), автоматическое чтение из файлов настроек и т.д. Но в моем случае такого не произошло и не могло произойти...

Увлекшись самой идеей перенести фильтры под управление Spring я совершенно забыл учесть, что в тот момент, когда Spring связать мой новенький фильтр через прокси, то его попросту нет. Что это значит? Дело в том, что при выше приведенной конфигурации все компоненты поднимаются в контексте самого диспетчера. Напомню, что диспетчеры порождают свой дочерний контекст от корневого контекста приложения, который в данном случае пуст. Когда связывается сам фильтр, то необходимый компонент ищется, конечно, в корневом контексте, ведь о контексте диспетчера фильтр ничего не знает и не должен знать.

Решение

Самым простым и идеологически правильным решением будет разбиение всей конфигурации на два контекста - корневой и контекст диспетчера. В корневом контексте будем разворачивать компоненты, репозитории, сервисы, которые важны для логики всего приложения, а в контексте диспетчера только сами контроллеры, как репрезентативную часть и компоненты и сервисы, которые могут использоваться, только на уровне контроллера (например, каптча). Организуется это примерно следующим образом:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/spring/root/*.xml</param-value>
</context-param>
 
<filter>
    <filter-name>ExampleFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>ExampleFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
 
<servlet>
    <servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
         <param-name>contextConfigLocation</param-name>
         <param-value>/WEB-INF/spring/dispatcher/*.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
 
<servlet-mapping>
    <servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

После такого разбиения все глобальные конфигурации будет браться из конфигов /WEB-INF/spring/root/, а все конфигурации уровня диспетчера из конфигов /WEB-INF/spring/dispatcher/. Стоит обратить внимание, что существует проблема двойной инициализации (вполне возможно, что это уже исправили, но когда-то эта проблема доставляла головной боли), которую нужно контролировать. Поэтому убираем разворачивание контроллеров из корневого контекста и переносим их в контекст диспетчера:

<!-- In ROOT context -->
<context:component-scan base-package="your.package.mvc" >
     <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
...
<!-- In dispatcher context -->
<context:component-scan base-package="your.package.mvc" >
     <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

После таких манипуляций получится следующее:

  • Элементы приложения будут логически разбиты и отделены от репрезентативной части
  • Уровень контроллер будет иметь свой контекст для различных обвесов (например, каптча)
  • Под управление Spring-ом попадают фильтры со всеми отсюда вытекающими.
Необходимо так же помнить, что существует еще инструкция <mvc:annotation-driven/>, про которую не стоит забывать.

Тонкости решения

В описанной истории опустилось, что в проекте так же использовался фреймворк Hibernate, с которым тоже произошел сюрприз...

Каково же было мое удивление, когда после все моих изменений и попытки обратиться к своему контроллеру я получил следующее исключение: org.hibernate.HibernateException: No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional  one here at... Думаю любой разработчик рано или поздно сталкивался таким исключением

Понятно, что дело оказалось в том, что по каким-то причинам перестала открываться транзакция в цепочке контроллер-репозиторий. В данном конкретном проекте управление транзакциями было организованно классически: DataSource-SessionFactory-TransactionManager. И с помощью милой инструкции <tx:annotation-driven mode="proxy" /> управление транзакциями происходило с помощью аннотации @Transactional. 

Оказалось, что данная инструкция ограничивается одним контекстом (!!!). Это достаточно странно, так как контекст диспетчера является дочерним. Собственно, поэтому если вы хотите использовать аннотацию @Transactional и в контроллерах и в компонентах, то данную инструкцию придется дублировать в обоих контекстах.

Заключение

Будьте внимательны, друзья. Помните, что проблемы иногда могут появиться из нечего и превратиться в снежный ком...

Progg it

3 комментария:

  1. @Transactional не используют в контроллерах, это противоречит шаблонам проектирования приложения с использованием spring framework.
    И логически это не правильно.

    ОтветитьУдалить
    Ответы
    1. Это ничему на самом деле не противоречит, нет ни одного реального ограничения. Кроме того, в случае использования "ленивой" дозагрузки данных, это единственное адекватное решение в противовес избыточным моделям.

      Удалить
  2. Спасибо большое! Даже не представляете (а может представляете), как мне помогла ваша статья. Пол недели ковырялся, не мог подключить фильтры к проекту. Благодаря вам приложение опять работает, но с фильтрами.
    Однако, теперь у меня другая проблема. После редиректа фильтра отображается пустая страница, хотя контроллер на этот URI есть и при закомменчивании тела метода doFilter Фильтра, работает.

    ОтветитьУдалить