34

Spring Boot's embedded tomcat is very handy, for both development and deploy.

But what if an another (3rd-party) WAR file (for example, GeoServer) should be added?

Perhaps the following is the normal procedure:

  1. Install a normal Tomcat server.
  2. Build the Spring Boot application as a WAR file, and add it to the webapps folder of the Tomcat.
  3. Also add an another (3rd-party) WAR file to the webapps folder.

But it would be nice if the following configuration were possible.

  1. Build the Spring boot application as a standalone Jar, which includes the embedded Tomcat.
  2. Deploy the Spring boot application Jar.
  3. Add an another (3rd-party) WAR file to a folder which the embedded Tomcat recognizes.
  4. Serve both the Spring boot application contents and the another WAR's contents using the embedded Tomcat.

How can it be done?

UPDATE

When the spring boot application is made of fat jar(=executable jar), the code in the answer is not enough. The revised one is as follows:

@Bean
public EmbeddedServletContainerFactory servletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory() {

        @Override
        protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
                Tomcat tomcat) {
            try {
                Context context = tomcat.addWebapp("/foo", "/path/to/foo.war");
                WebappLoader loader =
                    new WebappLoader(Thread.currentThread().getContextClassLoader());
                context.setLoader(loader);
            } catch (ServletException ex) {
                throw new IllegalStateException("Failed to add webapp", ex);
            }
            return super.getTomcatEmbeddedServletContainer(tomcat);
        }

    };
}

Since the jar files in a fat jar cannot be loaded by the system classloader, an explicit parent classloader must be specified. Otherwise, the additional WAR cannot load the library jars in the fat jar of the spring boot application that added the WAR.

zeodtr
  • 10,645
  • 14
  • 43
  • 60
  • Read the tag descriptions. "embedded" is not "embedding"! – too honest for this site Jul 13 '15 at 02:44
  • I'm currently trying to do the same ([see here](http://stackoverflow.com/questions/42191815/cannot-add-web-application-to-built-in-tomcat)) but I'm getting tons of exceptions because of missing file dependencies. Any idea what I'm missing out here? – Stefan Falk Feb 12 '17 at 19:04

3 Answers3

30

You can add a war file to embedded Tomcat using Tomcat.addWebapp. As its javadoc says, it's the "equivalent to adding a web application to Tomcat's web apps directory". To use this API in Spring Boot, you need to use a custom TomcatEmbeddedServletContainerFactory subclass:

@Bean
public EmbeddedServletContainerFactory servletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory() {

        @Override
        protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
                Tomcat tomcat) {
            // Ensure that the webapps directory exists
            new File(tomcat.getServer().getCatalinaBase(), "webapps").mkdirs();

            try {
                Context context = tomcat.addWebapp("/foo", "/path/to/foo.war");
                // Allow the webapp to load classes from your fat jar
                context.setParentClassLoader(getClass().getClassLoader());
            } catch (ServletException ex) {
                throw new IllegalStateException("Failed to add webapp", ex);
            }
            return super.getTomcatEmbeddedServletContainer(tomcat);
        }

    };
}
Andy Wilkinson
  • 108,729
  • 24
  • 257
  • 242
  • Thanks! I've succeeded to integrate a WAR with your code. (A few warnings appear, but it's specific to the WAR I've added and I guess that it can be solved). – zeodtr Jul 13 '15 at 09:10
  • 7
    A minor problem: When server.tomcat.basedir is not specified in application.properties, WAR expansion fails. The reason is as follows: If that variable is not specified, Spring creates a temp directory and assigns it as the baseDir. But that directory does not have 'wepapps' subdirectory in which the embedded tomcat tries to mkdir() a subdirectory for the expanded files of the WAR. – zeodtr Jul 13 '15 at 10:07
  • When the spring boot application is made of fat jar, the above code is not enough. Please see my UPDATE. – zeodtr Jul 15 '15 at 03:04
  • @andy-wilkinson Do you have an idea how to do the same thing with undertow? – lcestari Apr 01 '16 at 22:20
  • 1
    to fix the failure caused by webapps dir not existing, I have also overriden `createTempDir` in the `TomcatEmbeddedServletContainerFactory` and add create the `webapps` dir myself – Alex Burdusel Sep 26 '16 at 12:48
  • I am currently trying to do that ([see code](https://github.com/silentsnooc/easy-model-access/blob/master/ema-server/server/src/main/java/org/ema/server/Server.java#L46)) but I am getting a `ClassNotFoundException` for `JspServlet` ([see the whole thing](http://pastebin.com/zfgKSc3N)). Any idea how I can fix this? – Stefan Falk Feb 06 '17 at 11:46
  • 2
    In Spring Boot 2, `TomcatEmbeddedServletContainerFactory` has been replaced: https://stackoverflow.com/questions/47700115/tomcatembeddedservletcontainerfactory-is-missing-in-spring-boot-2 – Lucas Ross Feb 20 '18 at 00:24
9

The accepted answer covers Spring Boot 1.x. The class mentioned is no longer present in Spring Boot 2.x. When using version 2, you need to use a different one:

    @Bean
    @ConditionalOnProperty(name = "external.war.file")
    public TomcatServletWebServerFactory servletContainerFactory(@Value("${external.war.file}") String path,
                                                                 @Value("${external.war.context:}") String contextPath) {
        return new TomcatServletWebServerFactory() {

            @Override
            protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
                new File(tomcat.getServer().getCatalinaBase(), "webapps").mkdirs();

                Context context = tomcat.addWebapp(contextPath, path);
                context.setParentClassLoader(getClass().getClassLoader());

                return super.getTomcatWebServer(tomcat);
            }

        };
    }

Also, Spring boot enbedded Tomcat does not by default contain dependencies for JSPs. If you are using JSPs in your external war, you need to include them.

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
</dependency>

UPDATE: I've written a more detailed blog post on how to set this up for both Spring Boot 1 and 2.

Vojtech Ruzicka
  • 16,384
  • 15
  • 63
  • 66
-1

It took a while to figure this out for Spring Boot 2 as none of the answers fully worked for me. I finally came up with this (fyi I have SSL turned on): WarRun.java with Gradle dependencies below to make it work.

What it gives:

embedded tomcat with context path / at https://localhost:8070

sample.war at https://localhost:8070/sample

SampleWebApp.war at https://localhost:8070/yo

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Properties;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource;

@ComponentScan({ "com.towianski.controllers" })
@SpringBootApplication
@Profile("server")
public class WarRun extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(WarRun.class).web(  WebApplicationType.SERVLET );
    }

    public static void main(String[] args) {

        SpringApplication app = new SpringApplication(WarRun.class);
        System.out.println( "Entered WarRun.main");

        String loggingFile = "";
        String dir = "";

        for ( int i = 0; i < args.length; i++ )
            {
//            logger.info( "** args [" + i + "] =" + args[i] + "=" );
            System.out.println( "** args [" + i + "] =" + args[i] + "=" );
            if ( args[i].toLowerCase().startsWith( "-dir" ) )
                {
                dir = args[i].substring( "-dir=".length() );
                }
            else if ( args[i].toLowerCase().startsWith( "--logging.file" ) )
                {
                loggingFile = args[i].substring( "--logging.file=".length() );
                stdOutFilePropertyChange( loggingFile );
                stdErrFilePropertyChange( loggingFile );
                }
            }

        Properties properties = new Properties();
//        properties.setProperty( "spring.resources.static-locations",
//                               "classpath:/home/stan/Downloads" );
        properties.setProperty( "server.port", "8070" );
//        System.setProperty("server.servlet.context-path", "/prop");     <--- Will set embedded Spring Boot Tomcat context path
        properties.setProperty( "spring.security.user.name", "stan" );
        properties.setProperty( "spring.security.user.password", "stan" );
        System.out.println( "Entered WarRun.main after set properties");
        app.setDefaultProperties(properties);
        System.out.println( "Entered WarRun.main after call set props. before app.run");

        app.run(args);
        System.out.println( "Entered WarRun.main after app.run()");
    }

    @Bean
    public ServletWebServerFactory servletContainer() {
        return new TomcatServletWebServerFactory() {
            protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
                System.out.println( "tomcat.getServer().getCatalinaBase() =" + tomcat.getServer().getCatalinaBase() + "=" );
                new File(tomcat.getServer().getCatalinaBase(), "/webapps").mkdirs();
    //            try {
    //                Files.copy( (new File( "/home/stan/Downloads/sample.war" ) ).toPath(), (new File( tomcat.getServer().getCatalinaBase() +"/webapp/sample.war") ).toPath());
    //            } catch (IOException ex) {
    //                Logger.getLogger(WarRun.class.getName()).log(Level.SEVERE, null, ex);
    //            }
                try {
                    System.out.println( "Entered ServletWebServerFactory servletContainer()");
                    Context context2 = tomcat.addWebapp("/sample", new ClassPathResource("file:/home/stan/Downloads/sample.war").getFile().toString());
                    Context context3 = tomcat.addWebapp("/yo", new ClassPathResource("file:/home/stan/Downloads/SampleWebApp.war").getFile().toString());
    //                Context context = tomcat.addWebapp("/what", new ClassPathResource( "file:" + tomcat.getServer().getCatalinaBase() +"/webapps/sample.war" ).getFile().toString() );

                    context2.setParentClassLoader(getClass().getClassLoader());
                    context3.setParentClassLoader(getClass().getClassLoader());

    //  also works but above seems better
    //                WebappLoader loader2 = new WebappLoader(Thread.currentThread().getContextClassLoader());
    //                WebappLoader loader3 = new WebappLoader(Thread.currentThread().getContextClassLoader());
    //                context2.setLoader(loader2);
    //                context3.setLoader(loader3);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                return super.getTomcatWebServer(tomcat);
            }
        };
    }
}

Gradle:

apply plugin: 'war'

war {
    enabled = true
}

. . . .
dependencies {
    compile("org.springframework.boot:spring-boot-starter:2.1.6.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-web:2.1.6.RELEASE") 
    compile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper', version: '9.0.21'
    compile("org.springframework.boot:spring-boot-starter-security:2.1.6.RELEASE")
    compile 'org.apache.httpcomponents:httpclient:4.5.7'
    compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.5.6'
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.jcraft:jsch:0.1.55'

    testCompile group: 'junit', name: 'junit', version: '4.12'
}
Stan Towianski
  • 411
  • 2
  • 6