(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:
Someone kindly shouted Thank You for this post. I appreciate that. Unfortunately, I accidentally deleted the comment while removing some ad-spam. Sorry!
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!
Great! thank you
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. :)
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.
Using maven2 you can make the job without one-jar, by using the
maven-assembly-plugin
and the descriptorRef
jar-with-dependencies
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.
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 :)
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
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").
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.
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 :)
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.
Note: Doesn't work with CDI - at least Weld - screws it's classloading.
Doesn't work with CDI - breaks Weld's classloadng.
OK, great.
But how do you do this in netbeans7 WITHOUT manual work? After all, it is 2011...
Post a Comment