环境:Ubuntu 12.04, java 1.7.0, ant 1.8.2。
前言
Apache Ant 是一个软件自动化构建工具,构建过程包括编译、测试和部署等。它和 Make 工具相似,但由 Java 实现,所以要求 Java 运行环境,非常适合构建 Java 程序。
历史
搭建环境
1. 安装 JDK。
我们将安装 Oracle JDK 而不是 Open JDK(也可以用安装 Open JDK),因此首先添加第三方库:
$ sudo add-apt-repository ppa:webupd8team/java $ sudo apt-get update
然后安装 oracle-java7-set-default:
$ sudo apt-get install oracle-java7-set-default
2. 安装 ant(Another Neat Tool):
$ sudo apt-get install ant-optional
准备项目
我们将源代码文件和生成的文件分开保管,我们将源代码文件保存在 src 目录,所有生成的文件保存在 build 目录,其子目录 classes 用以保存编译后的 Java 类文件,子目录 jar 用以保存 JAR 文件。
首先创建 src 目录:
$ mkdir src
接下来让我们创建一个 Java 类,该类会使用标准输出打印一句话 Hello World。让我们创建保存该类的源代码文件 src/oata/HelloWorld.java:
package oata; public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); } }
编译 HelloWorld.java 源代码并运行:
$ mkdir build/classes $ javac -sourcepath src -d build/classes src/oata/HelloWorld.java $ java -cp build/classes oata.HelloWorld
Hello World
接着我们创建一个 jar 文件,要创建一个 jar 文件并不难。但要创建一个可启动的 jar 文件则需要有以下几步:创建一个 manifest 文件,其中包括启动类,创建目标目录并将文件归档。
$ echo Main-Class: oata.HelloWorld>myManifest $ mkdir build/jar $ jar cfm build/jar/HelloWorld.jar myManifest -C build/classes . $ java -jar build/jar/HelloWorld.jar Hello World
注意:在 echo Main-Class 语句中 > 字符的两边不要有空格。
运行 Java 程序的四个步骤
我们现在来考虑一下我们的构建过程。
1. 编译 - 编译源代码,否则无法启动程序。
2. 执行 - 运行程序或者编译命令,下文 build.xml 中每个 target 对应着一个执行。
3. 打包 - 虽然目前我们的程序只有一个类,但如果我们要对外发布我们的程序,没人愿意下载几百个文件。因此我们要创建 jar 文件,最好是可执行 jar 文件。
4. 清理 - 清理自动生成的东西。事实证明许多错误都是没有做好清理工作导致。
Ant 默认的构建文件是 build.xml,构建过程中每一个步骤就是一个 target。现在为我们的项目新建一个构建文件 ./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 进行编译、打包和运行程序:
# 编译
$ ant compile
# 打包
$ ant jar
# 运行
$ ant run
也可以简化为:
$ ant compile jar run
对比一下使用 JDK 自身工具和使用 Ant 的构建过程:
java-only | Ant |
---|---|
$ mkdir build/classes $ javac -sourcepath src -d build/classes src/oata/HelloWorld.java $ echo Main-Class: oata.HelloWorld>myManifest $ mkdir 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"/> |
增强版构建文件
许多时候,我们在构建过程中会反复引用相同的目录,main-class 和 jar 文件,这些目前都是硬编码在构建文件中,另外我们还得记住构建步骤不能搞错。
为解决反复引用相同的东西和避免硬编码,我们可以使用 properties,而主类我们可以使用 <project> 标签的属性来指定,另外使用依赖包来保持构建过程稳步有序。让我们重新编辑我们的 build.xml:
<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:
$ ant Buildfile: /home/xavier/Exploration/000_build_java_application_with_ant/AntHelloWorld2/build.xml clean: [delete] Deleting directory /home/xavier/Exploration/000_build_java_application_with_ant/AntHelloWorld2/build compile: [mkdir] Created dir: /home/xavier/Exploration/000_build_java_application_with_ant/AntHelloWorld2/build/classes [javac] /home/xavier/Exploration/000_build_java_application_with_ant/AntHelloWorld2/build.xml:19: warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds [javac] Compiling 1 source file to /home/xavier/Exploration/000_build_java_application_with_ant/AntHelloWorld2/build/classes jar: [mkdir] Created dir: /home/xavier/Exploration/000_build_java_application_with_ant/AntHelloWorld2/build/jar [jar] Building jar: /home/xavier/Exploration/000_build_java_application_with_ant/AntHelloWorld2/build/jar/HelloWorld.jar run: [java] Hello World main: BUILD SUCCESSFUL Total time: 5 seconds
使用第三方库
总是有人会告诉你不要使用 syso-statements,即不要使用 System.out.println() 来记录日志,而应该使用日志 API。下面我们就在我们的项目引入一个第三方库记录日志 Log4J。
我们将第三方库文件放在 lib 目录下。你可以点击这里下载 Log4J 库。创建 lib 目录,并将 log4j-1.2.13.jar 放到 lib 目录下。
$ wget https://archive.apache.org/dist/logging/log4j/1.2.13/logging-log4j-1.2.13.tar.gz $ cd lib $ tar zvxf logging-log4j-1.2.13.tar.gz $ mv logging-log4j-1.2.13/dist/lib/log4j-1.2.13.jar .
接下来要修改我们的源代码和构建文件,使得在编译和运行我们的程序时可以访问到这个第三方库。
$ vi src/oata/HelloWorld.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 } }
现在还不能执行 ant,因为 Log4J 还不在我们的类搜索路径中。我们要做的不是修改 CLASSPATH 环境变量,因为这样做可能会影响到其他项目,我们只是在这个项目中引入 Log4J,我们要告诉 ant 所有第三方库(jar 文件)都放在 ./lib 目录下:
$ vi build.xml
<project name="HelloWorld" basedir="." default="main"> <property name="src.dir" value="src"></property> <property name="build.dir" value="build"></property> <property name="classes.dir" value="${build.dir}/classes"></property> <property name="jar.dir" value="${build.dir}/jar"></property> <property name="main-class" value="oata.HelloWorld"></property>
<!-- 新增 --> <property name="lib.dir" value="lib"></property> <path id="classpath"> <fileset dir="${lib.dir}" includes="**/*.jar"></fileset> </path> <target name="clean"> <delete dir="${build.dir}"></delete> </target>
<!-- 修改后 --> <target name="compile"> <mkdir dir="${classes.dir}"></mkdir> <javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath"></javac> </target> <target name="jar" depends="compile"> <mkdir dir="${jar.dir}"></mkdir> <jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${classes.dir}"> <manifest> <attribute name="Main-Class" value="${main-class}"></attribute> </manifest> </jar> </target>
<!-- 修改后 --> <target name="run" depends="jar"> <java fork="true" classname="${main-class}"> <classpath> <path refid="classpath"></path> <path location="${jar.dir}/${ant.project.name}.jar"></path> </classpath> </java> </target> <target name="clean-build" depends="clean,jar"></target> <target name="main" depends="clean,run"></target> </project>
运行 ant:
$ ant run ... run: [java] 1 [main] INFO oata.HelloWorld - Hello World
以上内容表示名为 run 的任务日志为:[java] 1 [main] INFO oata.HelloWorld - Hello World
1. [java]:表示 ant 任务正在运行 java 命令。
2. 1:Log4J 库定义的字段,详情请查阅 Apache Log4J。
3. [main]:表示当前线程为主线程。
4. INFO:表示日志级别。
5. oata.HelloWorld:日志消息来自的类名。
6. -:分隔符。
7. Hello World:日志消息。
配置文件
虽然我们使用了 Log4J,但目前为止我们仍然是硬编码,因为我们只是简单调用了 BasicConfigurator.configure(),如果我们想输出不同格式的日志消息,那我们就应该使用一个 property 文件。
我们在源代码删除 BasicConfigurator.configure() 所在行,以及相关的 import 语句。此时运行 ant 会提示:
... [java] log4j:WARN No appenders could be found for logger (oata.HelloWorld). [java] log4j:WARN Please initialize the log4j system properly.
现在让我们为 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
以上配置文件表示创建一个输出通道(Appender)到控制台的标准输出 stdout,标准输出流将打印出日志消息(%m),消息末尾添加一个换行符(%n)。这个和之前的 System.out.println() 效果相同。
创建了配置文件,我们还要激活该配置文件,编辑构建文件 build.xml,修改 <target name="compile">:
<target name="compile"> <mkdir dir="${classes.dir}"></mkdir> <javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath"></javac> <copy todir="${classes.dir}"> <fileset dir="${src.dir}" excludes="**/*.java"></fileset> </copy> </target>
<copy> 节点表示复制所有的非 .java 后缀的资源文件到 build 目录,这样我们就可以启动 build 目录下的程序并且将这些资源文件包含到 jar 文件中。运行 ant:
$ ant
Buildfile: /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build.xml
clean:
[delete] Deleting directory /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build
compile:
[mkdir] Created dir: /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build/classes
[javac] /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build.xml:20: warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds
[javac] Compiling 1 source file to /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build/classes
[copy] Copying 2 files to /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build/classes
jar:
[mkdir] Created dir: /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build/jar
[jar] Building jar: /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build/jar/HelloWorld.jar
run:
[java] Hello World
log4j.properties 被复制到了 build/classes 目录,run 的输出日志为 [java] Hello World。
测试 Java 类
Ant 内置了 JUnit,你可以直接使用 JUnit 测试框架来测试你的代码。新建一个测试类 src/HelloWorldTest.java:
public class HelloWorldTest extends junit.framework.TestCase { public void testNothing() { } public void testWillAlwaysFail() { fail("An error message"); } }
注:本文直接使用 ant 内置 JUnit 导致了 package junit.framework does not exist 的报错。尝试将 /usr/share/java/ant-junit4-1.8.2.jar 复制到 ./lib 目录依然无法解决报错。最后只能下载一个 junit.jar 到 ./lib 目录中才解决该问题。
$ wget http://search.maven.org/remotecontent?filepath=junit/junit/4.11/junit-4.11.jar -O ./lib
因为我们的项目还没有真正的业务逻辑,所以这个测试类非常简单,只是展示如何使用它而已。要了解更多关于 JUnit 测试框架,请查阅 junit 手册。
让我们把 juni 指令添加到我们的构建文件 build.xml 中:
... <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> ...
我们给我们这项目生成的 jar 文件路径一个 ID,并让它成为一个全局变量,这样我们在 target run 中就可以使用它的 ID 来引用它。printsummary=yes 可以输出更多的信息给我们,而不是简单的 FAILED 或 PASSED,例如失败了多少项,什么失败了,printsummary 都可以提供。classpath 是用来寻找我们的类。batchtest 是为了更方便测试在将来你添加了新的测试用例,约定俗成的测试类命名为 *Test.java。
$ ant junit Buildfile: /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build.xml compile: [javac] /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build.xml:20: warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds [copy] Copying 1 file to /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build/classes jar: [jar] Building jar: /home/xavier/exploration/000_build_java_application_with_ant/AntHelloWorld2/build/jar/HelloWorld.jar junit: [junit] Running HelloWorldTest [junit] Tests run: 2, Failures: 1, Errors: 0, Time elapsed: 0.02 sec [junit] Test HelloWorldTest FAILED BUILD SUCCESSFUL Total time: 4 seconds
为方便阅读测试结果,我们可以生成一个测试报告。首先,让 <junit> 负责记录测试数据;其次,将数据转换为可阅读文本。编辑构建文件 build.xml:
... <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>
因为我们可能会产生大量的文件,而这些文件默认都保存在当前目录下,因此我们定义了一个 report 目录,ant 会在运行 junit 之前创建该目录并将日志输出到该目录。因为日志格式为 XML,这可以让 junitreport 对其进行解析。另外一个 target junitreport 会将 report 目录下所有 XML 文件创建相应的可阅读的 HTML 格式文档。你可以打开 ${report.dir}/index.html 文件查看所有测试结果(看起来很像 JavaDoc)。
你可以将测试与制作测试报告分为两个任务,因为生成 HTML 报告需要一定的时间,而你完全没有必要在测试的时候停下来等待报告生成。
附:
1. Tutorial Hello World with Ant
2. Ant 入门教程