Tomcat as code, without a plugin

Plain Tomcat is enough for many Java web applications, but it has no maintained Maven plugin that builds the server from the repository, as TomEE has. Cargo, an overlay folder and the Ant plugin fill that gap. The result passes the same six principles as TomEE as code, and it also runs integration tests on the real server.
The second and third parts of the series used the TomEE plugin. This post replaces it.
Why Cargo
Apache’s Tomcat Maven plugin had its last release in 2013 and embeds Tomcat 7, which implements Servlet 3.0. Current frameworks need a newer Servlet version.
Cargo downloads, configures, starts and packages many application servers, Tomcat included, and its Maven plugin is maintained. It describes a server with three concepts. The container is the server type and version, the configuration is the home folder that Cargo generates, and the deployables are the applications.
<properties>
<tomcat.version>9.0.121</tomcat.version>
<tomcat.port>8080</tomcat.port>
</properties>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven3-plugin</artifactId>
<version>${cargo.version}</version>
<configuration>
<container>
<containerId>tomcat9x</containerId>
<!-- The Tomcat distribution comes from a Maven repository, not from a URL -->
<artifactInstaller>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat</artifactId>
<version>${tomcat.version}</version>
</artifactInstaller>
</container>
<configuration>
<properties>
<cargo.servlet.port>${tomcat.port}</cargo.servlet.port>
</properties>
</configuration>
<deployables>
<deployable>
<type>war</type>
<location>${project.build.directory}/${project.build.finalName}.war</location>
<properties>
<context>ROOT</context>
</properties>
</deployable>
</deployables>
<packager>
<outputLocation>${project.build.directory}/tomcat</outputLocation>
</packager>
</configuration>
<executions>
<execution>
<id>package-tomcat</id>
<phase>package</phase>
<goals>
<goal>configure</goal>
<goal>package</goal>
</goals>
</execution>
</executions>
</plugin>
| Command or setting | Result |
|---|---|
mvn package cargo:run | Downloads the declared Tomcat, generates its home under target/, deploys the WAR and runs it, for use from the IDE |
mvn package | Leaves a complete Tomcat with the WAR deployed in target/tomcat, for the container image |
tomcat.version | A minor upgrade is one line. A new major version also changes containerId |
artifactInstaller | Tomcat comes from the Maven repository, through the same proxy and cache as other dependencies |
explicit WAR location | Without it, a plain mvn cargo:run starts an empty Tomcat that answers 404 |
What Cargo lacks
| TomEE plugin feature | Gap in Cargo |
|---|---|
| Overlay folder | Cargo copies files listed one by one |
| Library removal by prefix | Cargo only adds |
| System properties in the packaged server | cargo.jvmargs applies when Cargo starts the server and is not written into the package |
Closing the gaps
An overlay folder
Keep a folder that mirrors Tomcat’s layout and holds only the files you changed:
src/main/tomcat/
└── conf/
├── catalina.properties
└── logging.properties
Point Cargo at the whole folder:
<configuration>
<files>
<file>
<file>${project.basedir}/src/main/tomcat</file>
<todir>.</todir>
</file>
</files>
</configuration>
The setting applies to cargo:run, to the package and to every execution that uses this configuration. Leave server.xml out of the overlay, because Cargo generates it and writes the port there.
System properties in catalina.properties
Tomcat sets every entry of conf/catalina.properties as a system property at startup, whether Cargo, catalina.sh or an entrypoint starts it. Properties that every environment needs belong there:
# Resolve ${...} in server.xml and context.xml from environment variables
org.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.EnvironmentPropertySource
Start from the file shipped with the same Tomcat version. Keep cargo.jvmargs for local-only settings such as a debugger agent, and memory settings for the container.
Cleanup and trimming with Ant
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>clean-tomcat</id>
<phase>prepare-package</phase>
<goals><goal>run</goal></goals>
<configuration>
<target>
<delete dir="${project.build.directory}/tomcat"/>
<delete dir="${project.build.directory}/cargo/configurations"/>
</target>
</configuration>
</execution>
<execution>
<id>trim-tomcat</id>
<phase>package</phase>
<goals><goal>run</goal></goals>
<configuration>
<target>
<delete includeemptydirs="true">
<fileset dir="${project.build.directory}/tomcat">
<include name="webapps/manager/**"/>
<include name="webapps/host-manager/**"/>
<include name="bin/*.bat"/>
</fileset>
</delete>
</target>
</configuration>
</execution>
</executions>
</plugin>
The first execution deletes the previous output, so every package starts from a clean server even without clean. It keeps the Tomcat that Cargo downloaded, so nothing is downloaded again. The second execution removes the administration applications and the Windows scripts. Declare the Ant plugin after Cargo in the POM, because Maven runs plugins of the same phase in declaration order.
Integration tests on the real server
Cargo can start and stop the server around Maven’s integration test phase:
<execution>
<id>start-tomcat-it</id>
<phase>pre-integration-test</phase>
<goals><goal>start</goal></goals>
<configuration>
<configuration>
<home>${project.build.directory}/cargo/it</home>
<properties>
<cargo.servlet.port>18080</cargo.servlet.port>
</properties>
</configuration>
</configuration>
</execution>
<execution>
<id>stop-tomcat-it</id>
<phase>post-integration-test</phase>
<goals><goal>stop</goal></goals>
<configuration>
<configuration>
<home>${project.build.directory}/cargo/it</home>
</configuration>
</configuration>
</execution>
With the Failsafe plugin running the tests in between, mvn verify builds the server, starts it, runs HTTP tests and stops it. The tests run on the same Tomcat and overlay that go to production. The separate home and port avoid conflicts with a local server.
The container
The container follows the third part, with target/tomcat instead of the TomEE distribution. The entrypoint ends with:
exec "${CATALINA_HOME}/bin/catalina.sh" run
catalina.sh run already replaces itself with the Java process, so the stop signal reaches Tomcat. Starting org.apache.catalina.startup.Bootstrap by hand would require a copy of every flag catalina.sh sets, kept in sync with each Tomcat release.
Comparison with the TomEE plugin
| TomEE plugin | This setup |
|---|---|
| Downloads a pinned server | Cargo, with the server as a Maven artifact |
| Applies an overlay folder | Cargo’s files, pointing at the overlay folder |
| Adds libraries to the server | Cargo’s container dependencies |
| Removes libraries by prefix | Ant delete after Cargo |
| Runs the server from Maven | mvn package cargo:run |
| Builds a runnable distribution | mvn package, into target/tomcat |
| System properties everywhere | conf/catalina.properties in the overlay |
The cost is two plugins instead of one, an ordering rule between them, and Cargo behaviours that need a comment where they are used.

