Previous posts such as this one have shown using embedded Jetty to REST-enable a standalone Java program. Those posts were lacking an important feature for real applications: packaging into a JAR so the application will run outside of Eclipse and won’t be dependent on Maven and jetty:run. To make this happen, we will use Maven to build an executable JAR that also includes all of the Jetty and Spring dependencies we need.
The goal of this work is to get to the point where we can run the example application by:
- Cloning the Git repository.
java -jar target/webmvc-standalone.jar
When I started adding the necessary bits to the
pom.xml file of my sample
application, I expected a relatively straightforward solution. I ended
up with a relatively straightforward solution that was completely different
from what I expected. So I think it’s worth a detailed discussion of how this
solution works and what Maven is doing for us.
Our desire to make an executable JAR is complicated by the fact that we want our Maven project to build a WAR as a default package, so that we can use this code in a Java web container if desired. Additionally, we introduce some complexity by making a single JAR with all dependencies, because that causes files in the Spring JARs to collide. I’ll show what I did to address each of these.
Build both JAR and WAR
The basic idea here is that we want Maven to make both a JAR file and a WAR
file during the “package” phase. Our
pom.xml file specifies
war as the
packaging for this project, so the WAR file will be created as expected. We
need to add the JAR file without disturbing this.
I found a great post here that got me started. The basic idea is
to add the following to
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.4</version> <executions> <execution> <id>package-jar</id> <phase>package</phase> <goals> <goal>jar</goal> </goals> </execution> </executions> </plugin>
This is the behavior we would get for “free” if we used
jar packaging in
execution section ties it to the
package phase so that it
runs during the default build process. The
jar goal tells the plugin what to
make. This gets us a basic JAR with the classes in the normal place for a JAR
(rather than in
WEB-INF/classes as they must be in the WAR file).
At the same time, we need to deal with the fact that the Maven resources plugin
src/main/resources to be a resources directory, while in our
case we have files in
src/main/webapp that also need to be included. We want
to copy these resources to the target directory so the JAR plugin will pick
them up. (This is an important distinction; the typical Maven question, “how do
I include extra resources in my JAR?” should really be “how do I get extra
target so the JAR plugin will pick them up?”)
We add this to the
build section of
<resources> <resource> <directory>src/main/resources</directory> </resource> <resource> <directory>src/main/webapp</directory> </resource> </resources>
This causes our new
required for our embedded Jetty webapp.
JAR with dependencies
Next, we make an additional JAR that has the correct
Main-Class entry in the
MANIFEST.MF file and includes the necessary dependencies so we only have to
ship one file. This is done using the Maven assembly plugin. The
assembly plugin does repackaging only; that’s why we had to add a JAR artifact
above. Without that JAR artifact to work from, the assembly plugin repackages
the WAR, and we end up with classes in
WEB-INF/classes. This causes Java to
complain that it can’t find our main class when we try to run the JAR.
The assembly plugin comes with a
jar-with-dependencies configuration that can
be used simply by adding it as a
descriptorRef to the relevant section of
pom.xml, as shown in this StackOverflow question. However, this
configuration doesn’t work in our particular case, as the Spring dependencies
we need have files with overlapping names. As a result, we need to make our own
assembly configuration. Fortunately, this is pretty simple. We first add this
build/plugins section of
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>2.4</version> <configuration> <descriptors> <descriptor>src/assemble/distribution.xml</descriptor> </descriptors> <archive> <manifest> <mainClass>org.anvard.webmvc.server.EmbeddedServer</mainClass> </manifest> </archive> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>
As before, we use the
executions section to make sure this is run
package. We also specify the main class for our
application. Finally, we point the plugin to our assembly configuration file,
which lives in
src/assemble. I present the assembly configuration below, but
first we need to talk about the issue with the Spring JARs that made this
custom assembly necessary.
Spring schemas and handlers
With this sample application, we use Spring WebMVC to provide a REST API for ordinary Java classes, as discussed in this post. The Spring code we use is spread across a few different JARs.
Recent versions of Spring added a “custom XML namespace” feature that allows
the contents of a Spring XML configuration file to be very extensible. Spring
WebMVC, and other Spring libraries, use this feature to provide custom XML
tags. In order to parse the XML file with these custom tags, Spring needs to be
able to match these custom namespaces to handlers. To do this, Spring expects
to find files called
spring.schemas in the
directory of any JAR providing a Spring custom namespace.
Several of the Spring JARs used by this application include those
spring.schemas files. Of course, each JAR only includes
its own handlers and schemas. When the Maven assembly plugin uses the
jar-with-dependencies configuration, only one copy of those files “wins” and
makes it into the executable JAR.
We really just need a single
spring.schemas that are
the concatentation of the respective files. There is probably some Maven magic
to accomplish this, but I elected to do it manually as my Bash-fu is much
greater than my Maven-fu. I added two files to the
that have the combined contents of the various files in the Spring JARs.
Maven assembly configuration
The assembly file looks like this:
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd"> <id>standalone</id> <formats> <format>jar</format> </formats> <baseDirectory></baseDirectory> <dependencySets> <dependencySet> <unpack>true</unpack> <unpackOptions> <excludes> <exclude>META-INF/spring.handlers</exclude> <exclude>META-INF/spring.schemas</exclude> </excludes> </unpackOptions> </dependencySet> </dependencySets> <files> <file> <source>src/assemble/spring.handlers</source> <outputDirectory>/META-INF</outputDirectory> <filtered>false</filtered> </file> <file> <source>src/assemble/spring.schemas</source> <outputDirectory>/META-INF</outputDirectory> <filtered>false</filtered> </file> </files> </assembly>
id will be used to name this assembly. The
baseDirectory tells the
assembly plugin that the pieces it assembles should go at the root of the new
JAR. (Otherwise they would go into a directory using the project name, in this
The next two sections are important. We want to exclude the
spring.schemas from the Spring JARs (a.k.a. the dependency set). Instead,
we want to explicitly include them from our
src/assemble directory, and put
them into the right place. We also want the assembly plugin to unpack the
dependency set JARs so we wind up with Java class files in our new JAR, rather
than just JAR-files-inside-JAR-file, which would not run correctly.
Notice that there is no directive telling Spring to include all dependencies
from the dependency set, including transitive dependencies. This is the default
so we don’t need to specify it. It’s also the default to include the unpacked
files from our own artifact (
webmvc.jar) into the new JAR.
A real-world application would probably pick either WAR packaging or executable JAR packaging, and be simpler. Additionally, it would be possible to use multiple Maven modules to build a JAR and embed it in the WAR. But it’s interesting to see how to implement a more complex solution that builds everything we need from a single project.