30

Updated

My question is how do I initialise an isolated spring webmvc web-app in spring boot. The isolated Web application should:

  1. Should not initialise itself in the application class. We want to do these in a starter pom via auto configuration. We have multiple such web-apps and we need the flexibility of auto configuration.
  2. Have the ability to customise itself using interfaces like: WebSecurityConfigurer (we have multiple web-apps, each does security in its own way) and EmbeddedServletContainerCustomizer (to set the context path of the servlet).
  3. We need to isolate beans specific to certain web-apps and do not want them to enter the parent context.

Progress

The configuration class below is listed in my META-INF/spring.factories.

The following strategy does not lead to a functioning web-mvc servlet. The context path is not set and neither is the security customised. My hunch is that I need to include certain webmvc beans that process the context and auto configure based on what beans are present -- similar to how I got boot based property placeholder configuration working by including 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;
    }
}

Hassan Syed
  • 19,054
  • 9
  • 76
  • 156
  • 8
    Why? Why do you want such a contraption, you are basically trying to mimic an ear deployment with a jar or war... That is something you shouldn't be doing imho. – M. Deinum Feb 01 '16 at 12:46
  • We are porting a OSGI Karaf spring-dm application to spring boot. I don't see any alternatives other than refactoring the whole code-base and that isn't an option. – Hassan Syed Feb 01 '16 at 14:58
  • 8
    OSGi and Spring Boot are different beasts and have quite different uses. You are trying to use Spring Boot for something it wasn't supposed to do. With a lot of sweat you can probably shoe horn it into something (or by using a big hammer). You would basically have to do everything that is done by the `MvcAutoConfiguration` for each `DispatcherServlet` you are loading, and you probably need to get access to the underlying container to have it registered. – M. Deinum Feb 01 '16 at 16:04
  • 1
    Also adding multiple `EmbeddedServletContainerCustomizer` is going to fail, you will have only a single embedded container. Depending on what you are trying to change it generally will not work, the same with the `WebSecurityConfigurerAdapter` that needs to be registered globally in the root context not child contexts. – M. Deinum Feb 01 '16 at 16:06
  • To give you some more context. We'd like to stick with a single JVM for this if possible. We'd like to share the JPA stack and the domain logic. We have 2 hessian servlets (one for debugging), 2 webmvc servlets and a jersey servlet for more involved rest api's. Replacing once JVM with 4 is quite a leap. Rolling the `WebsecurityConfigurerAdapters` into one doesn't sound right either as it completely breaks the modularisation of our web applications. How much sweat are we talking ? – Hassan Syed Feb 01 '16 at 16:38
  • [A similar case](https://stackoverflow.com/questions/34728814/spring-boot-with-two-mvc-configurations) was discussed here just two weeks ago. – Hendrik Jander Feb 01 '16 at 17:04
  • The Spring Security filter chain, as the name implies, is a `Filter` filters come before servlets, so for the configuration to be effective the configuration has to be global, although you can add security rules per servlet by adding a security config for each, however if things are loaded in a wrong order and there is a `/**` mapping loaded somewhere it doesn't do a thing. Also I nowhere said you need to have a single 1 you can have multiple one for each servlet but things like the `AuthenticationManager` need to be global. – M. Deinum Feb 01 '16 at 19:21
  • @M.Deinum We're trying to refactor along your comments (after some more research). This solution does seem to be the way to align ourselves with spring boot -- this is turning out to be as obtuse a solution as the solution my question is referring to :(. – Hassan Syed Feb 08 '16 at 16:07
  • 5
    This sounds like an ideal opportunity to introduce a microservice based application, as blogged about [here](https://spring.io/blog/2015/07/14/microservices-with-spring) – PaulNUK Feb 09 '16 at 13:44
  • I agree with the @PaulNUK, It sounds like microservice which provides modularization in your app. – NIrav Modi May 09 '16 at 03:49
  • Just write different apps, use a simple approach, do not invest too much into tinkering something that doesn't need that much effort – Timoteo Ponce Oct 05 '16 at 13:41
  • Just want to make sure I'm understanding correctly, you're trying to run multiple web apps in a single jar? – mad_fox Jan 18 '17 at 13:57

4 Answers4

3

We had the similar issue using Boot (create a multi-servlets app with parent context) and we solved it in the following way:

1.Create your parent Spring config, which will consist all parent's beans which you want to share. Something like this:

@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.Create type which will determine the type of your specific app module (for example ou case is REST or SOAP). Also here you can specify your required context path or another app specific data (I will show bellow how it will be used):

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.Create Boot app initializer for your whole app:

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.Create abstract config classes which will contain child-specific beans and everything that you can not or don't want share via parent context. Here you can specify all required interfaces such as WebSecurityConfigurer or EmbeddedServletContainerCustomizer for your particular app module:

/*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.Create entry point class which will compile whole our app:

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

Now everything is ready to rocket our super multi-app boot in two steps:

1.Init your child apps, for example, REST and 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.Prepare entry point and run as Boot app: 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)
                    );
}

}

enjoy ^_^

Useful links:

Serhii Povísenko
  • 2,092
  • 22
  • 32
0

This could be one way of doing this (it's in our production code). We point to XML config, so maybe instead of dispatcherServlet.setContextConfigLocation() you could use dispatcherServlet.setContextClass()

@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;
    }
}
Dominika
  • 128
  • 1
  • 10
0

Unfortunately I couldn't find a way to use auto configuration for multiple servlets.

However, you can use the ServletRegistrationBean to register multiple servlets for your application. I would recommend you to use the AnnotationConfigWebApplicationContext to initiate the context because this way you can use the default Spring configuration tools (not the spring boot one) to configure your servlets. With this type of context you just have to register a configuration class.

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

If you want to handle multipart requests you should set the multipart configuration for the registration bean. This configuration can be autowired for the registration and will be resolved from the parent context.

public ServletRegistrationBean servletRegistration(MultipartConfigElement mutlipart) ...
registration.setMultipartConfig(mutlipartConfig);

I've created a little github example project which you can reach here. Note that I set up the servlet configs by Java package but you can define custom annotations for this purpose too.

Szilárd Fodor
  • 314
  • 2
  • 8
0

I manage to create an independant jar that makes tracking on my webapp and it is started depending on the value of a property in a spring.factories file in resources/META-INF in the main app:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=my package.tracking.TrackerConfig

Maybe, you could try to have independant war, started with this mechanism and then inject values in the properties files with maven mechanism/plugin (Just a theory, never tried, but based on several projects I worked on)

Mohicane
  • 150
  • 13