教程:使用 Apache Ant 编写 Hello World

本文档提供了一个使用 Apache Ant 开始 Java 编程的逐步教程。它**不**包含关于 Java 或 Ant 的更深入的知识。本教程的目标是让你了解如何在 Ant 中执行最简单的步骤。

内容

准备项目

我们希望将源代码与生成的代码分开,因此我们的 Java 源代码文件将位于 src 文件夹中。所有生成的代码都应该位于 build 文件夹下,并根据各个步骤分成几个子文件夹:classes 用于编译后的文件,jar 用于我们自己的 JAR 文件。

我们只需要创建 src 目录。(因为我在 Windows 上工作,所以这里使用的是 Windows 语法,请将其翻译成你的 shell)

md src

以下简单的 Java 类只是将固定消息输出到 STDOUT,因此只需将此代码写入 src\oata\HelloWorld.java 中。

package oata;

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

现在尝试编译并运行它

md build\classes
javac -sourcepath src -d build\classes src\oata\HelloWorld.java
java -cp build\classes oata.HelloWorld
这将导致
Hello World

创建 jar 文件并不困难。但是创建可启动的 jar 文件需要更多步骤:创建包含启动类的清单文件,创建目标目录并归档文件。

echo Main-Class: oata.HelloWorld>myManifest
md build\jar
jar cfm build\jar\HelloWorld.jar myManifest -C build\classes .
java -jar build\jar\HelloWorld.jar

注意:在 echo Main-Class 指令中不要在 > 符号周围留空格,因为这会导致错误!

运行应用程序的四个步骤

完成 Java 相关的步骤后,我们必须考虑我们的构建过程。我们必须编译我们的代码,否则我们就无法启动程序。哦,"启动",是的,我们可以为此提供一个目标。我们应该打包我们的应用程序。现在它只是一个类,但是如果你想提供下载,没有人会下载数百个文件...(想想一个复杂的 Swing GUI,所以让我们创建一个 jar 文件。一个可启动的 jar 文件会很好... 而且养成一个 "clean" 目标的习惯很好,它会删除所有生成的代码。许多错误可以通过 "clean build" 解决。

默认情况下,Ant 使用 build.xml 作为构建文件的名称,因此我们的 .\build.xml 将是

<project>

    <target name="clean">
        <delete dir="build"/>
    </target>

    <target name="compile">
        <mkdir dir="build/classes"/>
        <javac srcdir="src" destdir="build/classes"/>
    </target>

    <target name="jar">
        <mkdir dir="build/jar"/>
        <jar destfile="build/jar/HelloWorld.jar" basedir="build/classes">
            <manifest>
                <attribute name="Main-Class" value="oata.HelloWorld"/>
            </manifest>
        </jar>
    </target>

    <target name="run">
        <java jar="build/jar/HelloWorld.jar" fork="true"/>
    </target>

</project>

现在你可以通过以下命令编译、打包和运行应用程序

ant compile
ant jar
ant run

或者更简短地使用

ant compile jar run

在查看构建文件时,我们会发现 Ant 和 Java 相关的命令之间有一些类似的步骤

Java 相关 Ant
md build\classes
javac
    -sourcepath src
    -d build\classes
    src\oata\HelloWorld.java
echo Main-Class: oata.HelloWorld>mf
md build\jar
jar cfm
    build\jar\HelloWorld.jar
    mf
    -C build\classes
    .



java -jar build\jar\HelloWorld.jar
<mkdir dir="build/classes"/>
<javac
    srcdir="src"
    destdir="build/classes"/>
<!-- automatically detected -->
<!-- obsolete; done via manifest tag -->
<mkdir dir="build/jar"/>
<jar
    destfile="build/jar/HelloWorld.jar"

    basedir="build/classes">
    <manifest>
        <attribute name="Main-Class" value="oata.HelloWorld"/>
    </manifest>
</jar>
<java jar="build/jar/HelloWorld.jar" fork="true"/>

增强构建文件

现在我们有了可用的构建文件,我们可以进行一些增强:很多时候你会引用相同的目录,主类和 jar 文件名是硬编码的,并且在调用时你必须记住构建步骤的正确顺序。

第一个和第二个问题可以通过属性解决,第三个问题可以通过一个特殊的属性解决,它是 <project> 标签的一个属性,第四个问题可以通过依赖关系解决。

<project name="HelloWorld" basedir="." default="main">

    <property name="src.dir"     value="src"/>

    <property name="build.dir"   value="build"/>
    <property name="classes.dir" value="${build.dir}/classes"/>
    <property name="jar.dir"     value="${build.dir}/jar"/>

    <property name="main-class"  value="oata.HelloWorld"/>



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

    <target name="compile">
        <mkdir dir="${classes.dir}"/>
        <javac srcdir="${src.dir}" destdir="${classes.dir}"/>
    </target>

    <target name="jar" depends="compile">
        <mkdir dir="${jar.dir}"/>
        <jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="${main-class}"/>
            </manifest>
        </jar>
    </target>

    <target name="run" depends="jar">
        <java jar="${jar.dir}/${ant.project.name}.jar" fork="true"/>
    </target>

    <target name="clean-build" depends="clean,jar"/>

    <target name="main" depends="clean,run"/>

</project>

现在更容易了,只需执行 ant,你将获得

Buildfile: build.xml

clean:

compile:
    [mkdir] Created dir: C:\...\build\classes
    [javac] Compiling 1 source file to C:\...\build\classes

jar:
    [mkdir] Created dir: C:\...\build\jar
      [jar] Building jar: C:\...\build\jar\HelloWorld.jar

run:
     [java] Hello World

main:

BUILD SUCCESSFUL

使用外部库

有人告诉我们不要使用 System 语句。对于输出,我们应该使用一个日志记录 API,它可以高度定制(包括在正常运行时(即非开发时)执行时关闭)。我们使用 Log4J,因为

我们将外部库存储在一个新的目录 lib 中。Log4J 可以从 Logging 的主页下载 [1]。创建 lib 目录并将 log4j-1.2.17.jar 解压缩到该目录中。之后,我们必须修改 Java 源代码文件以使用该库,以及修改构建文件,以便在编译和运行时可以访问该库。

Log4J 的使用方式在它的手册中有所说明。这里我们使用 简短手册 [2] 中的 MyApp 示例。首先,我们必须修改 Java 源代码以使用日志记录框架

package oata;

import org.apache.log4j.Logger;
import org.apache.log4j.BasicConfigurator;

public class HelloWorld {
    static Logger logger = Logger.getLogger(HelloWorld.class);

    public static void main(String[] args) {
        BasicConfigurator.configure();
        logger.info("Hello World");          // the old SysO-statement
    }
}

大多数修改都是 "框架开销",只需要做一次。蓝色的行是我们的 "旧的 System-out" 语句。

不要尝试运行 ant,你只会得到很多编译错误。Log4J 不在类路径中,因此我们必须做一些工作。但是不要更改 CLASSPATH 环境变量!这只是针对这个项目,你可能会破坏其他环境(这是使用 Ant 时最常见的错误之一)。我们将 Log4J(或者更准确地说,是所有位于 .\lib 下的库(jar 文件))引入到我们的构建文件中

<project name="HelloWorld" basedir="." default="main">
    ...
    <property name="lib.dir"     value="lib"/>

    <path id="classpath">
        <fileset dir="${lib.dir}" includes="**/*.jar"/>
    </path>

    ...

    <target name="compile">
        <mkdir dir="${classes.dir}"/>
        <javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath"/>
    </target>

    <target name="run" depends="jar">
        <java fork="true" classname="${main-class}">
            <classpath>
                <path refid="classpath"/>
                <path location="${jar.dir}/${ant.project.name}.jar"/>
            </classpath>
        </java>
    </target>

    ...

</project>

在这个例子中,我们不是通过它的 Main-Class 清单属性启动应用程序,因为我们无法提供 jar 文件名类路径。因此,将我们的类添加到红色行中已定义的路径中,并像往常一样启动。运行 ant 将会得到(在通常的编译步骤之后)

[java] 0 [main] INFO oata.HelloWorld  - Hello World

这是什么?

对于其他布局... 请查看 Log4J 文档中关于使用其他 PatternLayouts 的内容。

配置文件

为什么我们使用 Log4J?"它高度可配置"?不,所有内容都是硬编码的!但这并不是 Log4J 的错,而是我们的错。我们编码了 BasicConfigurator.configure();,这意味着一个简单但硬编码的配置。更方便的是使用属性文件。在 Java 源代码文件中,从 main() 方法(以及相关的 import 语句)中删除 BasicConfiguration 行。Log4J 然后会搜索它的手册中描述的配置。然后创建一个新的文件 src/log4j.properties。这是 Log4J 配置的默认名称,使用该名称会让生活更轻松,不仅框架知道里面是什么,你也知道!

log4j.rootLogger=DEBUG, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender

log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%m%n

此配置创建一个名为 stdout 的控制台输出通道("Appender"),它打印消息("%m")后跟一个换行符("%n"),与之前的 System.out.println() 一样 :-) 哦,好吧,但我们还没有完成。我们也应该提供配置文件。因此,我们修改构建文件

    ...
    <target name="compile">
        <mkdir dir="${classes.dir}"/>
        <javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath"/>
        <copy todir="${classes.dir}">
            <fileset dir="${src.dir}" excludes="**/*.java"/>
        </copy>
    </target>
    ...

这会将所有资源(只要它们没有 .java 后缀)复制到构建目录,因此我们可以从该目录启动应用程序,这些文件将被包含到 jar 文件中。

测试类

在此步骤中,我们将介绍在 Ant 中结合使用 JUnit [3] 测试框架。由于 Ant 内置了 JUnit 4.13.1,因此你可以直接开始使用它。在 src\oata\HelloWorldTest.java 中编写一个测试类

package oata;

import org.junit.Test;

import static org.junit.Assert.fail;

public class HelloWorldTest {

    @Test
    public void testNothing() {
    }

    @Test
    public void testWillAlwaysFail() {
        fail("An error message");
    }

}

由于我们没有真正的业务逻辑要测试,因此这个测试类非常小:只是展示了如何开始。有关更多信息,请参阅 JUnit 文档 [3] 和 junit 任务的手册。现在,我们将 junit 指令添加到我们的构建文件中

    ...

    <path id="application" location="${jar.dir}/${ant.project.name}.jar"/>

    <target name="run" depends="jar">
        <java fork="true" classname="${main-class}">
            <classpath>
                <path refid="classpath"/>
                <path refid="application"/>
            </classpath>
        </java>
    </target>

    <target name="junit" depends="jar">
        <junit printsummary="yes">
            <classpath>
                <path refid="classpath"/>
                <path refid="application"/>
            </classpath>

            <batchtest fork="yes">
                <fileset dir="${src.dir}" includes="**/*Test.java"/>
            </batchtest>
        </junit>
    </target>

    ...

我们通过给它一个 id 并使其全局可用,来重用我们在 "run" 目标中定义的我们自己的 jar 文件的路径。printsummary=yes 使我们能够看到比仅仅 "FAILED" 或 "PASSED" 消息更详细的信息。有多少测试失败了?一些错误?printsummary 会让我们知道。类路径被设置为找到我们的类。为了运行测试,这里使用了 batchtest,因此你可以在将来轻松地添加更多测试类,只需将它们命名为 *Test.java。这是一个常见的命名方案。

在执行 ant junit 后,你将得到

...
junit:
    [junit] Running oata.HelloWorldTest
    [junit] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0,01 sec
    [junit] Test oata.HelloWorldTest FAILED

BUILD SUCCESSFUL
...

我们还可以生成报告。一些你(以及其他人)在关闭 shell 后可以阅读的内容... 有两个步骤:1. 让 <junit> 记录信息,2. 将这些日志文件转换为可读(可浏览)的内容。

    ...
    <property name="report.dir"  value="${build.dir}/junitreport"/>
    ...
    <target name="junit" depends="jar">
        <mkdir dir="${report.dir}"/>
        <junit printsummary="yes">
            <classpath>
                <path refid="classpath"/>
                <path refid="application"/>
            </classpath>

            <formatter type="xml"/>

            <batchtest fork="yes" todir="${report.dir}">
                <fileset dir="${src.dir}" includes="**/*Test.java"/>
            </batchtest>
        </junit>
    </target>

    <target name="junitreport">
        <junitreport todir="${report.dir}">
            <fileset dir="${report.dir}" includes="TEST-*.xml"/>
            <report todir="${report.dir}"/>
        </junitreport>
    </target>

由于我们会生成很多文件,并且这些文件默认情况下会写入当前目录,因此我们定义了一个报告目录,在运行 "junit" 之前创建它,并将日志记录重定向到该目录。日志格式是 XML,因此 "junitreport" 可以解析它。在第二个目标 "junitreport" 中,应该为报告目录中的所有生成的 XML 日志文件创建一个可浏览的 HTML 报告。现在你可以打开 ${report.dir}\index.html 并查看结果(看起来有点像 JavaDoc)。
我个人使用两个不同的目标来运行 <junit><junitreport>。生成 HTML 报告需要一些时间,并且你不需要在测试时使用 HTML 报告,例如,如果你正在修复错误,或者集成服务器正在执行任务。

资源

  1. https://archive.apache.org/dist/logging/log4j/1.2.17/log4j-1.2.17.zip
  2. https://logging.apache.org/log4j/1.2/manual.html
  3. https://junit.java.net.cn/junit4