Spring Java Config with Multiple Dispatchers
Asked Answered
T

4

12

I've some experience Spring now and also have some pure java config web-apps in use. However, these are usually based on a quiet simple setup:

  • application config for services / repositories
  • dispatcher config for one dispatcher (and some controllers)
  • (optional) spring security to secure the access

For my current project I need to have separate dispatcher contexts with different configuration. That's not a problem with the XML based configuration as we have a dedicated ContextLoaderListener that's independent from Dispatcher Configuration. But with java config I'm not sure if what I'm doing is fine so far ;)

Here's a common 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";
  }
}

As said, I need a second (third, ...) dispatcher with another mapping (and view resolvers). So, I copied the config and added for both getServletName() (otherwise both will be named as 'dispatcher' which will cause errors). The second config was looking like that:

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";
  }
}

When I use it like this, starting application results in a problem with 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)
...

So I removed the second MyAppConfig.class return from one of the AbstractAnnotationConfigDispatcherServletInitializer and it works fine. However, that doesn't feel to be the right way ;)

For my understanding: should all DispatcherConfig be handled within one AbstractAnnotationConfigDispatcherServletInitializer or should I separate them as I did? I tried to configure them in one class but then my config was totally mixed (so I believe that's not the desired way).

How do you implement such a case? Is it possible to set the ContextLoaderListener in java config outside of the AbstractAnnotationConfigDispatcherServletInitializer? Or should I create a DefaultServlet which has only the root config? What about implementing the base interface of that configuration WebApplicationInitializer?

Tonedeaf answered 5/3, 2015 at 12:33 Comment(8)
Can you explain the reason for needing multiple dispatchers in a single application? The whole point of Front Controller is that you multiplex your requests onto one.Unbowed
@chrylis: sure. The project is more like a module based construction kit for shared services. These are not linked to each other but share the same base setup and entities. Having two applications to deploy is a no-go in that project and trying to configure the dispatcher to handle all kind of view technologies (some are based on tiles, others on jsp, newer ones on Thymeleaf) is also a bad idea.Tonedeaf
Why is it a bad idea? Spring Boot makes it easy.Unbowed
Spring Boot is another topic. I'd really just like to have different DispatcherServlets (with different web contexts). That was easy with web.xml configuration (as ContextLoaderListener was not bound to Dispatcher). I'm sure there's a solution or at least best practice.Tonedeaf
Did you find a soultion?Mutilate
have you tried adding the AnotherDispatcherConfig.class into the getServletConfigClasses of MyWebAppInitializer , it should be like @Override protected Class<?>[] getServletConfigClasses() { return new Class[]{MyDispatcherConfig.class,AnotherDispatcherConfig.class}; }Graminivorous
@pakman: solution is described below in the two answers.Tonedeaf
@chrylis: I switched to Spring Boot / MicroService architecture... that makes it more easy and comes with a lot of advantages (thanks for the hint!)Tonedeaf
T
15

Mahesh C. showed the right path, but his implementation is too limited. He is right on one point : you cannot use directly AbstractAnnotationConfigDispatcherServletInitializer for multiple dispatcher servlet. But the implementation should :

  • create a root application context
  • gives it an initial configuration and say what packages it should scan
  • add a ContextListener for it to the servlet context
  • then for each dispatcher servlet
    • create a child application context
    • gives it the same an initial configuration and packages to scan
    • create a DispatcherServlet using the context
    • add it to the servlet context

Here is a more complete implementation :

@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
    ...
}

That way, you have full control on which beans will end in which context, exactly as you would have with XML configuration.

Trembles answered 10/6, 2015 at 14:2 Comment(2)
That's quite similar to how I implemented it. Meanwhile I changed my mind and switched to MicroService architecture with Spring Boot.Tonedeaf
In reality, you can use several AbstractAnnotationConfigDispatcherServletInitialize classes. See my answer.Konrad
L
7

I think you can work it out if you use generic WebApplicationInitializer interface rather than using abstract implementation provided by spring - AbstractAnnotationConfigDispatcherServletInitializer.

That way, you could create two separate initializers, so you would get different ServletContext on startUp() method and register different AppConfig & dispatcher servlets for each of them.

One of such implementing class may look like this:

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");

    }

}
Links answered 10/6, 2015 at 13:2 Comment(0)
M
2

I faced the same issue. Actually I had a complex configuration with multiple dispatcher servlets, filters and listeners.

I had a web.xml like below

<?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>

I replaced above web.xml with below java file

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;
    }

}
Marmot answered 27/2, 2017 at 20:43 Comment(1)
There is little point in extending the abstract if all the methods do nothing. To do that, just implement the interface as in the answer.Konrad
M
0

It can and should be done using several AbstractAnnotationConfigDispatcherServletInitializer classes, one for each dispatcher. @Serge Ballesta's answer is incorrect on this.

The solution is precisely setting rootConfigClasses to null for the 2nd initializer to prevent ContextLoaderListener setting the root context twice, which is the error you are getting. When loading the 2nd DispatcherServlet, it would look for the root context registered in servletContext, so both dispatcher contexts will finally share the same root context without any issue.

But you have to take care of:

  • Configuring orders of initializers. If one dispatcher has the default mapping "/", it should be the last one.
  • 2nd and beyond dispatcher initializers to return null in getRootConfigClasses to avoid ContextLoaderListener registering root context twice.
  • Configuring desired LoadOnStartup order.

This fix is required to question code:

    @Order(1)
    public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
        ... // This class is ok
    }

    @Order(2)
    public class AnotherWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

        // All is ok but this
        @Override
        protected Class<?>[] getRootConfigClasses() {
            // Set to null to prevent registering root context again. Let FrameworkServlet load it from servletContext.
            return null;
        }

        @Override
        public void customizeRegistration(ServletRegistration.Dynamic registration) {
            registration.setLoadOnStartup(2);
        }
    }

Or you can do it everything by hand with a single WebApplicationInitializer as in the answer of @Serge Ballesta.

Some additional notes:

  • ContextListener is not mandatory, it just initializes the context, this can be either done calling refresh method on context.

  • If using WebApplicationInitializer class, you can have different ones for each dispatcher, ordered using @Order annotation.

  • The different dispatcher web contexts don't need to share a root context. This is usual, but you can create completely independent dispatchers with unrelated contexts. For example, if you want to serve a REST api along with static context and want to keep configurations separated.

  • When having several dispatchers, it's recommendable to configure the RequestMappingHandlerMapping to pass the full URL to controllers for those without default mapping ("/"), otherwise by default it trims the dispatcher mapping part. This will simplify you tests. Spring-boot does this automatically, or if you don't use it, it can be done with a WebMvcConfigurer:

     @Configuration
     @EnableWebMvc
     public class WebConfiguration implements WebMvcConfigurer {
    
         @Override
         public void configurePathMatch(PathMatchConfigurer configurer) {
             // Configure controller mappings to match with full path
             UrlPathHelper urlPathHelper = new UrlPathHelper();
             urlPathHelper.setAlwaysUseFullPath(true);
             configurer.setUrlPathHelper(urlPathHelper);
         }
     }
    
  • If using the abstract initializer class, you can prevent ContextLoaderListener to be registered at all, overwriting registerContextLoaderListener method and registering it by hand in other initializer. Although it's usually worth it to let the 1st initializer to do it. But this can be useful for example if you have 2 dispatchers with different parent contexts and need to avoid registering both of them as root contexts.

Spring security

An important question when having multiple dispatchers is Spring Security configuration. This can be done adding a class extending AbstractSecurityWebApplicationInitializer to your context. It registers a filter called DelegatingFilterProxy after the dispatcher configuration mapped to "/*". This filter looks for a securityFilterChain bean in root context by default. This bean is added to context when using @EnableWebSecurity annotation which is usually located in root context so you can share the security config between different dispatchers. But you can also put security config in one dispatcher context and tell the filter to load it with init-parameter contextAttribute.

You can have one shared security configuration with beans WebSecurityCustomizer & SecurityFilterChain (from Spring Security 5.7) or extending the previous WebSecurityConfigurer class. Or you can have different beans for each dispatcher, configuring several web and http elements.

Or even you can have separated configurations for different dispatchers by registering a filter for each one. Filters must have different names, and name is hardcoded in AbstractSecurityWebApplicationInitializer class (up to spring security 5.7). So you can create:

  • one of the filters with a standard AbstractSecurityWebApplicationInitializer with last order.

  • the other one using the onStartup method of you other dispatcher so you can set a different name. Like this:

      @Override
      public void onStartup(ServletContext servletContext) throws ServletException {
          super.onStartup(servletContext);
    
          // Register DelegatingFilterProxy for Spring Security. Filter names cannot repeat.
          // It can not be used here AbstractSecurityWebApplicationInitializer because the filter name is hardcoded.
          final String SERVLET_CONTEXT_PREFIX = "org.springframework.web.servlet.FrameworkServlet.CONTEXT.";
    
          FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("springSecurityFilterChain2", DelegatingFilterProxy.class);
          filterRegistration.addMappingForUrlPatterns(null, false, getServletMappings()[0]);
          // Spring security bean is in web app context.
          filterRegistration.setInitParameter("contextAttribute", SERVLET_CONTEXT_PREFIX + getServletName());
          // TargetBeanName by default is filter name, so change it to Spring Security standard one
          filterRegistration.setInitParameter("targetBeanName", AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME);
      }
    

Additional references:

Manthei answered 19/9, 2022 at 21:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.