使用 Ant 在新的 jar 文件构建中包含外部 jar 文件

Posted

技术标签:

【中文标题】使用 Ant 在新的 jar 文件构建中包含外部 jar 文件【英文标题】:Including external jar-files in a new jar-file build with Ant 【发布时间】:2011-04-15 18:22:50 【问题描述】:

我刚刚“继承”了一个 Java 项目,而不是来自 Java 背景,有时我有点迷茫。 Eclipse 用于在开发过程中调试和运行应用程序。我通过 Eclipse 成功创建了一个 .jar 文件,该文件“包含”所有必需的外部 jar,如 Log4J、xmlrpc-server 等。然后可以使用以下方法成功运行这个大 .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,但这也是一个挑战。 我确实设法找到了一个好的方法来完成我想要的。详情见下文。 【参考方案1】:

根据您的 ant 构建文件,我假设您想要创建一个 JAR 存档,其中不仅包含您的应用程序类,还包含您的应用程序所需的其他 JAR 的内容。

但是,您的 build-jar 文件只是将所需的 JAR 放入您自己的 JAR 中;这不会像here 解释的那样工作(见注释)。

尝试修改这个:

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

更灵活、更强大的解决方案是JarJar 或One-Jar 项目。如果以上内容不满足您的要求,请查看这些内容。

【讨论】:

这是否会提取 jar 并将类文件包含在最终 jar 中。它在我的构建文件中表现出相同的行为。我已经发布了我的问题here。【参考方案2】:

根据在这里回答的人的有用建议,我开始深入研究 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 自己构建。【参考方案3】:

Cheesle 是对的。类加载器无法找到嵌入的 jar。如果您在命令行上放置了足够多的调试命令,您应该能够看到“java”命令无法将 jars 添加到类路径

您想要制作的东西有时被称为“uberjar”。我发现one-jar 是帮助制作它们的工具,但我还没有尝试过。当然还有很多其他方法。

【讨论】:

有人知道 Eclipse 使用什么来完成这个任务吗?到目前为止,Eclipse 一直被用来构建这个应用程序,并且它总是制作一个包含所有内容的简洁包。用 Ant 完成同样的事情会很棒。将查看 one-jar 和 jarjar 看看我是否可以将其挂钩到 ant 进程中。 我不得不说,你的这个建议对我来说是救命稻草。非常感谢.. 效果很好! 我也想知道 Eclipse 是怎么做到的。 没关系,我想通了。答案是这样的:当 Eclipse 构建 jar 文件时,它会捆绑在一个名为 org.eclipse.jarinjar 或类似名称的自己的小粘合剂中。它是一个自定义类加载器,它知道如何从嵌入的 jar 文件中提取所需的类。我希望 one-jar 和 JarJar 做同样的事情。【参考方案4】:

两个选项,要么引用类路径中的新 jar,要么解压缩封闭 jar 中的所有类并重新打包!据我所知,不推荐在罐子内包装罐子,并且您将永远拥有未找到类的异常!

【讨论】:

我只需要相信你。我的 java-fu 没那么强。但是我认为这个过程是微不足道的,因为 Eclipse 设法做到了。 ;)【参考方案5】:

正如 Cheesle 所说,您可以解压缩和您的库罐子,并通过以下修改重新将它们全部重新罐子。

    <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 文件。您可以将依赖项 Jar 提取并重新打包到应用程序的 Jar 文件中。

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

请注意与您的依赖库相关的许可证。从外部链接到库和在应用程序中包含库在法律上是完全不同的事情。

编辑 1:该死的我打字慢。 Grodriguez 打败了我。 :)

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

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

【讨论】:

【参考方案6】:

这是运行可执行 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 自动查看类路径清单条目

Cannot find Main Class in File Compiled With Ant

【讨论】:

这显然不是 OP 想要的。从他的 build.xml 文件看来,他实际上想在他生成的 jar 文件中包含所有必需的 jar。 这是正确的格罗德里格兹。我的目标是让我们的用户只下载一个文件并运行它。将所有内容都包含在一个存档中很容易。【参考方案7】:

我正在使用 NetBeans,并且还需要一个解决方案。在谷歌搜索并从克里斯托弗的回答开始之后,我设法构建了一个脚本,可以帮助您在 NetBeans 中轻松地做到这一点。我将说明放在这里以防其他人需要它们。

你要做的是下载一个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- 的 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>

祝你好运,如果对你有帮助,别忘了投票。

【讨论】:

【参考方案8】:

您可以使用一些已经内置到 ant jar 任务中的功能。

如果您转到The documentation for the ant jar task 并向下滚动到“合并档案”部分,则会有一个 sn-p 用于将所有 jar 中的所有 *.class 文件包含在“lib/main”目录中:

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

创建一个带有主类“com.acme.checksites.Main”的可执行 jar 文件,并将所有 jar 中的所有类嵌入 lib/main 中。

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

如果你需要更高级的东西,看看one-jar和jar jar links

【讨论】:

以上是关于使用 Ant 在新的 jar 文件构建中包含外部 jar 文件的主要内容,如果未能解决你的问题,请参考以下文章

无法使用 Ant 从外部 jar 构建多个 Android dex 文件

如何使用Ant解决模式解析异常?

Android 项目使用 Ant 构建良好,但出现运行时错误

无法在 Windows 上使用 jar 命令

使用 ant 项目的 jenkins 集成在 svn 上生成 jar

Eclipse:如何使用外部 jar 构建可执行 jar?