Including external jar-files in a new jar-file build with Ant

112,031

Solution 1

With the helpful advice from people who have answered here I started digging into One-Jar. After some dead-ends (and some results that were exactly like my previous results I managed to get it working. For other peoples reference I'm listing the build.xml that worked for me.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project basedir="." default="build" name="<INSERT_PROJECT_NAME_HERE>">
    <property environment="env"/>
    <property name="debuglevel" value="source,lines,vars"/>
    <property name="target" value="1.6"/>
    <property name="source" value="1.6"/>

    <property name="one-jar.dist.dir" value="../onejar"/>
    <import file="${one-jar.dist.dir}/one-jar-ant-task.xml" optional="true" />

    <property name="src.dir"          value="src"/>
    <property name="bin.dir"          value="bin"/>
    <property name="build.dir"        value="build"/>
    <property name="classes.dir"      value="${build.dir}/classes"/>
    <property name="jar.target.dir"   value="${build.dir}/jars"/>
    <property name="external.lib.dir" value="../jars"/>
    <property name="final.jar"        value="${bin.dir}/<INSERT_NAME_OF_FINAL_JAR_HERE>"/>

    <property name="main.class"       value="<INSERT_MAIN_CLASS_HERE>"/>

    <path id="project.classpath">
        <fileset dir="${external.lib.dir}">
            <include name="*.jar"/>
        </fileset>
    </path>

    <target name="init">
        <mkdir dir="${bin.dir}"/>
        <mkdir dir="${build.dir}"/>
        <mkdir dir="${classes.dir}"/>
        <mkdir dir="${jar.target.dir}"/>
        <copy includeemptydirs="false" todir="${classes.dir}">
            <fileset dir="${src.dir}">
                <exclude name="**/*.launch"/>
                <exclude name="**/*.java"/>
            </fileset>
        </copy>
    </target>

    <target name="clean">
        <delete dir="${build.dir}"/>
        <delete dir="${bin.dir}"/>
    </target>

    <target name="cleanall" depends="clean"/>

    <target name="build" depends="init">
        <echo message="${ant.project.name}: ${ant.file}"/>
        <javac debug="true" debuglevel="${debuglevel}" destdir="${classes.dir}" source="${source}" target="${target}">
            <src path="${src.dir}"/>
            <classpath refid="project.classpath"/>   
        </javac>
    </target>

    <target name="build-jar" depends="build">
        <delete file="${final.jar}" />
        <one-jar destfile="${final.jar}" onejarmainclass="${main.class}">
            <main>
                <fileset dir="${classes.dir}"/>
            </main>
            <lib>
                <fileset dir="${external.lib.dir}" />
            </lib>
        </one-jar>
    </target>
</project>

I hope someone else can benefit from this.

Solution 2

From your ant buildfile, I assume that what you want is to create a single JAR archive that will contain not only your application classes, but also the contents of other JARs required by your application.

However your build-jar file is just putting required JARs inside your own JAR; this will not work as explained here (see note).

Try to modify this:

<jar destfile="${jar.file}" 
    basedir="${build.dir}" 
    manifest="${manifest.file}">
    <fileset dir="${classes.dir}" includes="**/*.class" />
    <fileset dir="${lib.dir}" includes="**/*.jar" />
</jar>

to this:

<jar destfile="${jar.file}" 
    basedir="${build.dir}" 
    manifest="${manifest.file}">
    <fileset dir="${classes.dir}" includes="**/*.class" />
    <zipgroupfileset dir="${lib.dir}" includes="**/*.jar" />
</jar>

More flexible and powerful solutions are the JarJar or One-Jar projects. Have a look into those if the above does not satisfy your requirements.

Solution 3

Cheesle is right. There's no way for the classloader to find the embedded jars. If you put enough debug commands on the command line you should be able to see the 'java' command failing to add the jars to a classpath

What you want to make is sometimes called an 'uberjar'. I found one-jar as a tool to help make them, but I haven't tried it. Sure there's many other approaches.

Share:
112,031
Christopher
Author by

Christopher

Engineer from Bergen, Norway. Want to know more? Ask. You will also find me on Twitter as @christopherraa.

Updated on July 17, 2020

Comments

  • Christopher
    Christopher almost 4 years

    I have just 'inherited' a Java-project and not coming from a Java-background I am a little lost at times. Eclipse is used to debug and run the application during development. I have through Eclipse succeeded in creating a .jar-file that 'includes' all the required external jars like Log4J, xmlrpc-server, etc. This big .jar can then be run successfully using:

    java -jar myjar.jar
    

    My next step is to automate builds using Ant (version 1.7.1) so I don't have to involve Eclipse to do builds and deployment. This has proven to be a challenge due to my lacking java-knowledge. The root of the project looks like this:

    |-> jars (where external jars have been placed)
    |-> java
    | |-> bin (where the finished .class / .jars are placed)
    | |-> src (Where code lives)
    | |-> ++files like build.xml etc
    |-> sql (you guessed it; sql! )
    

    My build.xml contains the following:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <project basedir="." default="build" name="Seraph">
        <property environment="env"/>
        <property name="debuglevel" value="source,lines,vars"/>
        <property name="target" value="1.6"/>
        <property name="source" value="1.6"/>
    
        <property name="build.dir"     value="bin"/>
        <property name="src.dir"       value="src"/>
        <property name="lib.dir"       value="../jars"/>
        <property name="classes.dir"   value="${build.dir}/classes"/>
        <property name="jar.dir"       value="${build.dir}/jar"/>
        <property name="jar.file"      value="${jar.dir}/seraph.jar"/>
        <property name="manifest.file" value="${jar.dir}/MANIFEST.MF"/>
    
        <property name="main.class" value="no.easyconnect.seraph.core.SeraphCore"/>
    
        <path id="external.jars">
            <fileset dir="${lib.dir}" includes="**/*.jar"/>
        </path>
    
        <path id="project.classpath">
            <pathelement location="${src.dir}"/>
            <path refid="external.jars" />
        </path>
    
        <target name="init">
            <mkdir dir="${build.dir}"/>
            <mkdir dir="${classes.dir}"/>
            <mkdir dir="${jar.dir}"/>
            <copy includeemptydirs="false" todir="${build.dir}">
                <fileset dir="${src.dir}">
                    <exclude name="**/*.launch"/>
                    <exclude name="**/*.java"/>
                </fileset>
            </copy>
        </target>
    
        <target name="clean">
            <delete dir="${build.dir}"/>
        </target>
    
        <target name="cleanall" depends="clean"/>
    
        <target name="build" depends="init">
            <echo message="${ant.project.name}: ${ant.file}"/>
            <javac debug="true" debuglevel="${debuglevel}" destdir="bin" source="${source}" target="${target}" classpathref="project.classpath">
                <src path="${src.dir}"/>
            </javac>
        </target>
    
        <target name="build-jar" depends="build">
            <delete file="${jar.file}" />
            <delete file="${manifest.file}" />
    
            <manifest file="${manifest.file}" >
                <attribute name="built-by" value="${user.name}" />
                <attribute name="Main-Class" value="${main.class}" />
            </manifest>
    
            <jar destfile="${jar.file}" 
                basedir="${build.dir}" 
                manifest="${manifest.file}">
                <fileset dir="${classes.dir}" includes="**/*.class" />
                <fileset dir="${lib.dir}" includes="**/*.jar" />
            </jar>
        </target>
    </project>
    

    I then run: ant clean build-jar

    and a file named seraph.jar is placed in the java/bin/jar-directory. I then try to run this jar using the following command: java -jar bin/jar/seraph.jar

    The result is this output at the console:

    Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/log4j/Logger
        at no.easyconnect.seraph.core.SeraphCore.<clinit>(SeraphCore.java:23)
    Caused by: java.lang.ClassNotFoundException: org.apache.log4j.Logger
        at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:252)
        at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:320)
        ... 1 more
    Could not find the main class: no.easyconnect.seraph.core.SeraphCore. Program will exit.
    

    I suspect that I have done something amazingly silly in the build.xml-file and have spent the better part of two days trying variations on the configuration, to no avail. Any help on getting this working is greatly appreciated.

    Oh, and I'm sorry if I left some crucial information out. This is my first time posting here at SO.

  • Grodriguez
    Grodriguez over 13 years
    This is not what the OP apparently wants. From his build.xml file it looks like he wants to actually include all required jars in the jar file he's generating.
  • Christopher
    Christopher over 13 years
    Anyone know exactly what Eclipse uses to accomplish this? Up until now Eclipse has been used to build this application and it has always made one neat package with everything included. Would be wonderful to accomplish the same with Ant. Will have a look at one-jar and jarjar to see if I can hook that into the ant-process.
  • Christopher
    Christopher over 13 years
    I'll just have to believe you on this. My java-fu is not so strong. However I had though that the process was trivial since Eclipse manages to do it right. ;)
  • Christopher
    Christopher over 13 years
    That's correct Grodriguez. My goal is to let our users download only one file and run that. Having everything contained in one archive makes it easy.
  • djangofan
    djangofan almost 13 years
    I have to say that this suggestion of yours was a life-saver for me. Thanks very much.. it works great!
  • Edward Falk
    Edward Falk over 12 years
    I too would like to know how Eclipse does it.
  • Edward Falk
    Edward Falk over 12 years
    Never mind, I figured it out. Here's the answer: When Eclipse builds the jar file, it bundles in a little glue of its own called org.eclipse.jarinjar or something like that. It's a custom class loader that knows how to extract the needed classes from the embedded jar file. I expect that one-jar and JarJar do the same thing.
  • Juzer Ali
    Juzer Ali over 11 years
    Does this extracts the jar and includes the class files in final jar. Its exhibiting same behaviour in my build file. I have posted my question here.
  • Alex Burdusel
    Alex Burdusel almost 11 years
    Hi, old answer, i know, but how should the manifest look for your example? Edit: Got it, one-jar builds it itself.