Embedding Tomcat 7 in a war file

Last Modified: Thu, 05 May 2016 19:29:59 +0000 ; Created: Fri, 01 Jul 2011 19:21:45 +0000

All code is Copyright 2016 Rodney Beede
Permission to use is licensed under the GNU AFFERO GENERAL PUBLIC LICENSE, Version 3, http://www.gnu.org/licenses/agpl.txt

So Jetty has the ability to java -jar YourWebApp.war which runs the Jetty server. I tried using Jetty 8 to do this with an application that had JSP files, but support just isn't there yet. I wanted the latest JSP APIs too.

I decided to try Tomcat 7's new embedded support. I used some known tricks from my Jetty experience to get it working correctly. This allows me to embed Tomcat 7 into my war file for easy deployment to others with java -jar WebApp.war. I used Maven for all the building which makes grabbing dependency jars much easier. The embedded version reads the web application configuration from the normal WEB-INF/web.xml. Here is a rundown:

pom.xml

Standard stuff for any web application project. Your build type should be a war not a jar.

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<tomcat.version>7.0.69</tomcat.version>
	</properties>


	<dependencies>
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.17</version>
		</dependency>
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>apache-log4j-extras</artifactId>
			<version>1.2.17</version>
		</dependency>


		<!-- Maven Central and other repos don't have consistent locations for 
			versions of the J2EE APIs This guide was helpful for older versions, but 
			it doesn't seem to be followed anymore in repos: http://maven.apache.org/guides/mini/guide-coping-with-sun-jars.html 
			You can also search http://jcp.org/en/jsr/all to get the middle point (x.'#'.z) 
			latest JSR spec version -->

		<!-- http://www.java.net/forum/topic/glassfish/glassfish/javaxservlet-api-version -->
		<!-- http://java.net/projects/servlet-spec -->
		<!-- Found in Maven central repository -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.0.1</version>
			<scope>provided</scope>
		</dependency>

		<!-- http://jsp.java.net/downLoad.html -->
		<!-- Found in Maven central repository -->
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>javax.servlet.jsp-api</artifactId>
			<version>2.2.1</version>
			<scope>provided</scope>
		</dependency>

		<!-- Find the latest version with groupId and artifactId per http://jstl.java.net/download.html -->
		<!-- Found in Maven central repository -->
		<dependency>
			<groupId>org.glassfish.web</groupId>
			<artifactId>javax.servlet.jsp.jstl</artifactId>
			<version>1.2.4</version>
		</dependency>

		<!-- These allow embedding of Tomcat 7 with JSP support -->
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-core</artifactId>
			<version>${tomcat.version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
			<version>${tomcat.version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-logging-juli</artifactId>
			<version>${tomcat.version}</version>
		</dependency>
	</dependencies>


	<build>
		<plugins>
		
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-shade-plugin</artifactId>
				<version>2.4.3</version>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>shade</goal>
						</goals>
						<configuration>
							<artifactSet>
								<excludes>
									<!-- Must have Excluding javax.servlet:servlet-api:jar:2.5 from the shaded jar to avoid conflict with tomcat-embed-core -->
									<exclude>javax.servlet:servlet-api:*</exclude>
									<!-- For the warning about [WARNING] log4j-1.2.17.jar, apache-log4j-extras-1.2.17.jar define 43 overlapping classes
										we can ignore since it is okay.
									 -->
								</excludes>
							</artifactSet>
							<createDependencyReducedPom>true</createDependencyReducedPom>
							<minimizeJar>false</minimizeJar>
							<transformers>
								<transformer
									implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
									<mainClass>EmbeddedTomcatMain</mainClass>
								</transformer>
							</transformers>
						</configuration>
					</execution>
				</executions>
			</plugin>
			<!-- The main class needs to be in the root of the war in order to be 
				runnable -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-antrun-plugin</artifactId>
				<version>1.8</version>
				<executions>
					<execution>
						<id>move-main-class</id>
						<phase>compile</phase>
						<configuration>
							<tasks>
								<move
									todir="${project.build.directory}/${project.artifactId}-${project.version}">
									<fileset dir="${project.build.directory}/classes/">
										<include name="EmbeddedTomcatMain.class" />
									</fileset>
								</move>
							</tasks>
						</configuration>
						<goals>
							<goal>run</goal>
						</goals>
					</execution>
				</executions>
			</plugin>

EmbeddedTomcatMain.java (HTTP listener example 1)


import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.ProtectionDomain;

import javax.servlet.ServletException;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.apache.tomcat.util.http.fileupload.IOUtils;

/**
 * @author rbeede
 * 
 * Supports Tomcat 7
 * 
 */
public class EmbeddedTomcatMain {
	public static void main(final String[] args) throws ServletException, LifecycleException, URISyntaxException, IOException {
		final Tomcat tomcat = new Tomcat();
		
		
		// Extract the pre-packaged SSL keys
		final File[] certificateStores = extractCertificateStores();
		
		
		
		tomcat.setPort(80);  // Default connector
		
		addConnector(82, false, tomcat, null);
		addConnector(443, true, tomcat, certificateStores);
		
        
		// Load the war (assumes this class in in root of war file)
		final ProtectionDomain domain = EmbeddedTomcatMain.class.getProtectionDomain();
		final URL location = domain.getCodeSource().getLocation();

		System.out.println("Using webapp at " + location.toExternalForm());


		tomcat.addWebapp("/", location.toURI().getPath());
		tomcat.start();
		tomcat.getServer().await();
	}
	
	
	private static void addConnector(final int port, final boolean https, final Tomcat tomcat, final File[] certificateStores) throws IOException {
		final Connector connector = new Connector();
		connector.setScheme((https) ? "https" : "http");
		connector.setPort(port);
		connector.setProperty("maxPostSize", "0");  // unlimited
		connector.setProperty("xpoweredBy", "true");
		if(https) {
			connector.setSecure(true);
			connector.setProperty("SSLEnabled","true");
			connector.setProperty("keyPass", "123456");
			connector.setProperty("keystoreFile", certificateStores[0].getCanonicalPath());
			connector.setProperty("keystorePass", "123456");
			connector.setProperty("truststoreFile", certificateStores[1].getCanonicalPath());
			connector.setProperty("truststorePass", "123456");
		}
		tomcat.getService().addConnector(connector);
	}
	
	
	/**
	 * @param tomcat
	 * @return 0 = keystoreFile, 1 = truststoreFile
	 * @throws IOException 
	 */
	private static File[] extractCertificateStores() throws IOException {
		//FIXME Not secure in creation because Java 6 and before provide no platform independent API for setting file permissions & ownership.  Java 7 will.
		final File keystoreFile = File.createTempFile("ETM", null);
		final File truststoreFile = File.createTempFile("ETM", null);
		
		keystoreFile.deleteOnExit();
		truststoreFile.deleteOnExit();
		
		final FileOutputStream fosKeystore = new FileOutputStream(keystoreFile);
		final FileOutputStream fosTruststore = new FileOutputStream(truststoreFile);
		
		// Assumes .jks files were in root of project resources which causes them to be under /WEB-INF/classes/ inside the war
		IOUtils.copy(EmbeddedTomcatMain.class.getResourceAsStream("/WEB-INF/classes/keyStore.jks"), fosKeystore);
		IOUtils.copy(EmbeddedTomcatMain.class.getResourceAsStream("/WEB-INF/classes/trustStore.jks"), fosTruststore);
		
		fosKeystore.close();
		fosTruststore.close();
		
		
		return new File[] {keystoreFile, truststoreFile};
	}
}

EmbeddedTomcatMain.java (AJP listener only example 2)

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.security.ProtectionDomain;
import java.util.UUID;

import javax.servlet.ServletException;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.Tomcat;

/**
 * @author rbeede
 * 
 * Supports Tomcat 7
 * 
 * Enforces secure options but does not handle https in preference of localhost binding AJP and letting external
 * service handle user identity authorization and authentication.
 * 
 * Since this is embedded inside a WAR file it must be found in the root of the packaged & assembled WAR
 * 
 */
public class EmbeddedTomcatMain {
	public static void main(final String[] args) throws ServletException, LifecycleException, URISyntaxException, IOException {
		final Tomcat tomcat = new Tomcat();

		// Tomcat 7 must have a BaseDir for temporary files (like work, wars, etc)
		final Path tempBaseDir = java.nio.file.Files.createTempDirectory(EmbeddedTomcatMain.class.getSimpleName());
		tempBaseDir.toFile().deleteOnExit();
		tomcat.setBaseDir(tempBaseDir.toString());
		System.out.println("Using temporary base directory of:  " + tempBaseDir);  // Tomcat 7 API has no tomcat.getBaseDir() method
		
		tomcat.getHost().setAutoDeploy(false);
		tomcat.getHost().setCreateDirs(false);
		tomcat.getHost().setDeployOnStartup(false);
		tomcat.setPort(-1);  // Not using default connector;  Undocumented API trick
		
		
		// Tomcat 8 supports unpackWARS=false with untouched war file
		// Tomcat 7 even with unpackWARS=false still has to extract Java libs and temp files for JSP compile
		// 2015-04-06	https://wiki.apache.org/tomcat/RemoveUnpackWARs
		((StandardHost) tomcat.getHost()).setUnpackWARs(false);
		

		// Setup AJP connector
		addAJPConnector(8009, tomcat);


		// Logging is not yet available
		System.out.println("Listening via AJP on port 8009");


		// Load the war (assumes this class is inside a war file)
		final ProtectionDomain domain = EmbeddedTomcatMain.class.getProtectionDomain();
		final URL location = domain.getCodeSource().getLocation();

		System.out.println("Using webapp at " + location.toExternalForm());
		
		
		// Disable the shutdown port
		tomcat.getServer().setShutdown(UUID.randomUUID().toString());  // Just in case future API doesn't disable it
		tomcat.getServer().setPort(-1);  // Not officially API documented trick

		
		System.out.println("Adding web application at " + location.toURI().getPath());
		tomcat.addWebapp("", location.toURI().getPath());
		
		
		tomcat.getServer().start();  // Do not call tomcat.start() as that adds a default connector we do not want
		
		for(final Connector connector : tomcat.getService().findConnectors()) {
			System.out.println("Connector:  " + connector.toString());
			System.out.println("Connector Port:  " + connector.getPort());
		}
		
		tomcat.getServer().await();
	}
	
	
	private static void addAJPConnector(final int port, final Tomcat tomcat) throws IOException {
		final Connector connector = new Connector();
		connector.setProtocol("AJP/1.3");  // "The standard protocol value for an AJP connector is AJP/1.3"  https://tomcat.apache.org/tomcat-7.0-doc/config/ajp.html
		connector.setPort(port);
		connector.setAllowTrace(false);
		connector.setEnableLookups(false);
		connector.setXpoweredBy(true);
		connector.setSecure(false);  // AJP does not encrypt in-transit
		
		connector.setProperty("address", "127.0.0.1");  //IPv4 only
		connector.setProperty("connectionTimeout", Integer.toString(10 * 1000));  // 10 seconds
		//We do not use requiredSecret
		connector.setProperty("tomcatAuthentication", Boolean.toString(false));
		connector.setProperty("tomcatAuthorization", Boolean.toString(false));
		
		tomcat.getService().addConnector(connector);
	}
}