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

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

1. Вступление

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

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

2. Корневой контекст веб-приложения

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

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

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

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

В любом случае, приложения обычно не должны беспокоиться об этих деталях реализации:the root web application context is simply a centralized place to define shared beans.

2.1. ContextLoaderListener

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

By default, the listener will load an XML application context from /WEB-INF/applicationContext.xml. Однако эти значения по умолчанию можно изменить. Например, мы можем использовать аннотации Java вместо XML.

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

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

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

При использовании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

В любом случаеonly one context is defined, путем объединения всех определений bean-компонентов, загруженных из указанных мест.

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

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

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


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

Every type of context may have a default configuration location. В нашем случае уAnnotationConfigWebApplicationContext его нет, поэтому мы должны его предоставить.

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


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

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


    contextConfigLocation
    org.example.bean.config

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

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

Version 3 of the Servlet API has made configuration through the web.xml file completely optional. Библиотеки могут предоставлять свои веб-фрагменты, которые представляют собой части конфигурации XML, которые могут регистрировать слушателей, фильтры, сервлеты и т. д.

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

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

Spring сканирует путь к классам приложения на наличие экземпляров классаorg.springframework.web.WebApplicationInitializer. Это интерфейс с единственным методомvoid onStartup(ServletContext servletContext) throws ServletException, который вызывается при запуске приложения.

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

2.5. Использование Servlet 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. Использование Servlet 3.x и контекста приложения Java

Если мы хотим использовать контекст на основе аннотаций, мы могли бы изменить фрагмент кода в предыдущем разделе, чтобы вместо этого он создавал экземплярAnnotationConfigWebApplicationContext.

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

The WebApplicationInitializer class that we’ve seen earlier is a general-purpose interface. Оказывается, 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 applications have at least one Dispatcher Servlet configured (но, возможно, более одного, мы поговорим об этом случае позже). Это сервлет, который получает входящие запросы, отправляет их соответствующему методу контроллера и возвращает представление.

Each DispatcherServlet has an associated application context. Компоненты, определенные в таких контекстах, настраивают сервлет и определяют объекты MVC, такие как контроллеры и преобразователи представлений.

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

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

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. То есть мы указываем параметрcontextClass вместе с подходящимcontextConfigLocation:


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

3.3. Использование Servlet 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. Использование Servlet 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. Родительский и дочерний контексты

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

Оказывается, да, они есть. Фактически,the root context is the parent of every dispatcher servlet context. Таким образом, bean-компоненты, определенные в контексте корневого веб-приложения, видны каждому контексту сервлета диспетчера, но не наоборот.

Итак, как правило, корневой контекст используется для определения сервисных компонентов, а контекст диспетчера содержит те компоненты, которые конкретно связаны с MVC.

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

В более простых приложениях MVC достаточно иметь один контекст, связанный только с одним сервлетом диспетчера. Нет необходимости в слишком сложных решениях!

Тем не менее, отношения родитель-потомок становятся полезными, когда у нас настроено несколько сервлетов-диспетчеров. Но когда мы должны иметь более одного?

В общем,we declare multiple dispatcher servletswhen we need multiple sets of MVC configuration.. Например, у нас может быть REST API наряду с традиционным приложением MVC или незащищенным и безопасным разделом веб-сайта:

image

Примечание: когда мы расширяем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 мы остановились на более простом сервисе greeter вместо постоянства:

package com.example.contexts.services;

@Service
public class GreeterService {
    @Resource
    private Greeting greeting;

    public String greet() {
        return greeting.getMessage();
    }
}

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

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

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

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

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

package com.example.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.example.contexts.secure; String message = "

Secure " + greeterService.greet() + "

";

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

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

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

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

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

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





5.4. Собираем все вместе

Теперь, когда у нас есть все части, нам просто нужно сказать Spring, чтобы соединить их. Напомним, что нам нужно загрузить корневой контекст и определить два сервлета диспетчера. Хотя мы видели несколько способов сделать это, теперь мы сосредоточимся на двух сценариях: Java и XML. Let’s start with 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";
}

Готово! Мы только что применили то, что коснулись в предыдущих разделах.

We can do the same with 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. Объединение нескольких контекстов

Есть и другие способы, помимо родительско-дочерних, для объединения нескольких конфигурационных местоположений,to split big contexts and better separate different concerns.. Мы уже видели один пример: когда мы указываемcontextConfigLocation с несколькими путями или пакетами, Spring создает единый контекст, объединяя все определения bean-компонентов, как если бы они были записаны в одном XML-файле или классе Java по порядку.

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

Одна из возможностей - сканирование компонентов, которое мы объясняемin another article.

6.1. Импорт контекста в другой

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

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

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

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

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

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

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

7. Веб-приложения Spring Boot

Spring Boot automatically configures the components of the application,, поэтому, как правило, меньше необходимости думать о том, как их организовать.

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

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

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

Однако для добавления сервлетов, фильтров и прослушивателей, как показано в этой статье, это необязательно. In fact, Spring Boot automatically registers every servlet-related bean to the container:с

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

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

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

8. Выводы

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

Мы упустили некоторые функции, в частностиsupport for a shared context in enterprise applications, который на момент написания все еще былmissing from Spring 5.

Реализацию всех этих примеров и фрагментов кода можно найти вthe GitHub project - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.