Spring Java Config с несколькими диспетчерами

У меня есть некоторый опыт работы с Spring, а также есть несколько веб-приложений с чистой конфигурацией Java. Однако они обычно основаны на тихой простой настройке:

  • конфигурация приложения для сервисов/репозиториев
  • конфигурация диспетчера для одного диспетчера (и некоторых контроллеров)
  • (опционально) пружинная защита для обеспечения доступа

Для моего текущего проекта мне нужно иметь отдельные контексты диспетчера с различной конфигурацией. Это не проблема с конфигурацией на основе XML, поскольку у нас есть выделенный ContextLoaderListener, который не зависит от конфигурации Dispatcher. Но с конфигурацией Java я не уверен, что то, что я делаю, пока в порядке;)

Вот общий DispatcherConfig:

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new class[]{MyAppConfig.class};
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class[]{MyDispatcherConfig.class};
  }

  @Override
  protected String[] getServletMappings() {
    return new String[]{"/mymapping/*"};
  }

  @Override
  protected String getServletName() {
    return "myservlet";
  }
}

Как уже было сказано, мне нужен второй (третий,...) диспетчер с другим отображением (и преобразователями представлений). Итак, я скопировал конфигурацию и добавил для обоих getServletName() (иначе оба будут названы «диспетчером», что вызовет ошибки). Второй конфиг выглядел так:

public class AnotherWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new class[]{MyAppConfig.class};
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class[]{AnotherDispatcherConfig.class};
  }

  @Override
  protected String[] getServletMappings() {
    return new String[]{"/another_mapping/*"};
  }

  @Override
  protected String getServletName() {
    return "anotherservlet";
  }
}

Когда я использую это так, запуск приложения приводит к проблеме с ContextLoaderListener:

java.lang.IllegalStateException: Cannot initialize context because there is already a root application context present - check whether you have multiple ContextLoader* definitions in your web.xml!
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:277)
...

Поэтому я удалил второй возврат MyAppConfig.class из одного из AbstractAnnotationConfigDispatcherServletInitializer, и он работает нормально. Однако это не кажется правильным;)

Насколько я понимаю: следует ли обрабатывать все DispatcherConfig в рамках одного AbstractAnnotationConfigDispatcherServletInitializer или их следует разделить, как я сделал? Я пытался настроить их в одном классе, но тогда моя конфигурация была полностью смешанной (поэтому я считаю, что это не желательный способ).

Как реализовать такой случай? Можно ли установить ContextLoaderListener в конфигурации java вне AbstractAnnotationConfigDispatcherServletInitializer? Или мне следует создать DefaultServlet, который имеет только корневую конфигурацию? Как насчет реализации базового интерфейса этой конфигурации WebApplicationInitializer?


person delimiter    schedule 05.03.2015    source источник
comment
Можете ли вы объяснить причину необходимости использования нескольких диспетчеров в одном приложении? Весь смысл Front Controller в том, что вы мультиплексируете свои запросы в один.   -  person chrylis -cautiouslyoptimistic-    schedule 05.03.2015
comment
@chrylis: конечно. Проект больше похож на модульный конструктор для общих сервисов. Они не связаны друг с другом, но имеют одни и те же базовые настройки и сущности. Наличие двух приложений для развертывания в этом проекте недопустимо, и попытка настроить диспетчер для обработки всех видов технологий просмотра (некоторые основаны на плитках, другие на jsp, более новые на Thymeleaf) также является плохой идеей.   -  person delimiter    schedule 05.03.2015
comment
Почему это плохая идея? Spring Boot упрощает задачу.   -  person chrylis -cautiouslyoptimistic-    schedule 05.03.2015
comment
Spring Boot — это отдельная тема. Мне бы очень хотелось иметь разные DispatcherServlets (с разными веб-контекстами). Это было легко с конфигурацией web.xml (поскольку ContextLoaderListener не был привязан к Dispatcher). Я уверен, что есть решение или, по крайней мере, лучшая практика.   -  person delimiter    schedule 05.03.2015
comment
Вы нашли душу?   -  person pakman    schedule 18.04.2015
comment
вы пытались добавить AnotherDispatcherConfig.class в getServletConfigClasses MyWebAppInitializer , это должно быть похоже на @Override protected Class‹?›[] getServletConfigClasses() { return new Class[]{MyDispatcherConfig.class,AnotherDispatcherConfig.class}; }   -  person AntJavaDev    schedule 10.06.2015
comment
@pakman: решение описано ниже в двух ответах.   -  person delimiter    schedule 14.06.2015
comment
@chrylis: я перешел на архитектуру Spring Boot/MicroService... это упрощает задачу и дает множество преимуществ (спасибо за подсказку!)   -  person delimiter    schedule 14.06.2015


Ответы (3)


Махеш С. показал правильный путь, но его реализация слишком ограничена. Он прав в одном: вы не можете напрямую использовать AbstractAnnotationConfigDispatcherServletInitializer для сервлета с несколькими диспетчерами. Но реализация должна:

  • создать корневой контекст приложения
  • дает ему первоначальную конфигурацию и говорит, какие пакеты он должен сканировать
  • добавить для него ContextListener в контекст сервлета
  • then for each dispatcher servlet
    • create a child application context
    • дает ему ту же начальную конфигурацию и пакеты для сканирования
    • создать DispatcherServlet, используя контекст
    • добавить его в контекст сервлета

Вот более полная реализация:

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    // root context
    AnnotationConfigWebApplicationContext rootContext =
            new AnnotationConfigWebApplicationContext();
    rootContext.register(RootConfig.class); // configuration class for root context
    rootContext.scan("...service", "...dao"); // scan only some packages
    servletContext.addListener(new ContextLoaderListener(rootContext));

    // dispatcher servlet 1
    AnnotationConfigWebApplicationContext webContext1 = 
            new AnnotationConfigWebApplicationContext();
    webContext1.setParent(rootContext);
    webContext1.register(WebConfig1.class); // configuration class for servlet 1
    webContext1.scan("...web1");            // scan some other packages
    ServletRegistration.Dynamic dispatcher1 =
    servletContext.addServlet("dispatcher1", new DispatcherServlet(webContext1));
    dispatcher1.setLoadOnStartup(1);
    dispatcher1.addMapping("/subcontext1");

    // dispatcher servlet 2
    ...
}

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

person Serge Ballesta    schedule 10.06.2015
comment
Это очень похоже на то, как я это реализовал. Тем временем я передумал и переключился на архитектуру MicroService с Spring Boot. - person delimiter; 14.06.2015

Я думаю, вы можете решить это, если будете использовать общий интерфейс WebApplicationInitializer, а не использовать абстрактную реализацию, предоставляемую Spring — AbstractAnnotationConfigDispatcherServletInitializer.

Таким образом, вы можете создать два отдельных инициализатора, чтобы получить разные ServletContext в методе startUp() и зарегистрировать разные сервлеты AppConfig и диспетчера для каждого из них.

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

public class FirstAppInitializer implements WebApplicationInitializer {

    public void onStartup(ServletContext container) throws ServletException {

        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(AppConfig.class);
        ctx.setServletContext(container);

        ServletRegistration.Dynamic servlet = container.addServlet(
                "dispatcher", new DispatcherServlet(ctx));

        servlet.setLoadOnStartup(1);
        servlet.addMapping("/control");

    }

}
person Mahesh C.    schedule 10.06.2015

Я столкнулся с той же проблемой. На самом деле у меня была сложная конфигурация с несколькими сервлетами-диспетчерами, фильтрами и слушателями.

У меня был файл web.xml, как показано ниже.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    version="3.1">
    <listener>
        <listener-class>MyAppContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>spring.profiles.active</param-name>
        <param-value>${config.environment}</param-value>
    </context-param>
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>MyAppConfig</param-value>
    </context-param>
    <servlet>
        <servlet-name>restEntryPoint</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>MyRestConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>restEntryPoint</servlet-name>
        <url-pattern>/api/*</url-pattern>
    </servlet-mapping>
    <servlet>
        <servlet-name>webSocketEntryPoint</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>MyWebSocketWebConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>webSocketEntryPoint</servlet-name>
        <url-pattern>/ws/*</url-pattern>
    </servlet-mapping>
    <servlet>
        <servlet-name>webEntryPoint</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>MyWebConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>webEntryPoint</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    <filter>
        <filter-name>exceptionHandlerFilter</filter-name>
        <filter-class>com.san.common.filter.ExceptionHandlerFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>exceptionHandlerFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>validationFilter</filter-name>
        <filter-class>MyValidationFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>validationFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>lastFilter</filter-name>
        <filter-class>MyLastFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>lastFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

Я заменил вышеуказанный файл web.xml ниже java-файлом

import java.util.EnumSet;

import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;


public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {

        servletContext.addListener(MyAppContextLoaderListener.class);

        servletContext.setInitParameter("spring.profiles.active", "dev");
        servletContext.setInitParameter("contextClass", "org.springframework.web.context.support.AnnotationConfigWebApplicationContext");
        servletContext.setInitParameter("contextConfigLocation", "MyAppConfig");

        // dispatcher servlet for restEntryPoint
        AnnotationConfigWebApplicationContext restContext = new AnnotationConfigWebApplicationContext();
        restContext.register(MyRestConfig.class);
        ServletRegistration.Dynamic restEntryPoint = servletContext.addServlet("restEntryPoint", new DispatcherServlet(restContext));
        restEntryPoint.setLoadOnStartup(1);
        restEntryPoint.addMapping("/api/*");

        // dispatcher servlet for webSocketEntryPoint
        AnnotationConfigWebApplicationContext webSocketContext = new AnnotationConfigWebApplicationContext();
        webSocketContext.register(MyWebSocketWebConfig.class);
        ServletRegistration.Dynamic webSocketEntryPoint = servletContext.addServlet("webSocketEntryPoint", new DispatcherServlet(webSocketContext));
        webSocketEntryPoint.setLoadOnStartup(1);
        webSocketEntryPoint.addMapping("/ws/*");

        // dispatcher servlet for webEntryPoint
        AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
        webContext.register(MyWebConfig.class);
        ServletRegistration.Dynamic webEntryPoint = servletContext.addServlet("webEntryPoint", new DispatcherServlet(webContext));
        webEntryPoint.setLoadOnStartup(1);
        webEntryPoint.addMapping("/");

        FilterRegistration.Dynamic validationFilter = servletContext.addFilter("validationFilter", new MyValidationFilter());
        validationFilter.addMappingForUrlPatterns(null, false, "/*");

        FilterRegistration.Dynamic lastFilter = servletContext.addFilter("lastFilter", new MyLastFilter());
        lastFilter.addMappingForUrlPatterns(null, false, "/*");

    }

    @Override
    protected Class<?>[] getRootConfigClasses() {
        // return new Class<?>[] { AppConfig.class };
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    protected String[] getServletMappings() {
        // TODO Auto-generated method stub
        return null;
    }

}
person Anil Agrawal    schedule 27.02.2017