使用自动配置创建多个(正常运行的)WebMVC应用程序的Spring Boot

30

更新

我的问题是如何在Spring Boot中初始化一个孤立的Spring Web MVC Web应用程序。这个孤立的Web应用程序应该具有以下特点:

  1. 不应该在应用程序类中初始化自己。我们想通过自动配置在启动POM中完成这些操作。我们有多个这样的Web应用程序,需要自动配置的灵活性。
  2. 能够使用接口进行自定义,例如:WebSecurityConfigurer(我们有多个Web应用程序,每个应用程序都以自己的方式执行安全控制)和EmbeddedServletContainerCustomizer(用于设置servlet的上下文路径)。
  3. 我们需要隔离某些Web应用程序特定的bean,并且不希望它们进入父上下文。

进展

我的META-INF/spring.factories中列出了以下配置类。

以下策略无法导致一个功能完整的Web MVC Servlet。上下文路径没有设置,安全也没有被自定义。我猜测我需要包含某些处理上下文并根据存在的bean自动配置的Web MVC bean,就像我通过包括PropertySourcesPlaceholderConfigurer.class获得基于引导属性占位符配置的工作方式一样。

@Configuration
@AutoConfigureAfter(DaoServicesConfiguration.class)
public class MyServletConfiguration {
    @Autowired
    ApplicationContext parentApplicationContext;

    @Bean
    public ServletRegistrationBean myApi() {
        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.setParent(parentApplicationContext);
        applicationContext.register(PropertySourcesPlaceholderConfigurer.class);
        // a few more classes registered. These classes cannot be added to 
        // the parent application context.
        // includes implementations of 
        //   WebSecurityConfigurerAdapter
        //   EmbeddedServletContainerCustomizer

        applicationContext.scan(
                // a few packages
        );

        DispatcherServlet ds = new DispatcherServlet();
        ds.setApplicationContext(applicationContext);

        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(ds, true, "/my_api/*");
        servletRegistrationBean.setName("my_api");
        servletRegistrationBean.setLoadOnStartup(1);
        return servletRegistrationBean;
    }
}


8
为什么?为什么你想要这样一个装置,你基本上是试图用罐子或战争模拟耳部放置...在我看来,这是你不应该做的事情。 - M. Deinum
我们正在将一个基于OSGI Karaf spring-dm的应用程序移植到spring boot。除了重构整个代码库,我没有看到其他选择。但这并不是一个可行的选项。 - Hassan Syed
8
OSGi和Spring Boot是两种不同的技术,并且用途有很大的区别。你试图使用Spring Boot去做它原本不应该做的事情。虽然你可以尝试费力地把它改造成所需的形式(或使用“大锤子”),但这需要你基本上为每个加载的“DispatcherServlet”执行“MvcAutoConfiguration”所做的所有操作,并且你可能需要访问底层容器来进行注册。 - M. Deinum
1
同时添加多个 EmbeddedServletContainerCustomizer 将会失败,你只会有一个嵌入式容器。根据你试图改变的内容,通常不会起作用,同样适用于需要在根上下文中全局注册的 WebSecurityConfigurerAdapter,而不是子上下文。 - M. Deinum
5
这似乎是一个引入基于微服务的应用程序的理想机会,就像这里所博客的那样。 - PaulNUK
显示剩余7条评论
4个回答

3
我们曾经使用Boot(创建具有父上下文的多个servlet应用程序)遇到了类似的问题,我们通过以下方式解决了这个问题:
1.创建你的父Spring配置,其中包含你想要共享的所有父bean。类似于这样:
@EnableAutoConfiguration(
    exclude = {
        //use this section if your want to exclude some autoconfigs (from Boot) for example MongoDB if you already have your own
    }
)
@Import(ParentConfig.class)//You can use here many clasess from you parent context
@PropertySource({"classpath:/properties/application.properties"})
@EnableDiscoveryClient
public class BootConfiguration {
}

2. 创建类型以确定您特定应用程序模块的类型(例如,我们的情况是REST或SOAP)。在此处,您还可以指定所需的上下文路径或其他应用程序特定数据(我将在下面展示如何使用):

public final class AppModule {

    private AppType type;

    private String name;

    private String contextPath;

    private String rootPath;

    private Class<?> configurationClass;

    public AppModule() {
    }

    public AppModule(AppType type, String name, String contextPath, Class<?> configurationClass) {
        this.type = type;
        this.name = name;
        this.contextPath = contextPath;
        this.configurationClass = configurationClass;
    }

    public AppType getType() {
        return type;
    }

    public void setType(AppType type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getRootPath() {
        return rootPath;
    }

    public AppModule withRootPath(String rootPath) {
        this.rootPath = rootPath;
        return this;
    }

    public String getContextPath() {
        return contextPath;
    }

    public void setContextPath(String contextPath) {
        this.contextPath = contextPath;
    }

    public Class<?> getConfigurationClass() {
        return configurationClass;
    }

    public void setConfigurationClass(Class<?> configurationClass) {
        this.configurationClass = configurationClass;
    }

    public enum AppType {
        REST,
        SOAP
    }
}

3. 为整个应用创建 Boot 应用程序初始化器:

public class BootAppContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    private List<AppModule> modules = new ArrayList<>();

    BootAppContextInitializer(List<AppModule> modules) {
        this.modules = modules;
    }

    @Override
    public void initialize(ConfigurableApplicationContext ctx) {

        for (ServletRegistrationBean bean : servletRegs(ctx)) {
            ctx.getBeanFactory()
               .registerSingleton(bean.getServletName() + "Bean", bean);
        }
    }

    private List<ServletRegistrationBean> servletRegs(ApplicationContext parentContext) {

        List<ServletRegistrationBean> beans = new ArrayList<>();

        for (AppModule module: modules) {

            ServletRegistrationBean regBean;

            switch (module.getType()) {
                case REST:
                    regBean = createRestServlet(parentContext, module);
                    break;
                case SOAP:
                    regBean = createSoapServlet(parentContext, module);
                    break;
                default:
                    throw new RuntimeException("Not supported AppType");
            }

            beans.add(regBean);
        }

        return beans;
    }

    private ServletRegistrationBean createRestServlet(ApplicationContext parentContext, AppModule module) {
        WebApplicationContext ctx = createChildContext(parentContext, module.getName(), module.getConfigurationClass());
        //Create and init MessageDispatcherServlet for REST
        //Also here you can init app specific data from AppModule, for example, 
        //you  can specify context path in the follwing way 
      //servletRegistrationBean.addUrlMappings(module.getContextPath() + module.getRootPath());
    }

    private ServletRegistrationBean createSoapServlet(ApplicationContext parentContext, AppModule module) {
        WebApplicationContext ctx = createChildContext(parentContext, module.getName(), module.getConfigurationClass());
        //Create and init MessageDispatcherServlet for SOAP
        //Also here you can init app specific data from AppModule, for example, 
        //you  can specify context path in the follwing way 
      //servletRegistrationBean.addUrlMappings(module.getContextPath() + module.getRootPath());
    }

 private WebApplicationContext createChildContext(ApplicationContext parentContext, String name,
                                                     Class<?> configuration) {
        AnnotationConfigEmbeddedWebApplicationContext ctx = new AnnotationConfigEmbeddedWebApplicationContext();
        ctx.setDisplayName(name + "Context");
        ctx.setParent(parentContext);
        ctx.register(configuration);

        Properties source = new Properties();
        source.setProperty("APP_SERVLET_NAME", name);
        PropertiesPropertySource ps = new PropertiesPropertySource("MC_ENV_PROPS", source);

        ctx.getEnvironment()
           .getPropertySources()
           .addLast(ps);

        return ctx;
    }
}

4. 创建抽象的配置类,其中包含特定于子模块的bean和您不能或不想通过父上下文共享的所有内容。在此处,您可以指定所有所需的接口,例如WebSecurityConfigurerEmbeddedServletContainerCustomizer,以适用于您的特定应用程序模块:

/*Example for REST app*/
@EnableWebMvc
@ComponentScan(basePackages = {
    "com.company.package1",
    "com.company.web.rest"})
@Import(SomeCommonButChildSpecificConfiguration.class)
public abstract class RestAppConfiguration extends WebMvcConfigurationSupport {

    //Some custom logic for your all REST apps

    @Autowired
    private LogRawRequestInterceptor logRawRequestInterceptor;

    @Autowired
    private LogInterceptor logInterceptor;

    @Autowired
    private ErrorRegister errorRegister;

    @Autowired
    private Sender sender;

    @PostConstruct
    public void setup() {
        errorRegister.setSender(sender);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(logRawRequestInterceptor);
        registry.addInterceptor(scopeInterceptor);
    }

    @Override
    public void setServletContext(ServletContext servletContext) {
        super.setServletContext(servletContext);
    }
}

/*Example for SOAP app*/
@EnableWs
@ComponentScan(basePackages = {"com.company.web.soap"})
@Import(SomeCommonButChildSpecificConfiguration.class)
public abstract class SoapAppConfiguration implements ApplicationContextAware {

    //Some custom logic for your all SOAP apps

    private boolean logGateWay = false;

    protected ApplicationContext applicationContext;

    @Autowired
    private Sender sender;

    @Autowired
    private ErrorRegister errorRegister;

    @Autowired
    protected WsActivityIdInterceptor activityIdInterceptor;

    @Autowired
    protected WsAuthenticationInterceptor authenticationInterceptor;

    @PostConstruct
    public void setup() {
        errorRegister.setSender(sender);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * Setup preconditions e.g. interceptor deactivation
     */
    protected void setupPrecondition() {
    }

    public boolean isLogGateWay() {
        return logGateWay;
    }

    public void setLogGateWay(boolean logGateWay) {
        this.logGateWay = logGateWay;
    }

    public abstract Wsdl11Definition defaultWsdl11Definition();
}

5.创建入口点类,编译我们整个应用程序:

public final class Entrypoint {

    public static void start(String applicationName, String[] args, AppModule... modules) {
        System.setProperty("spring.application.name", applicationName);
        build(new SpringApplicationBuilder(), modules).run(args);
    }

    private static SpringApplicationBuilder build(SpringApplicationBuilder builder, AppModule[] modules) {
        return builder
                .initializers(
                    new LoggingContextInitializer(),
                    new BootAppContextInitializer(Arrays.asList(modules))
                )
                .sources(BootConfiguration.class)
                .web(true)
                .bannerMode(Banner.Mode.OFF)
                .logStartupInfo(true);
    }
}

现在,一切都已准备就绪,我们只需要两个步骤即可启动我们的超级多应用程序引导:

1.初始化你的子应用程序,例如REST和SOAP:

//REST module
@ComponentScan(basePackages = {"com.module1.package.*"})
public class Module1Config extends RestAppConfiguration {
    //here you can specify all your child's Beans and etc
}

//SOAP module
@ComponentScan(
    basePackages = {"com.module2.package.*"})
public class Module2Configuration extends SoapAppConfiguration {

    @Override
    @Bean(name = "service")
    public Wsdl11Definition defaultWsdl11Definition() {
        ClassPathResource wsdlRes = new ClassPathResource("wsdl/Your_WSDL.wsdl");
        return new SimpleWsdl11Definition(wsdlRes);
    }

    @Override
    protected void setupPrecondition() {
        super.setupPrecondition();
        setLogGateWay(true);
        activityIdInterceptor.setEnabled(true);
    }
}

2. 准备入口点并作为引导应用程序运行:

public class App {

public static void main(String[] args) throws Exception {
    Entrypoint.start("module1",args,
                     new AppModule(AppModule.AppType.REST, "module1", "/module1/*", Module1Configuration.class),
                     new AppModule(AppModule.AppType.SOAP, "module2", "module2", Module2Configuration.class)
                    );
}

}

享受 ^_^

有用的链接:


0

很遗憾,我找不到一种方法来为多个servlet使用自动配置。

但是,您可以使用ServletRegistrationBean为应用程序注册多个servlet。我建议您使用AnnotationConfigWebApplicationContext来初始化上下文,因为这样您可以使用默认的Spring配置工具(而不是spring boot)来配置您的servlet。使用这种类型的上下文,您只需要注册一个配置类即可。

@Bean
    public ServletRegistrationBean servletRegistration() {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(YourConfig.class);

        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setApplicationContext(context);

        ServletRegistrationBean registration = new ServletRegistrationBean(servlet, "/servletX");

        registration.setLoadOnStartup(1);
        registration.setName("servlet-X");

        return registration;
    }

如果您想处理多部分请求,您应该为注册bean设置多部分配置。此配置可以自动装配到注册中,并将从父上下文解析。
public ServletRegistrationBean servletRegistration(MultipartConfigElement mutlipart) ...
registration.setMultipartConfig(mutlipartConfig);

我创建了一个小的Github示例项目,您可以通过这里访问。 请注意,我使用Java包设置了servlet配置,但是您也可以为此定义自定义注释。


0

我成功创建了一个独立的jar包,用于在我的Web应用程序上进行跟踪,并且它是根据主应用程序中resources/META-INF下spring.factories文件中属性值的情况启动的:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=my package.tracking.TrackerConfig

也许,您可以尝试使用此机制启动独立的war包,然后使用Maven机制/插件将值注入属性文件中(这只是一种理论,从我参与的几个项目中得出)。


0

这可能是一种实现方式(我们的生产代码中有这个)。我们指向XML配置,所以也许可以使用dispatcherServlet.setContextClass()代替dispatcherServlet.setContextConfigLocation()

@Configuration
public class JettyConfiguration {

    @Autowired
    private ApplicationContext applicationContext;

    @Bean
    public ServletHolder dispatcherServlet() {
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(MvcConfiguration.class);//CUSTOM MVC @Configuration
        DispatcherServlet servlet = new DispatcherServlet(ctx);
        ServletHolder holder = new ServletHolder("dispatcher-servlet", servlet);
        holder.setInitOrder(1);
        return holder;
    }

    @Bean
    public ServletContextHandler servletContext() throws IOException {
        ServletContextHandler handler =
            new ServletContextHandler(ServletContextHandler.SESSIONS);

        AnnotationConfigWebApplicationContext rootWebApplicationContext =
            new AnnotationConfigWebApplicationContext();
        rootWebApplicationContext.setParent(applicationContext);
        rootWebApplicationContext.refresh();
        rootWebApplicationContext.getEnvironment().setActiveProfiles(applicationContext.getEnvironment().getActiveProfiles());

        handler.setAttribute(
            WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
            rootWebApplicationContext);
        handler.setContextPath("/my-root");
        handler.setResourceBase(new ClassPathResource("webapp").getURI().toString());
        handler.addServlet(AdminServlet.class, "/metrics/*");//DROPWIZARD
        handler.addServlet(dispatcherServlet(), "/");


        /*Web context 1*/
        DispatcherServlet webMvcDispatcherServlet1 = new DispatcherServlet();
        webMvcDispatcherServlet1.setContextConfigLocation("classpath*:/META-INF/spring/webmvc-config1.xml");
        webMvcDispatcherServlet1.setDetectAllHandlerAdapters(true);
        webMvcDispatcherServlet1.setDetectAllHandlerMappings(true);
        webMvcDispatcherServlet1.setDetectAllViewResolvers(true);
        webMvcDispatcherServlet1.setEnvironment(applicationContext.getEnvironment());
        handler.addServlet(new ServletHolder("webMvcDispatcherServlet1",webMvcDispatcherServlet1), "/web1/*");

        /*Web context 2*/
        DispatcherServlet webMvcDispatcherServlet2 = new DispatcherServlet();
        webMvcDispatcherServlet2.setContextConfigLocation("classpath*:/META-INF/spring/web-yp-config.xml");
        webMvcDispatcherServlet2.setDetectAllHandlerAdapters(true);
        webMvcDispatcherServlet2.setDetectAllHandlerMappings(true);
        webMvcDispatcherServlet2.setDetectAllViewResolvers(false);
        webMvcDispatcherServlet2.setEnvironment(applicationContext.getEnvironment());
        handler.addServlet(new ServletHolder("webMvcDispatcherServlet2",webMvcDispatcherServlet2), "/web2/*");

        /* Web Serices context 1 */
        MessageDispatcherServlet wsDispatcherServlet1 = new MessageDispatcherServlet();
        wsDispatcherServlet1.setContextConfigLocation("classpath*:/META-INF/spring/ws-config1.xml");
        wsDispatcherServlet1.setEnvironment(applicationContext.getEnvironment());  
        handler.addServlet(new ServletHolder("wsDispatcherServlet1", wsDispatcherServlet1), "/ws1/*");

        /* Web Serices context 2 */
        MessageDispatcherServlet wsDispatcherServlet2 = new MessageDispatcherServlet();
        wsDispatcherServlet2.setContextConfigLocation("classpath*:/META-INF/spring/ws-siteconnect-config.xml");
        wsDispatcherServlet2.setEnvironment(applicationContext.getEnvironment());  
        handler.addServlet(new ServletHolder("wsDispatcherServlet2", wsDispatcherServlet2), "/ws2/*");

        /*Spring Security filter*/
        handler.addFilter(new FilterHolder(
            new DelegatingFilterProxy("springSecurityFilterChain")), "/*",
            null);
        return handler;
    }

    @Bean
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter bean = new CharacterEncodingFilter();
        bean.setEncoding("UTF-8");
        bean.setForceEncoding(true);
        return bean;
    }

    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
        HiddenHttpMethodFilter filter = new HiddenHttpMethodFilter();
        return filter;
    }

    /**
     * Jetty Server bean.
     * <p/>
     * Instantiate the Jetty server.
     */
    @Bean(initMethod = "start", destroyMethod = "stop")
    public Server jettyServer() throws IOException {

        /* Create the server. */
        Server server = new Server();

        /* Create a basic connector. */
        ServerConnector httpConnector = new ServerConnector(server);
        httpConnector.setPort(9083);
        server.addConnector(httpConnector);
        server.setHandler(servletContext());
        return server;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接