Рубрики
Без рубрики

Весенние веб-контексты

Узнайте об общих способах настройки и организации контекстов приложений в веб-приложении Spring.

Автор оригинала: Alessio Stalla.

1. введение

При использовании Spring в веб-приложении у нас есть несколько вариантов организации контекстов приложений, которые связывают все это.

В этой статье мы проанализируем и объясним наиболее распространенные варианты, которые предлагает Spring.

2. Контекст Корневого Веб-Приложения

Каждое веб-приложение Spring имеет связанный контекст приложения, связанный с его жизненным циклом: контекст корневого веб-приложения.

Это старая функция, которая предшествовала Spring Web MVC, поэтому она не привязана конкретно к какой-либо технологии веб-фреймворка.

Контекст запускается при запуске приложения и уничтожается при остановке благодаря прослушивателю контекста сервлета. Наиболее распространенные типы контекстов также могут быть обновлены во время выполнения, хотя не все реализации ApplicationContext имеют такую возможность.

Контекст в веб-приложении всегда является экземпляром WebApplicationContext . Это интерфейс, расширяющий ApplicationContext с контрактом на доступ к ServletContext .

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

2.1. ContextLoaderListener

Корневой webapplicationcontext , описанный в предыдущем разделе, управляется прослушивателем класса org.springframework.web.context.ContextLoaderListener , который является частью модуля spring-web .

По умолчанию прослушиватель загрузит контекст приложения XML из /WEB-INF/applicationContext.xml . Однако эти значения по умолчанию могут быть изменены. Например, мы можем использовать аннотации Java вместо XML.

Мы можем настроить этот прослушиватель либо в дескрипторе веб-приложения ( web.xml файл) или программно в средах Servlet 3.x.

В следующих разделах мы подробно рассмотрим каждый из этих вариантов.

2.2. Использование web.xml и XML ApplicationContext

При использовании web.xml , мы настраиваем прослушиватель как обычно:


    
        org.springframework.web.context.ContextLoaderListener
    

Мы можем указать альтернативное расположение конфигурации контекста XML с помощью параметра contextConfigLocation :


    contextConfigLocation
    /WEB-INF/rootApplicationContext.xml

Или несколько мест, разделенных запятыми:


    contextConfigLocation
    /WEB-INF/context1.xml, /WEB-INF/context2.xml

Мы даже можем использовать шаблоны:


    contextConfigLocation
    /WEB-INF/*-context.xml

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

2.3. Использование web.xml и контекст приложения Java

Мы также можем указать другие типы контекстов, помимо контекста на основе XML по умолчанию. Давайте посмотрим, например, как вместо этого использовать конфигурацию аннотаций Java.

Мы используем параметр contextClass , чтобы указать слушателю, какой тип контекста следует создать:


    contextClass
    
        org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    

Каждый тип контекста может иметь расположение конфигурации по умолчанию. В нашем случае AnnotationConfigWebApplicationContext не имеет такового, поэтому мы должны его предоставить.

Таким образом, мы можем перечислить один или несколько аннотированных классов:


    contextConfigLocation
    
        com.baeldung.contexts.config.RootApplicationConfig,
        com.baeldung.contexts.config.NormalWebAppConfig
    

Или мы можем указать контексту сканировать один или несколько пакетов:


    contextConfigLocation
    com.baeldung.bean.config

И, конечно, мы можем смешивать и сочетать эти два варианта.

2.4. Программная Конфигурация С Сервлетом 3.x

Версия 3 API сервлета произвела настройку через web.xml файл полностью необязателен. Библиотеки могут предоставлять свои веб-фрагменты, которые представляют собой части конфигурации XML, которые могут регистрировать прослушиватели, фильтры, сервлеты и так далее.

Кроме того, пользователи имеют доступ к API, который позволяет программно определять каждый элемент приложения на основе сервлетов.

Модуль spring-web использует эти функции и предлагает свой API для регистрации компонентов приложения при его запуске.

Spring сканирует путь к классу приложения на наличие экземпляров класса org.springframework.web.WebApplicationInitializer . Это интерфейс с одним методом, void OnStartup(ServletContext ServletContext) вызывает исключение 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();

Затем, во второй строке, мы сообщаем контексту, откуда загружать определения его компонентов. Опять же, 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 настроен по крайней мере один DispatcherServlet (но, возможно, и не один, мы поговорим об этом случае позже). Это сервлет, который получает входящие запросы, отправляет их соответствующему методу контроллера и возвращает представление.

Каждый DispatcherServlet имеет связанный контекст приложения. Бобы, определенные в таких контекстах, настраивают сервлет и определяют объекты MVC, такие как контроллеры и распознаватели представлений.

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

3.1. Использование web.xml и XML ApplicationContext

DispatcherServlet обычно объявляется в web.xml с именем и отображением:


    normal-webapp
    
        org.springframework.web.servlet.DispatcherServlet
    
    1


    normal-webapp
    /api/*

Если не указано иное, имя сервлета используется для определения загружаемого XML-файла. В нашем примере мы будем использовать файл WEB-INF/normal-webapp-servlet.xml .

Мы также можем указать один или несколько путей к XML-файлам, аналогично ContextLoaderListener :


    ...
    
        contextConfigLocation
        /WEB-INF/normal/*.xml
    

3.2. Использование web.xml и контекст приложения Java

Когда мы хотим использовать другой тип контекста , мы снова действуем, как с ContextLoaderListener . То есть мы указываем класс контекста параметр вместе с подходящим contextConfigLocation :


    normal-webapp-annotations
    
        org.springframework.web.servlet.DispatcherServlet
    
    
        contextClass
        
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        
    
    
        contextConfigLocation
        com.baeldung.contexts.config.NormalWebAppConfig
    
    1

3.3. Использование сервлета 3.x и контекста приложения XML

Опять же, мы рассмотрим два различных метода для программного объявления DispatcherServlet , и мы применим один из них к контексту XML, а другой-к контексту Java.

Итак, давайте начнем с общего WebApplicationInitializer и контекста приложения XML.

Как мы видели ранее, мы должны реализовать метод OnStartup . Однако на этот раз мы также создадим и зарегистрируем dispatcherservlet:

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 .

Это абстрактный класс, который, помимо создания корневого webapplicationcontext, как было показано ранее, позволяет нам зарегистрировать один сервлет диспетчера с минимальным шаблоном:

@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. Родительский и дочерний контексты

До сих пор мы видели два основных типа контекстов: контекст корневого веб-приложения и контекст сервлета диспетчера. Тогда у нас может возникнуть вопрос: связаны ли эти контексты?

Оказывается, да, это так. Фактически, корневой контекст является родительским для каждого контекста dispatcherservlet. Таким образом, компоненты, определенные в корневом webapplicationcontext, видны каждому контексту dispatcherservlet, но не наоборот.

Таким образом, как правило, корневой контекст используется для определения компонентов службы, в то время как контекст диспетчера содержит те компоненты, которые конкретно связаны с 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.baeldung.contexts.services;

@Service
public class GreeterService {
    @Resource
    private Greeting greeting;
    
    public String greet() {
        return greeting.getMessage();
    }
}

Мы объявим службу в корневом webapplicationcontext, используя сканирование компонентов:

@Configuration
@ComponentScan(basePackages = { "com.baeldung.contexts.services" })
public class RootApplicationConfig {
    //...
}

Вместо этого мы могли бы предпочесть XML:

5.2. Контроллеры

Давайте определим два простых контроллера, которые используют службу и выводят приветствие:

package com.baeldung.contexts.normal;

@Controller
public class HelloWorldController {

    @Autowired
    private GreeterService greeterService;
    
    @RequestMapping(path = "/welcome")
    public ModelAndView helloWorld() {
        String message = "

Normal " + greeterService.greet() + "

"; return new ModelAndView("welcome", "message", message); } } //"Secure" Controller package com.baeldung.contexts.secure; String message = "

Secure " + greeterService.greet() + "

";

Как мы видим, контроллеры лежат в двух разных пакетах и печатают разные сообщения: один говорит “нормально”, другой “безопасно”.

5.3. Контексты сервлета Диспетчера

Как мы уже говорили ранее, у нас будет два разных контекста dispatcherservlet, по одному для каждого контроллера. Итак, давайте определим их на Java:

//Normal context
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.baeldung.contexts.normal" })
public class NormalWebAppConfig implements WebMvcConfigurer {
    //...
}

//"Secure" context
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.baeldung.contexts.secure" })
public class SecureWebAppConfig implements WebMvcConfigurer {
    //...
}

Или, если мы предпочитаем, в XML:





5.4. Собрать Все Это Воедино

Теперь, когда у нас есть все детали, нам просто нужно сказать Спрингу, чтобы он их подключил. Напомним, что нам нужно загрузить корневой контекст и определить два сервлета диспетчера. Хотя мы видели несколько способов сделать это, теперь мы сосредоточимся на двух сценариях: 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 , опять же, просто объединив части, которые мы обсуждали до сих пор.

Определение контекста корневого приложения:


    
        org.springframework.web.context.ContextLoaderListener
    

“Нормальный” контекст диспетчера:


    normal-webapp
    
        org.springframework.web.servlet.DispatcherServlet
    
    1


    normal-webapp
    /api/*

И, наконец, “безопасный” контекст:


    secure-webapp
    
        org.springframework.web.servlet.DispatcherServlet
    
    1


    secure-webapp
    /s/api/*

6. Объединение Нескольких Контекстов

Есть и другие способы, кроме родительско-дочерних, объединить несколько конфигурационных местоположений, разделить большие контексты и лучше разделить различные проблемы. Мы уже видели один пример: когда мы указываем contextConfigLocation с несколькими путями или пакетами, Spring создает единый контекст, объединяя все определения компонентов, как если бы они были написаны в одном XML-файле или классе Java, по порядку.

Однако мы можем достичь аналогичного эффекта другими средствами и даже использовать разные подходы вместе. Давайте рассмотрим наши варианты.

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

6.1. Импорт Контекста В Другой

В качестве альтернативы мы можем импортировать определение контекста в другое. В зависимости от сценария у нас есть разные виды импорта.

Импорт класса @Configuration в Java:

@Configuration
@Import(SomeOtherConfiguration.class)
public class Config { ... }

Загрузка какого-либо другого типа ресурса, например определения контекста XML, в Java:

@Configuration
@ImportResource("classpath:basicConfigForPropertiesTwo.xml")
public class Config { ... }

Наконец, включение XML-файла в другой файл:

Таким образом, у нас есть много способов организовать службы, компоненты, контроллеры и т. Д., Которые сотрудничают для создания нашего потрясающего приложения. И самое приятное, что идеи понимают их все!

7. Веб-приложения для весенней загрузки

Spring Boot автоматически настраивает компоненты приложения, поэтому, как правило, меньше нужно думать о том, как их организовать.

Тем не менее, под капотом Boot использует пружинные функции, в том числе те, которые мы видели до сих пор. Давайте рассмотрим несколько примечательных различий.

Веб-приложения Spring Boot, работающие во встроенном контейнере не запускают WebApplicationInitializer по дизайну.

Если это необходимо, мы можем написать ту же логику в SpringBootServletInitializer или ServletContextInitializer вместо этого, в зависимости от выбранной стратегии развертывания.

Однако для добавления сервлетов, фильтров и прослушивателей, как показано в этой статье, это не обязательно. На самом деле Spring Boot автоматически регистрирует каждый компонент, связанный с сервлетом, в контейнере:

@Bean
public Servlet myServlet() { ... }

Определенные таким образом объекты сопоставляются в соответствии с соглашениями: фильтры автоматически сопоставляются с/*, то есть с каждым запросом. Если мы регистрируем один сервлет, он сопоставляется с/, в противном случае каждый сервлет сопоставляется с его именем компонента.

Если вышеуказанные соглашения не работают для нас, мы можем определить FilterRegistrationBean , ServletRegistrationBean, или ServletListenerRegistrationBean вместо этого. Эти классы позволяют нам контролировать тонкие аспекты регистрации.

8. Выводы

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

Мы упустили некоторые функции, в частности поддержку общего контекста в корпоративных приложениях , которая на момент написания статьи все еще отсутствует в Spring 5 .

Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub .