1. Введение
При использовании Spring в веб-приложении у нас есть несколько вариантов организации контекстов приложения, которые связывают все это.
В этой статье мы собираемся проанализировать и объяснить наиболее распространенные варианты, которые предлагает Spring.
2. Контекст корневого веб-приложения
Каждое веб-приложение Spring имеет связанный с ним контекст приложения, связанный с его жизненным циклом: корневой контекст веб-приложения.
Это старая функция, предшествовавшая Spring Web MVC, поэтому она не привязана конкретно к какой-либо технологии веб-фреймворка.
Контекст запускается при запуске приложения и уничтожается при его остановке благодаря слушателю контекста сервлета. Наиболее распространенные типы контекстов также могут обновляться во время выполнения, хотя не все реализации ApplicationContext
имеют эту возможность.
Контекст в веб-приложении всегда является экземпляром WebApplicationContext
. Это интерфейс, расширяющий ApplicationContext
контрактом на доступ к ServletContext
.
В любом случае, приложения обычно не должны беспокоиться об этих деталях реализации: контекст корневого веб-приложения — это просто централизованное место для определения общих компонентов.
2.1. Слушатель ContextLoader
Контекст корневого веб-приложения, описанный в предыдущем разделе, управляется прослушивателем класса org.springframework.web.context.ContextLoaderListener
, который является частью модуля spring-web .
По умолчанию прослушиватель загружает контекст приложения XML из /WEB-INF/applicationContext.xml
.
Однако эти значения по умолчанию можно изменить. Например, мы можем использовать аннотации Java вместо XML.
Мы можем настроить этот прослушиватель либо в дескрипторе веб-приложения ( файл web.xml
), либо программно в средах Servlet 3.x.
В следующих разделах мы подробно рассмотрим каждый из этих вариантов.
2.2. Использование web.xml
и контекста приложения XML
При использовании web.xml
мы настраиваем прослушиватель как обычно:
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
Мы можем указать альтернативное расположение конфигурации контекста XML с помощью параметра contextConfigLocation
:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/rootApplicationContext.xml</param-value>
</context-param>
Или несколько мест, разделенных запятыми:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/context1.xml, /WEB-INF/context2.xml</param-value>
</context-param>
Мы даже можем использовать шаблоны:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/*-context.xml</param-value>
</context-param>
В любом случае определяется только один контекст путем объединения всех определений bean-компонентов, загруженных из указанных местоположений.
2.3. Использование web.xml
и контекста приложения Java
Мы также можем указать другие типы контекстов, кроме контекста по умолчанию на основе XML. Давайте посмотрим, например, как вместо этого использовать конфигурацию аннотаций Java.
Мы используем параметр contextClass
, чтобы сообщить слушателю, какой тип контекста создавать:
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
Каждый тип контекста может иметь расположение конфигурации по умолчанию. В нашем случае у AnnotationConfigWebApplicationContext
его нет, поэтому мы должны его предоставить.
Таким образом, мы можем перечислить один или несколько аннотированных классов:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
com.foreach.contexts.config.RootApplicationConfig,
com.foreach.contexts.config.NormalWebAppConfig
</param-value>
</context-param>
Или мы можем указать контексту сканировать один или несколько пакетов:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.foreach.bean.config</param-value>
</context-param>
И, конечно же, мы можем смешивать и сочетать два варианта.
2.4. Программная конфигурация с сервлетом 3.x
Версия 3 Servlet API сделала настройку через файл web.xml
совершенно необязательной. Библиотеки могут предоставлять свои веб-фрагменты, представляющие собой части XML-конфигурации, которые могут регистрировать прослушиватели, фильтры, сервлеты и т. д.
Кроме того, у пользователей есть доступ к API, который позволяет программно определять каждый элемент приложения на основе сервлета.
Модуль spring-web
использует эти функции и предлагает свой API для регистрации компонентов приложения при его запуске.
Spring сканирует путь к классам приложения на наличие экземпляров класса org.springframework.web.WebApplicationInitializer
. Это интерфейс с одним методом, void onStartup(ServletContext servletContext) throws ServletException
, который вызывается при запуске приложения.
Давайте теперь посмотрим, как мы можем использовать это средство для создания таких же типов корневых контекстов веб-приложений, которые мы видели ранее.
2.5. Использование сервлета 3.x и контекста приложения XML
Начнем с контекста XML, как в разделе 2.2.
Мы реализуем вышеупомянутый метод onStartup
:
public class ApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext)
throws ServletException {
//...
}
}
Разберем реализацию построчно.
Сначала мы создаем корневой контекст. Поскольку мы хотим использовать XML, это должен быть контекст приложения на основе XML, а поскольку мы находимся в веб-среде, он также должен реализовывать WebApplicationContext
.
Таким образом, первая строка — это явная версия параметра contextClass
, с которой мы столкнулись ранее, с помощью которой мы решаем, какую конкретную реализацию контекста использовать:
XmlWebApplicationContext rootContext = new XmlWebApplicationContext();
Затем во второй строке мы сообщаем контексту, откуда загружать его определения bean-компонентов. Опять же, setConfigLocations
— это программный аналог параметра contextConfigLocation
в web.xml
:
rootContext.setConfigLocations("/WEB-INF/rootApplicationContext.xml");
Наконец, мы создаем ContextLoaderListener
с корневым контекстом и регистрируем его в контейнере сервлета. Как мы видим, ContextLoaderListener
имеет соответствующий конструктор, который принимает WebApplicationContext
и делает его доступным для приложения:
servletContext.addListener(new ContextLoaderListener(rootContext));
2.6. Использование сервлета 3.x и контекста приложения Java
Если мы хотим использовать контекст на основе аннотаций, мы можем изменить фрагмент кода в предыдущем разделе, чтобы он вместо этого создавал экземпляр AnnotationConfigWebApplicationContext .
Однако давайте рассмотрим более специализированный подход для получения того же результата.
Класс WebApplicationInitializer
, который мы видели ранее, представляет собой интерфейс общего назначения. Оказывается, Spring предоставляет еще несколько конкретных реализаций, в том числе абстрактный класс с именем AbstractContextLoaderInitializer
.
Его работа, как следует из названия, заключается в создании ContextLoaderListener
и регистрации его в контейнере сервлетов.
Нам нужно только указать ему, как построить корневой контекст:
public class AnnotationsBasedApplicationInitializer
extends AbstractContextLoaderInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
AnnotationConfigWebApplicationContext rootContext
= new AnnotationConfigWebApplicationContext();
rootContext.register(RootApplicationConfig.class);
return rootContext;
}
}
Здесь мы видим, что нам больше не нужно регистрировать ContextLoaderListener
, что избавляет нас от небольшого количества шаблонного кода.
Обратите также внимание на использование метода register
, характерного для AnnotationConfigWebApplicationContext
, вместо более общего setConfigLocations
: вызывая его, мы можем зарегистрировать отдельные аннотированные классы @Configuration
в контексте, избегая, таким образом, сканирования пакетов.
3. Контексты диспетчерского сервлета
Давайте теперь сосредоточимся на другом типе контекста приложения. На этот раз мы будем ссылаться на функцию, специфичную для Spring MVC, а не на часть общей поддержки веб-приложений Spring.
Приложения Spring MVC имеют по крайней мере один настроенный сервлет Dispatcher (но, возможно, более одного, мы поговорим об этом случае позже). Это сервлет, который получает входящие запросы, отправляет их соответствующему методу контроллера и возвращает представление.
Каждый DispatcherServlet
имеет связанный с ним контекст приложения. Бины, определенные в таких контекстах, настраивают сервлет и определяют объекты MVC, такие как контроллеры и преобразователи представлений.
Давайте сначала посмотрим, как настроить контекст сервлета. Мы рассмотрим некоторые подробные детали позже.
3.1. Использование web.xml
и контекста приложения XML
DispatcherServlet
обычно объявляется в web.xml
с именем и отображением:
<servlet>
<servlet-name>normal-webapp</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>normal-webapp</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
Если не указано иное, имя сервлета используется для определения XML-файла для загрузки. В нашем примере мы будем использовать файл WEB-INF/normal-webapp-servlet.xml
.
Мы также можем указать один или несколько путей к XML-файлам аналогично ContextLoaderListener
:
<servlet>
...
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/normal/*.xml</param-value>
</init-param>
</servlet>
3.2. Использование web.xml
и контекста приложения Java
Когда мы хотим использовать другой тип контекста, мы снова действуем, как с ContextLoaderListener
. То есть мы указываем параметр contextClass
вместе с подходящим contextConfigLocation
:
<servlet>
<servlet-name>normal-webapp-annotations</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.foreach.contexts.config.NormalWebAppConfig</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
3.3. Использование сервлета 3.x и контекста приложения XML
Опять же, мы рассмотрим два разных метода программного объявления DispatcherServlet
и применим один к контексту XML, а другой — к контексту Java.
Итак, давайте начнем с универсального WebApplicationInitializer
и контекста приложения XML.
Как мы видели ранее, нам нужно реализовать метод onStartup
. Однако на этот раз мы также создадим и зарегистрируем сервлет диспетчера:
XmlWebApplicationContext normalWebAppContext = new XmlWebApplicationContext();
normalWebAppContext.setConfigLocation("/WEB-INF/normal-webapp-servlet.xml");
ServletRegistration.Dynamic normal
= servletContext.addServlet("normal-webapp",
new DispatcherServlet(normalWebAppContext));
normal.setLoadOnStartup(1);
normal.addMapping("/api/*");
Мы можем легко провести параллель между приведенным выше кодом и эквивалентными элементами конфигурации web.xml .
3.4. Использование сервлета 3.x и контекста приложения Java
На этот раз мы настроим контекст на основе аннотаций, используя специализированную реализацию WebApplicationInitializer
: AbstractDispatcherServletInitializer
.
Это абстрактный класс, который, помимо создания корневого контекста веб-приложения, как было показано ранее, позволяет нам зарегистрировать один диспетчерский сервлет с минимальным шаблоном:
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext secureWebAppContext
= new AnnotationConfigWebApplicationContext();
secureWebAppContext.register(SecureWebAppConfig.class);
return secureWebAppContext;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/s/api/*" };
}
Здесь мы видим метод создания контекста, связанного с сервлетом, точно так же, как мы видели ранее для корневого контекста. Кроме того, у нас есть метод для указания сопоставлений сервлета, как в web.xml
.
4. Родительский и дочерний контексты
До сих пор мы видели два основных типа контекстов: контекст корневого веб-приложения и контексты сервлета диспетчера. Тогда у нас может возникнуть вопрос: связаны ли эти контексты?
Оказывается, да, они есть. Фактически, корневой контекст является родительским для каждого контекста сервлета диспетчера. Таким образом, bean-компоненты, определенные в корневом контексте веб-приложения, видны каждому контексту сервлета диспетчера, но не наоборот.
Таким образом, обычно корневой контекст используется для определения сервисных компонентов, в то время как контекст диспетчера содержит те компоненты, которые конкретно связаны с MVC.
Обратите внимание, что мы также видели способы программного создания контекста сервлета диспетчера. Если мы вручную установим его родителя, то Spring не отменит наше решение, и этот раздел больше не применяется.
В более простых приложениях MVC достаточно иметь один контекст, связанный только с одним сервлетом-диспетчером. Нет необходимости в слишком сложных решениях!
Тем не менее, отношения родитель-потомок становятся полезными, когда у нас настроено несколько сервлетов-диспетчеров. Но когда мы должны заботиться о том, чтобы иметь более одного?
Как правило, мы объявляем несколько сервлетов-диспетчеров , когда нам нужно несколько наборов конфигурации MVC. Например, у нас может быть REST API вместе с традиционным приложением MVC или незащищенным и безопасным разделом веб-сайта:
Примечание: когда мы расширяем AbstractDispatcherServletInitializer
(см. раздел 3.4), мы регистрируем как корневой контекст веб-приложения, так и один сервлет-диспетчер.
Итак, если нам нужно более одного сервлета, нам нужно несколько реализаций AbstractDispatcherServletInitializer
. Однако мы можем определить только один корневой контекст, иначе приложение не запустится.
К счастью, метод createRootApplicationContext
может возвращать значение null
. Таким образом, у нас может быть одна реализация AbstractContextLoaderInitializer
и множество реализаций AbstractDispatcherServletInitializer
, которые не создают корневой контекст. В таком случае рекомендуется явно заказывать инициализаторы с помощью @Order
.
Также обратите внимание, что AbstractDispatcherServletInitializer
регистрирует сервлет под заданным именем ( dispatcher
) и, конечно же, у нас не может быть нескольких сервлетов с одинаковыми именами. Итак, нам нужно переопределить getServletName
:
@Override
protected String getServletName() {
return "another-dispatcher";
}
5. Пример родительского и дочернего контекста ****
Предположим, что у нас есть две области нашего приложения, например общедоступная, доступная всему миру, и защищенная, с разными конфигурациями MVC. Здесь мы просто определим два контроллера, которые выводят разные сообщения.
Кроме того, предположим, что некоторым контроллерам требуется служба, которая содержит значительные ресурсы; повсеместным случаем является настойчивость. Затем мы захотим создать экземпляр этой службы только один раз, чтобы избежать двойного использования ресурсов, а также потому, что мы верим в принцип «Не повторяйся»!
Давайте теперь продолжим пример.
5.1. Общая служба
В нашем примере hello world мы остановились на более простой службе приветствия вместо постоянства:
package com.foreach.contexts.services;
@Service
public class GreeterService {
@Resource
private Greeting greeting;
public String greet() {
return greeting.getMessage();
}
}
Мы объявим службу в контексте корневого веб-приложения, используя сканирование компонентов:
@Configuration
@ComponentScan(basePackages = { "com.foreach.contexts.services" })
public class RootApplicationConfig {
//...
}
Вместо этого мы могли бы предпочесть XML:
<context:component-scan base-package="com.foreach.contexts.services" />
5.2. Контроллеры
Давайте определим два простых контроллера, которые используют сервис и выводят приветствие:
package com.foreach.contexts.normal;
@Controller
public class HelloWorldController {
@Autowired
private GreeterService greeterService;
@RequestMapping(path = "/welcome")
public ModelAndView helloWorld() {
String message = "<h3>Normal " + greeterService.greet() + "</h3>";
return new ModelAndView("welcome", "message", message);
}
}
//"Secure" Controller
package com.foreach.contexts.secure;
String message = "<h3>Secure " + greeterService.greet() + "</h3>";
Как видим, контроллеры лежат в двух разных упаковках и выводят разные сообщения: на одном написано «нормально», на другом «безопасно».
5.3. Контексты диспетчерского сервлета
Как мы уже говорили ранее, у нас будет два разных контекста диспетчерского сервлета, по одному для каждого контроллера. Итак, давайте определим их в Java:
//Normal context
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.foreach.contexts.normal" })
public class NormalWebAppConfig implements WebMvcConfigurer {
//...
}
//"Secure" context
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.foreach.contexts.secure" })
public class SecureWebAppConfig implements WebMvcConfigurer {
//...
}
Или, если угодно, в XML:
<!-- normal-webapp-servlet.xml -->
<context:component-scan base-package="com.foreach.contexts.normal" />
<!-- secure-webapp-servlet.xml -->
<context:component-scan base-package="com.foreach.contexts.secure" />
5.4. Собираем все вместе
Теперь, когда у нас есть все части, нам просто нужно сказать Spring подключить их. Напомним, что нам нужно загрузить корневой контекст и определить два сервлета-диспетчера. Хотя мы видели несколько способов сделать это, сейчас мы сосредоточимся на двух сценариях: на Java и на XML. Начнем с Java.
Мы определим AbstractContextLoaderInitializer
для загрузки корневого контекста:
@Override
protected WebApplicationContext createRootApplicationContext() {
AnnotationConfigWebApplicationContext rootContext
= new AnnotationConfigWebApplicationContext();
rootContext.register(RootApplicationConfig.class);
return rootContext;
}
Затем нам нужно создать два сервлета, поэтому мы определим два подкласса AbstractDispatcherServletInitializer
. Во-первых, «нормальный»:
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext normalWebAppContext
= new AnnotationConfigWebApplicationContext();
normalWebAppContext.register(NormalWebAppConfig.class);
return normalWebAppContext;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/api/*" };
}
@Override
protected String getServletName() {
return "normal-dispatcher";
}
Затем «безопасный», который загружает другой контекст и сопоставляется с другим путем:
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext secureWebAppContext
= new AnnotationConfigWebApplicationContext();
secureWebAppContext.register(SecureWebAppConfig.class);
return secureWebAppContext;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/s/api/*" };
}
@Override
protected String getServletName() {
return "secure-dispatcher";
}
И мы закончили! Мы только что применили то, что коснулись в предыдущих разделах.
Мы можем сделать то же самое с web.xml
, опять же, просто объединив части, которые мы обсуждали до сих пор.
Определите корневой контекст приложения:
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
«Нормальный» контекст диспетчера:
<servlet>
<servlet-name>normal-webapp</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>normal-webapp</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
И, наконец, «безопасный» контекст:
<servlet>
<servlet-name>secure-webapp</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>secure-webapp</servlet-name>
<url-pattern>/s/api/*</url-pattern>
</servlet-mapping>
6. Объединение нескольких контекстов
Существуют и другие способы, кроме родительско-дочерних, для объединения нескольких местоположений конфигурации, разделения больших контекстов и лучшего разделения различных проблем. Мы уже видели один пример: когда мы указываем contextConfigLocation
с несколькими путями или пакетами, Spring создает единый контекст, объединяя все определения bean-компонентов, как если бы они были записаны в одном XML-файле или классе Java, по порядку.
Однако мы можем добиться аналогичного эффекта другими средствами и даже использовать разные подходы вместе. Давайте рассмотрим наши варианты.
Одной из возможностей является сканирование компонентов, о котором мы расскажем в другой статье .
6.1. Импорт контекста в другой
В качестве альтернативы мы можем сделать так, чтобы определение контекста импортировало другое. В зависимости от сценария у нас есть разные виды импорта.
Импорт класса @Configuration
в Java:
@Configuration
@Import(SomeOtherConfiguration.class)
public class Config { ... }
Загрузка какого-либо другого типа ресурса, например определения контекста XML, в Java:
@Configuration
@ImportResource("classpath:basicConfigForPropertiesTwo.xml")
public class Config { ... }
Наконец, включение XML-файла в другой:
<import resource="greeting.xml" />
Таким образом, у нас есть много способов организовать сервисы, компоненты, контроллеры и т. д., которые взаимодействуют при создании нашего замечательного приложения. И хорошо, что IDE понимают их все!
7. Веб-приложения Spring Boot
Spring Boot автоматически настраивает компоненты приложения, поэтому, как правило, меньше нужно думать о том, как их организовать.
Тем не менее, под капотом Boot использует функции Spring, в том числе те, которые мы видели до сих пор. Давайте посмотрим на пару заслуживающих внимания отличий.
Веб-приложения Spring Boot, работающие во встроенном контейнере , изначально не запускают WebApplicationInitializer
.
Если необходимо, мы можем вместо этого написать ту же логику в SpringBootServletInitializer
или ServletContextInitializer
, в зависимости от выбранной стратегии развертывания.
Однако для добавления сервлетов, фильтров и прослушивателей, как показано в этой статье, этого делать не нужно. Фактически, Spring Boot автоматически регистрирует каждый bean-компонент, связанный с сервлетом, в контейнере:
@Bean
public Servlet myServlet() { ... }
Определенные таким образом объекты сопоставляются в соответствии с соглашениями: фильтры автоматически сопоставляются с /*, то есть с каждым запросом. Если мы регистрируем один сервлет, он сопоставляется с /, в противном случае каждый сервлет сопоставляется со своим именем компонента.
Если приведенные выше соглашения не работают для нас, мы можем вместо этого определить FilterRegistrationBean
, ServletRegistrationBean
или ServletListenerRegistrationBean
. Эти классы позволяют нам контролировать тонкие аспекты регистрации.
8. Выводы
В этой статье мы подробно рассмотрели различные варианты, доступные для структурирования и организации веб-приложения Spring.
Мы упустили некоторые функции, в частности поддержку общего контекста в корпоративных приложениях , которая на момент написания статьи все еще отсутствовала в Spring 5 .
Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub .