Так уж получилось, что последний год я работаю на проектом, который построен с использованием фреймворка Spring MVC. Сейчас я совсем от этого не в восторге, год назад было еще терпимо, а два года назад я даже не видел достойных конкурентов для быстрой и архитектурно правильной разработки. С выходом третьей версии, конечно, стало гораздо комфортнее работать, конфигурировать, но сейчас существуют куда более эффективные и стандартизированные решения. Но сейчас не о них,будем считать эту статью очередной ретроспективой.
Так получилось, что занимаясь конфигурацией (по старой привычке в основном на уровне xml) я не особо задумывался о том, как Spring MVC организует работу с контекстами, а оказалось, что это весьма интересно и понимание этой работы могло бы избавить меня от ряда нудных ошибок...
Контекст задачи
Исторически сложилось, что текущий проект целиком поднимался с разворачиванием самого обычного DispatcherServlet:
Все было очень просто, все наши репозитории, компоненты, контроллеры, сервисы (@Repository, @Component, @Controller, @Service) поднимались в одном контексте. Из параметра contextConfigLocation также понятно, что каши не было и различные конфигурации были разнесены по разным файлам.
Все было хорошо до тех пор пока неожиданно не понадобилось сделать самый обычный фильтр, который должен был иметь доступ к сессионному бину. Понятно, что сам бин находится под управлением Spring и чтобы получить к нему доступ из фильтра нужно получить контекст диспетчера, из этого контекста получить уже существующий Resolver, а уже из него получить конкретный бин.
Реальность
При оценке задачи все казалось очень просто, но на практике получить контекст диспетчера в обычном фильтре (который не находится под управлением Spring) можно только кривым путем через копание в сессии и обладая сакральными знаниями о имени диспетчера в web.xml. Этот кривой путь был сразу отвергнут. Как самый наивный я решил просто передать фильтр под управление Spring, сделать это очень легко через предоставляемый прокси фильтр:
Что будет происходить за кулисами при разворачивании этого фильтра? Благодаря такому проксированию наш реальный фильтр будет находится прод управлением Spring, только теперь наш настоящий фильтр должен подниматься в контексте Spring, что я и сделал указав в фильтре аннотацию @Component("ExampleFilter"), собственно по имени компонента произойдет состыковка DelegatingFilterProxy с нашим реальным фильтром:
После таких простых манипуляций фильтр становиться полноценным spring-овым бином, в котором можно использовать инжектинг (@Autowired), автоматическое чтение из файлов настроек и т.д. Но в моем случае такого не произошло и не могло произойти...
Увлекшись самой идеей перенести фильтры под управление Spring я совершенно забыл учесть, что в тот момент, когда Spring связать мой новенький фильтр через прокси, то его попросту нет. Что это значит? Дело в том, что при выше приведенной конфигурации все компоненты поднимаются в контексте самого диспетчера. Напомню, что диспетчеры порождают свой дочерний контекст от корневого контекста приложения, который в данном случае пуст. Когда связывается сам фильтр, то необходимый компонент ищется, конечно, в корневом контексте, ведь о контексте диспетчера фильтр ничего не знает и не должен знать.
Решение
Самым простым и идеологически правильным решением будет разбиение всей конфигурации на два контекста - корневой и контекст диспетчера. В корневом контексте будем разворачивать компоненты, репозитории, сервисы, которые важны для логики всего приложения, а в контексте диспетчера только сами контроллеры, как репрезентативную часть и компоненты и сервисы, которые могут использоваться, только на уровне контроллера (например, каптча). Организуется это примерно следующим образом:
После такого разбиения все глобальные конфигурации будет браться из конфигов /WEB-INF/spring/root/, а все конфигурации уровня диспетчера из конфигов /WEB-INF/spring/dispatcher/. Стоит обратить внимание, что существует проблема двойной инициализации (вполне возможно, что это уже исправили, но когда-то эта проблема доставляла головной боли), которую нужно контролировать. Поэтому убираем разворачивание контроллеров из корневого контекста и переносим их в контекст диспетчера:
После таких манипуляций получится следующее:
Так получилось, что занимаясь конфигурацией (по старой привычке в основном на уровне 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 и в контроллерах и в компонентах, то данную инструкцию придется дублировать в обоих контекстах.
Заключение
Будьте внимательны, друзья. Помните, что проблемы иногда могут появиться из нечего и превратиться в снежный ком...
@Transactional не используют в контроллерах, это противоречит шаблонам проектирования приложения с использованием spring framework.
ОтветитьУдалитьИ логически это не правильно.
Это ничему на самом деле не противоречит, нет ни одного реального ограничения. Кроме того, в случае использования "ленивой" дозагрузки данных, это единственное адекватное решение в противовес избыточным моделям.
УдалитьСпасибо большое! Даже не представляете (а может представляете), как мне помогла ваша статья. Пол недели ковырялся, не мог подключить фильтры к проекту. Благодаря вам приложение опять работает, но с фильтрами.
ОтветитьУдалитьОднако, теперь у меня другая проблема. После редиректа фильтра отображается пустая страница, хотя контроллер на этот URI есть и при закомменчивании тела метода doFilter Фильтра, работает.