使用Ant构建新的Jar文件时如何包含外部Jar文件

38

我刚刚继承了一个Java项目,由于我没有Java背景,有时会有点迷失。在开发过程中,使用Eclipse进行调试和运行应用程序。我已经通过Eclipse成功创建了一个包含所有必需外部jar文件(如Log4J,xmlrpc-server等)的.jar文件。可以使用这个大的.jar文件来运行:

java -jar myjar.jar

我的下一步是使用Ant(版本1.7.1)自动化构建,这样我就不必涉及Eclipse来进行构建和部署。由于我的Java知识不足,这被证明是一个挑战。项目的根目录如下:

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

这是我的build.xml文件内容:

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

我接着运行了以下命令:

ant clean build-jar

然后一个名为seraph.jar的文件被放置在java/bin/jar目录下。接着我尝试用以下命令运行这个jar文件:

java -jar bin/jar/seraph.jar

控制台输出如下:

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.

我怀疑在build.xml文件中做了一些非常愚蠢的事情,并花费了两天时间尝试各种配置变化,但都没有成功。非常感谢任何有助于让它工作的帮助。

哦,如果我遗漏了一些关键信息,很抱歉,这是我第一次在 SO 发帖。


如果我是你,我会使用Maven 2或Maven 3代替Ant,但这也将是一项挑战。 - Paweł Dyda
我确实设法找到了一种可以完成我想要的事情的方法。请参见下面的详细信息。 - Christopher
8个回答

50

根据你的ant build文件,我猜想你想要创建一个单一的JAR档案,不仅包含你的应用程序类,还包括应用程序所需的其他JAR包的内容。

然而,你的build-jar文件只是将所需的JAR包放在自己的JAR中,这样做不会起作用,如此处所解释的那样(请参见注释)。

尝试修改这个:

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

变成这样:

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

如果上述方法无法满足您的要求,更灵活、更强大的解决方案是JarJarOne-Jar项目。请查阅这些项目。


这个会提取jar文件并将类文件包含在最终的jar中吗?它在我的构建文件中表现出相同的行为。我已经在这里发布了我的问题。 - Juzer Ali

15

在这里得到了一些有用的建议后,我开始研究One-Jar。经过一些死胡同(和一些结果与我的先前结果完全相同),我成功地让它工作了。为了其他人的参考,我列出了对我有效的build.xml。

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

我希望其他人也能从中受益。


嗨,旧回答,我知道,但是你的示例应该如何查看清单?编辑:明白了,one-jar会自己构建它。 - Alex Burdusel

2

Cheesle是正确的。类加载器无法找到嵌入式 jar 文件。如果您在命令行上添加足够的调试命令,您应该能够看到 'java' 命令未能将 jar 文件添加到类路径中。

您要创建的东西有时被称为“超级 jar”。我发现one-jar是一个可以帮助创建它们的工具,但我还没有尝试过。当然还有其他许多方法。


有人知道Eclipse用了什么来实现这个吗?到目前为止,Eclipse一直被用来构建这个应用程序,并且它总是生成一个包含所有内容的整洁包。如果能够用Ant实现相同的效果就太好了。我会看看one-jar和jarjar,看看是否可以将其集成到ant进程中。 - Christopher
我不得不说,你的这个建议对我来说是救命稻草。非常感谢,它运行得非常好! - djangofan
我也想知道Eclipse是怎么做的。 - Edward Falk
2
没关系,我已经解决了。这是答案:当Eclipse构建jar文件时,它会捆绑一个自己的小粘合剂,称为org.eclipse.jarinjar或类似的东西。这是一个自定义类加载器,知道如何从嵌入式jar文件中提取所需的类。我预计one-jar和JarJar也是这样做的。 - Edward Falk

0

我正在使用NetBeans,并需要一个解决方案。在谷歌搜索并从Christopher的答案开始,我成功地构建了一个脚本,帮助您在NetBeans中轻松完成此操作。我将在此处放置说明,以防其他人需要。

您需要做的是下载one-jar。您可以使用此链接:http://one-jar.sourceforge.net/index.php?page=getting-started&file=ant。提取jar存档文件并查找包含one-jar-ant-task-.jar、one-jar-ant-task.xml和one-jar-boot-.jar的one-jar\dist文件夹。将它们提取或复制到我们将添加到下面脚本中的路径,作为属性one-jar.dist.dir的值。

只需将以下脚本复制到您的build.xml脚本末尾(从您的NetBeans项目中),就在/project标记之前,用正确的路径替换one-jar.dist.dir的值并运行one-jar目标。

对于那些对运行目标不熟悉的人来说,这个教程可能会有所帮助:http://www.oracle.com/technetwork/articles/javase/index-139904.html。它还向您展示如何将源文件放入一个jar文件中,但它们是解压缩的,而不是压缩成jar文件。

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

祝你好运,如果这篇文章对你有所帮助,请不要忘记点赞。

0
正如Cheesle所说,您可以解压缩您的库Jars,并进行以下修改后重新打包。
    <jar destfile="${jar.file}"  
        basedir="${build.dir}"  
        manifest="${manifest.file}"> 
        <fileset dir="${classes.dir}" includes="**/*.class" /> 
        <zipgroupfileset dir="${lib.dir}" includes="**/*.jar" /> 
    </jar> 

Jar文件实际上只是嵌入了清单文件的zip文件。您可以提取和重新打包依赖Jars到应用程序的Jar文件中。

http://ant.apache.org/manual/Tasks/zip.html “Zip任务还支持将多个zip文件合并到zip文件中。这可以通过任何嵌套文件集的src属性或使用特殊的嵌套文件集zipgroupfileset来实现。”

请注意您的依赖库涉及的许可证。在法律上,外部链接到库和将库包含在应用程序中是非常不同的事情。

编辑1:我的打字速度太慢了。Grodriguez比我先发了。 :)

编辑2:如果您决定无法将依赖项包含在应用程序中,则必须在启动时通过命令行或通过清单文件指定它们在Jar的类路径中。 ANT中有一个很好的命令来处理Manifest文件中类路径的特殊格式。

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

0

你可以利用Ant Jar任务中已经内置的一些功能。

如果你前往Ant jar任务的文档,并滚动到“合并归档”部分,你会找到一个代码片段,可以将“lib/main”目录下所有jar文件中的*.class文件都包含进去:

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

这将创建一个可执行的jar文件,其中包含一个名为"com.acme.checksites.Main"的主类,并嵌入来自lib/main中所有jar包中的所有类。

在命名空间冲突、重复和类似情况下,它不会做任何聪明的事情。此外,它将包括所有类文件,包括您不使用的类文件,因此组合的jar文件将是完整大小。

如果您需要更高级的功能,例如one-jarjar jar links,请查看。


0

有两个选择,可以在类路径中引用新的jar文件,或者解压包含jar文件的所有类,然后重新打包整个程序集!据我所知,在jar文件中打包其他jar文件是不推荐的做法,你将永远面临找不到类的异常问题!


我只能相信你说的。我的Java技能不是很强。然而,我认为这个过程很简单,因为Eclipse可以正确地完成它。 ;) - Christopher

0
这是一个与类路径相关的问题,当运行一个可执行的jar文件时会出现如下情况:
java -jar myfile.jar

解决问题的一种方法是在Java命令行上设置类路径,如下所示,添加缺失的log4j jar:

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

当然,最好的解决方案是将类路径添加到JAR清单中,以便我们可以使用“-jar”Java选项:
<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>

以下答案演示了如何使用manifestclasspath自动设置类路径清单条目。

在使用Ant编译的文件中找不到主类


这显然不是原帖作者想要的。从他的 build.xml 文件中可以看出,他实际上想在生成的 jar 文件中包含所有必需的 jars。 - Grodriguez
没错,Grodriguez。我的目标是让我们的用户只需下载一个文件并运行它。将所有内容都包含在一个存档中使得操作变得简单。 - Christopher

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接