Wednesday, December 27, 2006

Making a one-jar with Maven

(This is a lengthier post than usual. You can skip forward to the finished example, if you like.)

UPDATE:The One-JAR author comments:

Nice article, thanks for using (and persisting with) One-JAR. I just released a new version (0.97), checkout http://one-jar.sourceforge.net/

The one-jar-maven plugin has been updated with this new release, and there is also a new project in the CVS repository showing how to use maven2 to build (http://one-jar.cvs.sourceforge.net/viewvc/one-jar/one-jar-maven/)

A major part of this release was making it easier to set up One-JAR projects, to which end there is an application generator (one-jar-appgen) which will create a basic One-JAR directory tree, with support for building under Eclipse/Ant, and which contains a JUnit test harness. I’d be interested in hearing your thoughts on this if you’re still working with the product.

One-JAR

I recently came across Simon Tuffs' One-JAR Java project. It uses a custom "boot" classloader to place whole JARs within JARs and fix up the classpath accordingly.

The standard classloader will not do this and requires that supporting JARs be external to each other. So if I have Main.jar with dependency log4j.jar where Main.jar holds the main entry point—hm.binkley.Main, say—I cannot bundle log4j.jar into Main.jar but must distribute it alongside separately.

This has no effect on running Java, but changes distribution as I can no longer provide a single file (Main.jar in this example) for others to run.

Contrast these two ZIP files:

  • Foo-1.0-with-one-jar.zip:
    • foo-1.0/Main.jar
    • foo-1.0/README
  • Foo-1.0-without-one-jar.zip:
    • foo-1.0/Main.jar
    • foo-1.0/log4j.jar
    • foo-1.0/README

In this example, it does not seem to make a lot of difference; but when a project pulls in several Jakarta Commons JARs, several other open-source JARs, and several in-house, proprietary JARs, it becomes very obvious that distribution is an issue. One project I work on has a java command line between 5000 and 6000 characters long because of external jar dependencies in the classpath.

A traditional solution is to repack all the classes, internal and external, in to a single über-JAR, losing the independent qualities of each JAR including interesting MANIFEST.MF entries.

One-JAR solves this elegantly by packing everything into one JAR without unpacking the dependencies. My command line becomes just:

$ java [-options] -jar Main.jar

Ant

An Ant script for One-JAR is simple. One-JAR requires that you pack your original Main.jar into the final, single JAR along with dependencies:

  • main/Main.jar
  • lib/log4j.jar

They pack in with the One-JAR classes.

One-JAR then provides a custom classloader and alternative main entry point to glue it all together and a custom URL for classloading, onejar:.

As One-JAR provides you with a prototype outer JAR, all you need do is update the prototype, adding in main/Main.jar and lib/log4j.jar with Main.jar!main/Main.jar!META-INF/MANIFEST.MF unchanged:.

Manifest-Version: 1.0
Main-Class: hm.binkley.Main
Class-Path: log4j.jar

Leave unchanged the Main.jar!META-INF/MANIFEST.MF provided in the prototype:

Manifest-Version: 1.0
Main-Class: com.simontuffs.onejar.Boot

(Note that the first Main.jar is the prototype copied from one-jar-boot-0.95.jar, provided with the One-JAR download, and the second Main.jar is your original executable JAR.)

One-JAR follows the convention that your "real" JAR is in main/ and all dependency JARs are elsewhere within your single, distributable JAR. Nothing is unpacked.

The jar task handles this ably:

<target name="one-jar" depends="jar" description="Build one ONE-JAR">
    <property name="onejardir" location="${pom.build.directory}/one-jar"/>
    <mkdir dir="${onejardir}/main"/>
    <mkdir dir="${onejardir}/lib"/>

    <copy tofile="${onejardir}/${jarfile}"
            file="lib/one-jar-boot-0.95.jar"/>
    <copy todir="${onejardir}/main"
            file="${pom.build.directory}/${jarfile}"/>
    <copy todir="${onejardir}/lib" flatten="true">
        <fileset dir="lib" includes="*.jar"
                excludes="one-jar-boot-0.95.jar"/>
        <fileset refid="runtime.dependency.fileset"/>
    </copy>

    <jar jarfile="${onejardir}/${jarfile}" update="true"
            basedir="${onejardir}" excludes="${jarfile}"/>
</target>

But what of Maven?

Maven

Where Ant asks, How do I make this cake? - Maven asks Where can I find cake ingredients? But Maven does provide a mechanism for adding new recipes, the assembly plugin.

An assembly is a description of packaging for Maven, and is usually hooked into the package phase in your build lifecycle. (The assembly plugin is much simpler than adding new packaging with Plexus, the method described in the link.)

To build a One-JAR with Maven and the assembly plugin, add a new assembly descriptor which follows the outline of building with Ant, taking care to unpack the One-JAR prototype JAR and add to it Main.jar and its dependencies.

This is better explained by example.

Example

The sources

Unfortunately, One-JAR is not in the Maven central repository, so it is included here as part of the project. That is the reason for the added repository in the POM and the lib/ files. Ignoring directories:

  • lib/com/simontuffs/one-jar/0.95/one-jar-0.95.jar
  • lib/com/simontuffs/one-jar/0.95/one-jar-0.95.pom
  • pom.xml
  • src/assembly/one-jar.xml
  • src/main/java/hm/binkley/Main.java
  • src/main/resources/log4j.properties

The POM

<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hm.binkley</groupId>
    <artifactId>Main</artifactId>
    <packaging>jar</packaging>
    <version>1.0</version>
    <name>One-JAR Example</name>
    <url>http://binkley.blogspot.com/</url>

    <repositories>
        <repository>
            <id>project</id>
            <name>Project Repository</name>
            <url>file:///${basedir}/lib</url>
            <layout>default</layout>
        </repository>
    </repositories>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>hm.binkley.Main</mainClass>
                            <addClasspath>true</addClasspath>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.simontuffs.onejar.Boot</mainClass>
                        </manifest>
                    </archive>
                    <descriptors>
                        <descriptor>src/assembly/one-jar.xml</descriptor>
                    </descriptors>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>attached</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>com.simontuffs</groupId>
            <artifactId>one-jar</artifactId>
            <version>0.95</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.14</version>
        </dependency>
    </dependencies>
</project>

The assembly

In src/main/assembly/one-jar.xml:

<assembly>
    <id>one-jar</id>
    <formats>
        <format>jar</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <dependencySets>
        <dependencySet>
            <outputDirectory/>
            <unpack>true</unpack>
            <includes>
                <include>com.simontuffs:one-jar</include>
            </includes>
        </dependencySet>
        <dependencySet>
            <outputDirectory>main</outputDirectory>
            <includes>
                <include>${groupId}:${artifactId}</include>
            </includes>
        </dependencySet>
        <dependencySet>
            <outputDirectory>lib</outputDirectory>
            <scope>runtime</scope>
            <excludes>
                <exclude>com.simontuffs:one-jar</exclude>
                <exclude>${groupId}:${artifactId}</exclude>
            </excludes>
        </dependencySet>
    </dependencySets>
</assembly>

The finished One-JAR

  • META-INF/MANIFEST.MF
  • com/simontuffs/onejar/Boot.class
  • com/simontuffs/onejar/Boot.java
  • com/simontuffs/onejar/Handler$1.class
  • com/simontuffs/onejar/Handler.class
  • com/simontuffs/onejar/Handler.java
  • com/simontuffs/onejar/JarClassLoader$ByteCode.class
  • com/simontuffs/onejar/JarClassLoader.class
  • com/simontuffs/onejar/JarClassLoader.java
  • doc/one-jar-license.txt
  • main/Main-1.0.jar
  • lib/log4j-1.2.14.jar

16 comments:

binkley said...

Someone kindly shouted Thank You for this post. I appreciate that. Unfortunately, I accidentally deleted the comment while removing some ad-spam. Sorry!

Anonymous said...

Fhis info is so helpful...all the other sites just rehash the onjar-supplied docs, you actually explain it within the context of the actual build process (ant/maven). Thanks!

Anonymous said...

Great! thank you

binkley said...

Thanks, again.

The reason I posted these details is exactly what you said -- I couldn't find this information anywhere else, and I needed somewhere to refer to on the Web for instructions.

I often use my own blog to save interesting bits that otherwise leak out of my cluttered mind. :)

Ichi said...

Hmm, I'm not sure that your maven code is working, what version of assembly plugin are you using? Maybe it's worth specifying in the POM.

Markus Heidt said...

Using maven2 you can make the job without one-jar, by using the

maven-assembly-plugin
and the descriptorRef
jar-with-dependencies

binkley said...

Ah, Maven2--much improved. Actually, although you can build a single jar for packaging the dependencies, that one jar alone is not "executable" as "java -jar the-one.jar" which is the point of the post.

Markus Heidt said...

it is executable as java -jar, but the dependencies are extracted, so that everything runs within the jar-context.
Hence if you need an external log4j.properties-file, log-directories or config-files, the one-jar approach is the way to go.
At least I don't know how to solve those issues :)

cobinrox said...

So has anyone successfully executed a one-jar'ed super jar with a log4j configuration file that was not inside of the super jar itself? I can't seem to get log4j to work unless I package up the config file inside of the default package of the super jar file. The problem seems to be that when the super.jar file is executed, the classpath gets hijacked and any command-line parameters passed specifiying location of a classpath (for the log4j configuration file location, for example) is trounced on. For example, this won't work:
java -classpath=C:/myconffiles -Dlog4j.configuration=my.log4j.properties -jar mysuperonefile.jar

Just wondered if anyone else has run across this. Thanks either way

noah said...

This doesn't work for me in Maven 2.0.6

I had to use:
<dependencySet>
<outputFileNameMapping></outputFileNameMapping> <unpack>true</unpack>
<includes>
<include>com.simontuffs:one-jar</include>
</includes>
</dependencySet>
Otherwise, it would try to unpack it into a one-jar-0.95 subdirectory.


And <include>${groupId}:${artifactId}</include>
doesn't match anything. (Maven sez "The following patterns were never triggered in this artifact inclusion filter: myGroup:myArtifact").

thomas said...

WOW. With a little tweaking of your example (yes, the noah is right about "outputFileNameMapping"), I finally managed to make a fairly generic skeleton for my future one-jars! And it works! Thanks a lot.

Amon said...

I had the same issue Noah stated...so my dependent jars were included, but it was dropping my jar in the main directory. But this started me down the google search that led to: http://www.dstovall.org/onejar-maven-plugin/index.html

A maven plugin for one-jar...very simple and it works :)

Simon said...

Nice article, thanks for using (and persisting with) One-JAR. I just released a new version (0.97), checkout http://one-jar.sourceforge.net/

The one-jar-maven plugin has been updated with this new release, and there is also a new project in the CVS repository showing how to use maven2 to build (http://one-jar.cvs.sourceforge.net/viewvc/one-jar/one-jar-maven/)

A major part of this release was making it easier to set up One-JAR projects, to which end there is an application generator (one-jar-appgen) which will create a basic One-JAR directory tree, with support for building under Eclipse/Ant, and which contains a JUnit test harness. I’d be interested in hearing your thoughts on this if you’re still working with the product.

–simon.

Ondra Žižka said...

Note: Doesn't work with CDI - at least Weld - screws it's classloading.

Ondra Žižka said...

Doesn't work with CDI - breaks Weld's classloadng.

Anonymous said...

OK, great.

But how do you do this in netbeans7 WITHOUT manual work? After all, it is 2011...