开发者

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

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 r开发者_JS百科equired 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.


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.


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.


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.


Two options, either reference the new jars in your classpath or unpack all classes in the enclosing jars and re-jar the whole lot! As far as I know packaging jars within jars is not recommeneded and you'll forever have the class not found exception!


As Cheesle said, you can unpack and your library Jars and re-jar them all with the following modification.

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

Jar files are really just zip files with a manifest file embedded. You can extract and repackage the dependency Jars into your application's Jar file.

http://ant.apache.org/manual/Tasks/zip.html "The Zip task also supports the merging of multiple zip files into the zip file. This is possible through either the src attribute of any nested filesets or by using the special nested fileset zipgroupfileset."

Do pay attention to the licenses involved with your dependency libaries. Linking externally to a library and including the library in your application are very different things legally.

EDIT 1: Darn my slow typing. Grodriguez beat me to it. :)

EDIT 2: If you decide you can't include your dependencies into your application then you have to specify them in your Jar's classpath either at the command line at startup or via the Manifest file. There's a nice command in ANT to handle the special formatting of the classpath in a Manifest file for you.

<manifestclasspath property="manifest.classpath" jarfile="${jar.file}">
    <classpath location="${lib.dir}" />
</manifestclasspath>

 <manifest file="${manifest.file}" >      
        <attribute name="built-by" value="${user.name}" />      
        <attribute name="Main-Class" value="${main.class}" />    
        <attribute name="Class-Path" value="${manifest.classpath}" />
 </manifest>


This is a classpath issue when running an executable jar as follows:

java -jar myfile.jar

One way to fix the problem is to set the classpath on the java command line as follows, adding the missing log4j jar:

java -cp myfile.jar:log4j.jar:otherjar.jar com.abc.xyz.MyMainClass

Of course the best solution is to add the classpath into the jar manifest so that the we can use the "-jar" java option:

<jar jarfile="myfile.jar">
    ..
    ..
    <manifest>
       <attribute name="Main-Class" value="com.abc.xyz.MyMainClass"/>
       <attribute name="Class-Path" value="log4j.jar otherjar.jar"/>
    </manifest>
</jar>

The following answer demonstrates how you can use the manifestclasspath to automate the seeting of the classpath manifest entry

Cannot find Main Class in File Compiled With Ant


I'm using NetBeans and needed a solution for this also. After googleling around and starting from Christopher's answer i managed to build a script that helps you easily do this in NetBeans. I'm putting the instructions here in case someone else will need them.

What you have to do is download one-jar. You can use the link from here: http://one-jar.sourceforge.net/index.php?page=getting-started&file=ant Extract the jar archive and look for one-jar\dist folder that contains one-jar-ant-task-.jar, one-jar-ant-task.xml and one-jar-boot-.jar. Extract them or copy them to a path that we will add to the script below, as the value of the property one-jar.dist.dir.

Just copy the following script at the end of your build.xml script (from your NetBeans project), just before /project tag, replace the value for one-jar.dist.dir with the correct path and run one-jar target.

For those of you that are unfamiliar with running targets, this tutorial might help: http://www.oracle.com/technetwork/articles/javase/index-139904.html . It also shows you how to place sources into one jar, but they are exploded, not compressed into jars.

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

<target name="one-jar" depends="jar">
<property name="debuglevel" value="source,lines,vars"/>
<property name="target" value="1.6"/>
<property name="source" value="1.6"/>
<property name="src.dir"          value="src"/>
<property name="bin.dir"          value="bin"/>
<property name="build.dir"        value="build"/>
<property name="dist.dir"         value="dist"/>
<property name="external.lib.dir" value="${dist.dir}/lib"/>
<property name="classes.dir"      value="${build.dir}/classes"/>
<property name="jar.target.dir"   value="${build.dir}/jars"/>
<property name="final.jar"        value="${dist.dir}/${ant.project.name}.jar"/>
<property name="main.class"       value="${main.class}"/>

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

<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>

<!-- <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>

<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>

<delete dir="${jar.target.dir}"/>
<delete dir="${bin.dir}"/>
<delete dir="${external.lib.dir}"/>

</target>

Best of luck and don't forget to vote up if it helped you.


You can use a bit of functionality that is already built in to the ant jar task.

If you go to The documentation for the ant jar task and scroll down to the "merging archives" section there's a snippet for including the all the *.class files from all the jars in you "lib/main" directory:

<jar destfile="build/main/checksites.jar">
    <fileset dir="build/main/classes"/>
    <restrict>
        <name name="**/*.class"/>
        <archives>
            <zips>
                <fileset dir="lib/main" includes="**/*.jar"/>
            </zips>
        </archives>
    </restrict>
    <manifest>
      <attribute name="Main-Class" value="com.acme.checksites.Main"/>
    </manifest>
</jar>

This Creates an executable jar file with a main class "com.acme.checksites.Main", and embeds all the classes from all the jars in lib/main.

It won't do anything clever in case of namespace conflicts, duplicates and things like that. Also, it will include all class files, also the ones that you don't use, so the combined jar file will be full size.

If you need more advanced things like that, take a look at like one-jar and jar jar links

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜