Summary: How to build an executable jar file that has dependencies on other jar files.
If you ever need to create an executable jar file that has dependencies on other jar files, this tutorial is for you. I'll show you ever thing you need to do -- including the use of an Ant build script -- so your users can just type something like this at the command line:
java -jar my-application.jar
to run your Java application.
My example
In my case, I was going to deliver my application as a jar file, and my jar file had dependencies on the following external jar files:
commons-collections.jar commons-dbcp-1.1.jar commons-pool-1.1.jar log4j-1.2.8.jar mysql-connector-java-3.1.12-bin.jar
The key to referencing these other jar files is creating a custom manifest in my jar file that includes the path to these other jar files.
How I built the application, part 1
I built my application using Ant, and built the classpath for the application dynamically. That is, if I added a new jar file to my application, I didn't have to update my Ant script, it would find it automatically.
I was able to build the classpath dynamically using an Ant task named pathconvert, and a mapper named flattenmapper. The following segment of my build.xml file shows the commands that I used:
<!-- convert classpath to a flat list/string for use in manifest task --> <pathconvert property="mf.classpath" pathsep=" "> <path refid="build.class.path" /> <flattenmapper /> </pathconvert>
This segment of my Ant script can be read as "Create a new property named mf.classpath from the property build.class.path, and when you build mf.classpath, take that build.class.path (which is essentially a list) and flatten it into a space-separated list (string) of files that represents my build class path." This command effectively turns this list of jar files:
commons-collections.jar commons-dbcp-1.1.jar commons-pool-1.1.jar log4j-1.2.8.jar mysql-connector-java-3.1.12-bin.jar
into a string that looks like this:
commons-collections.jar commons-dbcp-1.1.jar commons-pool-1.1.jar log4j-1.2.8.jar mysql-connector-java-3.1.12-bin.jar
This string then becomes the classpath entry when you create your manifest file.
How I built the application, part 2
Th second part of my solution is to correctly build the manifest file for my application. With some research, I found that I could build the manifest file with Ant, and then include my new classpath in the manifest file, like this:
<tstamp/><!-- needed for TODAY --> <manifest file="MANIFEST.MF"> <attribute name="Built-By" value="${manifest.built.by}"/> <attribute name="Created-By" value="${manifest.created.by}"/> <attribute name="Main-Class" value="${manifest.main.class}"/> <attribute name="Implementation-Version" value="${version.number}-b${build.number}"/> <attribute name="Built-Date" value="${TODAY}"/> <attribute name="Class-Path" value="${mf.classpath}" /> </manifest>
All of those lines involve simple variable substitution, but the last attribute that looks like this:
<attribute name="Class-Path" value="${mf.classpath}" />
is the one that tells my jar file to use the space-separated list of jar files as its classpath when it runs.
Again, the cool thing about using Ant and this dynamic classpath is that when I add new jar files to my lib
directory, the build process just keeps working. It finds the list of jar files in my lib
directory, creates the new classpath, and copies all the jar files out to my deployment directory.
The build.xml file
To conclude this discussion, here's the entire package target from my ant build script, along with another part of the build script where I create my initial classpath.
To keep this build script flexible I do use a lot of variables that I define earlier in this build script, but hopefully those won't detract from what I'm trying to show in this article, which is how to dynamically create a classpath you can use to create a distributable, executable jar file.
With that introduction, here are some sections of code (XML code) from my Ant build script that create the dynamic classpath, and use that classpath to build the manifest file, which in the end makes my jar file executable:
<!-- First, I create my classpath (build.classpath) from all the jar files in my lib directory --> <path id="build.classpath"> <fileset dir="lib"> <include name="**/*.jar" /> </fileset> </path> <!-- Next, my package task uses that classpath --> <target name="package" depends="compile"> <echo>=== PACKAGE ===</echo> <!-- convert build.classpath to mf.classpath (the string needed for the manifest task) --> <pathconvert property="mf.classpath" pathsep=" "> <path refid="build.classpath" /> <flattenmapper /> </pathconvert> <!-- now build the manifest file, using mf.classpath --> <tstamp/><!-- needed for TODAY --> <manifest file="MANIFEST.MF"> <attribute name="Built-By" value="${manifest.built.by}"/> <attribute name="Created-By" value="${manifest.created.by}"/> <attribute name="Main-Class" value="${manifest.main.class}"/> <attribute name="Implementation-Version" value="${version.number}-b${build.number}"/> <attribute name="Built-Date" value="${TODAY}"/> <attribute name="Class-Path" value="${mf.classpath}" /> </manifest> <!-- create the jar file, including the manifest file we just created --> <jar basedir="${dest.dir.classes}" destfile="${package.file}" includes="**/*.*" excludes="**/*Test*" manifest="MANIFEST.MF" /> <!-- copy all the jar files out to the destination directory (dest.dir) --> <copy todir="${dest.dir}"> <fileset dir="${lib.dir}"> <exclude name="junit*" /> <include name="*.jar"/> <include name="*.zip"/> </fileset> </copy> <!-- move this file before the 'jar' task (and put it in the 'classes' dir) if you'd rather include it in the jar --> <copy file="${resources.dir}/log4j.properties" tofile="${dest.dir}/log4j.properties" overwrite="true" /> <copy file="${resources.dir}/${properties.file}" tofile="${dest.dir}/${properties.file}" overwrite="true" /> <delete dir="${dest.dir.classes}" /> </target>
I just added a bunch of comments to that build script, so I won't add anything else here, other than to say that this approach worked for me, and I hope it will work for you as well.