Spring MVC 具有多个调度程序的Spring Java配置

azpvetkf  于 2022-11-14  发布在  Spring
关注(0)|答案(4)|浏览(173)

我现在有一些Spring的经验,也有一些纯java配置的web应用程序在使用中。然而,这些应用程序通常都是基于一个安静简单的设置:

  • 服务/存储库的应用程序配置
  • 一个调度程序(和一些控制器)的调度程序配置
  • (可选)Spring安全装置,用于保护访问

对于我当前的项目,我需要有不同配置的单独的调度器上下文。对于基于XML的配置,这不是问题,因为我们有一个独立于调度器配置的专用ContextLoaderListener。但是对于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";
  }
}

如前所述,我需要第二个(第三个,......)调度器,它有另一个Map(和视图解析器)。因此,我复制了配置并添加了两个getServletName()(否则两个都将被命名为'dispatcher',这将导致错误)。第二个配置如下所示:

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)
...

所以我从一个 AbstractAnnotationConfigDispatcherServletInitializer 中删除了第二个MyAppConfig.class返回,它工作正常。)
对于我的理解:所有的DispatcherConfig应该在一个 AbstractAnnotationConfigDispatcherServletInitializer 中处理还是应该像我做的那样将它们分开?我尝试在一个类中配置它们,但是我的配置完全是混合的(所以我认为这不是理想的方式)。
如何实现这种情况?是否可以在 AbstractAnnotationConfigDispatcherServletInitializer 之外的java配置中设置 ContextLoaderListener?或者我应该创建一个只有根配置的 DefaultServlet?如何实现该配置的基本接口 WebApplicationInitializer

5kgi1eie

5kgi1eie1#

Mahesh C.展示了正确的道路,但他的实施太有限了。他在一点上是正确的:您不能直接将AbstractAnnotationConfigDispatcherServletInitializer用于多个分派器servlet。但是实现应该:

  • 创建根应用程序上下文
  • 给它一个初始配置,并告诉它应该扫描哪些软件包
  • 将其ContextListener添加到servlet上下文
  • 则对于每个分派器servlet
  • 创建子应用程序上下文
  • 为它提供相同的初始配置和要扫描的包
  • 使用上下文创建DispatcherServlet
  • 将其添加到servlet上下文

下面是一个更完整的实现:

@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配置所做的那样。

ubbxdtey

ubbxdtey2#

我认为,如果您使用通用WebApplicationInitializer接口,而不是使用Spring提供的抽象实现- AbstractAnnotationConfigDispatcherServletInitializer,您可以解决这个问题。
这样,您就可以创建两个单独的初始化程序,这样您就可以在startUp()方法上获得不同的ServletContext,并为每个初始化程序注册不同的AppConfig & dispatcher servlet。
其中一个实现类可能如下所示:

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

    }

}
8gsdolmq

8gsdolmq3#

我也遇到了同样的问题。实际上,我有一个复杂的配置,有多个调度器servlet、过滤器和监听器。
我有一个如下所示的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>

我用下面的java文件替换了上面的web.xml

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

}
qmelpv7a

qmelpv7a4#

它可以而且应该使用几个AbstractAnnotationConfigDispatcherServletInitializer类来完成,每个调度程序一个类。@Serge Ballesta的答案在这方面是不正确的。
解决方案是将第二个初始化器的rootConfigClasses设置为null,以防止ContextLoaderListener设置根上下文两次,这是您遇到的错误。当加载第二个DispatcherServlet时,它将查找在servletContext中注册的根上下文,因此两个调度器上下文最终将共享相同的根上下文,而不会出现任何问题。
但你要注意:

  • 正在配置初始化程序的顺序。如果一个调度程序具有默认Map“/,”则它应该是最后一个。
  • 第二个及以后的分派程序初始化器在getRootConfigClasses中返回null,以避免ContextLoaderListener两次注册根上下文。
  • 配置所需的LoadOnStartup顺序。

此修复程序是问题代码所必需的:

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

或者,您也可以使用一个WebApplicationInitializer手动完成所有操作,如@Serge Ballesta的答案所示。
一些附加说明:

  • ContextListener不是强制性的,它只是初始化上下文,这可以通过在上下文上调用refresh方法来完成。
  • 如果使用WebApplicationInitializer类,则每个调度程序可以有不同的类,使用@Order注解排序。
  • 不同的调度器Web上下文不需要共享根上下文。这很常见,但您可以创建具有不相关上下文的完全独立的调度器。例如,如果您希望提供REST API沿着静态上下文,并希望保持配置分离。
  • 当有多个调度程序时,建议配置RequestMappingHandlerMapping,将完整的URL传递给那些没有默认Map(“/”)的控制器,否则默认情况下它会修剪调度程序Map部分。这将简化您的测试。Spring-boot会自动执行此操作,或者如果您不使用它,也可以使用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);
     }
 }
  • 如果使用抽象的初始化器类,你可以完全阻止ContextLoaderListener被注册,覆盖registerContextLoaderListener方法并在其他初始化器中手动注册它。尽管通常值得让第一个初始化器来做这件事。但是这可能是有用的,例如,如果你有两个具有不同父上下文的调度器,并且需要避免将它们都注册为根上下文。
    ** Spring 安全**

当有多个调度器时,一个重要的问题是Spring Security配置。这可以通过向上下文添加一个扩展AbstractSecurityWebApplicationInitializer的类来完成。它在Map到“/*"的调度器配置之后注册一个名为DelegatingFilterProxy的过滤器。默认情况下,该过滤器在根上下文中查找securityFilterChain bean。当使用@EnableWebSecurity注解时,此bean被添加到上下文中,该注解通常位于根上下文中,因此您可以在不同的调度程序之间共享安全配置。但是,您也可以将安全配置放在一个调度程序上下文中,并告诉过滤器使用init-参数contextAttribute
您可以使用bean WebSecurityCustomizerSecurityFilterChain(来自Spring Security 5.7)或扩展以前的WebSecurityConfigurer类来共享安全配置。或者,您可以为每个调度程序使用不同的bean,配置多个 webhttp 元素。
或者,您甚至可以通过为每个调度程序注册一个过滤器来为不同的调度程序配置不同的配置。过滤器必须有不同的名称,并且名称是硬编码在AbstractSecurityWebApplicationInitializer类中的(最高为spring security 5.7)。因此,您可以创建:

  • 其中一个滤波器具有最后一阶的标准AbstractSecurityWebApplicationInitializer
  • 另一个使用另一个调度程序的 onStartup 方法,这样您就可以设置一个不同的名称。如下所示:
@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);
  }

其他参考:

相关问题