前言:
Ant是java世界里第一个具有里程碑意义的项目构建工具。
官网地址:http://ant.apache.org/。
文档地址(Ant task):http://ant.apache.org/manual/index.html。
扩展工具地址(比如checkstyle和clover):http://ant.apache.org/external.html。
本文并不打算长篇累牍的描述Ant的使用方法,因为官网已经够清楚,也有多位大神发过博文,这里只描述核心概念和一些基本实例,本文还会继续编辑,希望大家多提宝贵意见。
1,Ant的安装
从官网下载(http://ant.apache.org/bindownload.cgi)包,解压到任意目录,将bin目录加入到环境变量,一般推荐使用英文路径,并且先建立ANT_HOME的方式,以相对路径设置环境变量。(这是个好习惯)
2,Ant的核心概念
构建文件是以XML文件来描述,这就意味着,你可以通过xsd查看所有的熟悉和配置方式,这非常有用,当然eclipse支持Ant提示。
每个构建文件包含一个工程(project),其实是对一个项目概念的抽象,在Eclipse插件开发当中,也有这样的抽象。
每个工程包含若干个目标(target),目标是构成Ant构建文件的基本单元,在以后的Maven(另一种强大的项目管理工具)中类似于任务的概念。
目标可以依赖于其他目标(depends),可以称为项目内的依赖,Ant的目标可以构成链式,树形,网状的配置结构,目标的合理依赖可以使Ant文件有良好的扩展性和复用性。
易于扩展,很多优秀项目已经实现与Ant的整合,比如checkstyle,junit,clover。
目标的任务可以调用另一个工程的目标。(在一个项目中,可以引用另一个项目的配置,可以称为项目级别的依赖)
3,环境
环境准备:Eclipse + Ant 1.7 + jdk1.5+
项目准备:
使用Eclipse建立一个maven目录结构的项目或者普通的Java工程。
普通java项目:在classpath上显示的源目录为src
Maven目录结构项目:普通java项目建立后,在classpath上删除src源目录,并在src目录下新建类似于Maven工程的目录结构。
4,简单Ant文件
说明:我不会对每一个标签和属性做解释。最佳实践都属个人参考或总结,如有疏漏或错误,请高手指正并留下您的建议或指导。
如下是一份简单的通用构建的build.xml,并附上build.properties。
1),资源文件的配置
最佳实践:首先,尽量较少的定义非关联的属性,而采用相对引用的方式。其次,尽量采用直观而明确的名称定义属性,保持层次感和语义的完整性。
顺便提到一句,对于稍大项目的构建,如果是构建依赖的基础包目录,尽量应该采取按功能模块或者相互依赖的关系分成独立的目录,在不破坏整个项目结构下,子项目能够采用引入最少依赖的方式完成构建。
build.properties:
#依赖资源包根目录
dependency.lib.path=../../web-libs
dependency.resource.path=${dependency.lib.path}/resources
#编译源依赖包
dependency.lib.main.path=${dependency.lib.path}/main
#编译测试依赖包
dependency.lib.test.path=${dependency.lib.path}/test
#工具包目录
dependency.lib.tool.path=${dependency.lib.path}/tool
#源目录 目前定义为 maven的默认目录格式,方便之后介绍maven
src.main.java=src/main/java
src.main.resources=src/main/resources
src.test.java=src/test/java
src.test.resources=src/test/resources
2),构建文件的配置
最佳实践:target任务可以相互调用,我们采用depends属性来控制这个依赖,如果依赖的比较复杂,那么我们的Ant文件可能会偏向于倒立的树形结构,并且分支可能有所交叉,这从结构上并不是一个很好的脚本设计。
由于Ant脚本是顺序执行的,我们也最好依照这个特点,尽量避免target的交叉依赖,也就是完成一个target,和完成其它的target不要有太多的交集。还有一个非常重要的概念,很多target原则上都是需要清理的,我们应当树立一种清理的意识,比如我们在新建一个目录的时候,最好先清理这个目录,因为你不清楚之前的一次构建是什么时候或者构建是否过期。
build.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project name="common-web-demo" default="build" basedir=".">
<description>common project automatic build script</description>
<!--加载资源文件 -->
<property file="build.properties" />
<!-- 源目录classpath-->
<path id="main.classpath">
<fileset dir="${dependency.lib.main.path}" includes="**/*.jar" />
</path>
<!--测试目录classpath -->
<path id="test.classpath">
<fileset dir="${dependency.lib.main.path}" includes="**/*.jar" />
<fileset dir="${dependency.lib.test.path}" includes="**/*.jar" />
<fileset dir="${dependency.lib.tool.path}" includes="**/*.jar" />
</path>
<!--工具包classpath -->
<path id="tool.classpath">
<fileset dir="${dependency.lib.tool.path}" includes="**/*.jar" />
</path>
<!--工具包任务定义 -->
<taskdef resource="net/sf/antcontrib/antlib.xml">
<classpath refid="tool.classpath" />
</taskdef>
<target name="clean">
<delete dir="${build.target.dir}" />
<mkdir dir="${build.target.dir}" />
</target>
<!-- 主目录编译清理 -->
<target name="main.clean">
<delete dir="${build.target.bin.dir}" />
<mkdir dir="${build.target.bin.dir}" />
</target>
<!-- 主目录编译 -->
<target name="main.compile" depends="main.clean">
<javac srcdir="${src.main.java}" destdir="${build.target.bin.dir}" source="1.5" target="1.5" debug="on">
<classpath refid="main.classpath" />
</javac>
<copydir src="${src.main.resources}" dest="${build.target.bin.dir}" />
</target>
<!--主目录打包 -->
<target name="package.main.jar" depends="main.compile">
<jar destfile="${build.target.dir}/${build.target.assembly.name}.jar">
<fileset dir="${build.target.bin.dir}" />
</jar>
</target>
<!-- === 循环构建 === -->
<target name="build" depends="clean,package.main.jar" />
</project>
xml编写和标签解释:
1,关于xml的编写:在独立标签的编写上,应该选择倾向于<el .../> 而不是<el></el>,很多开源的Java框架都推荐前者的写法,比如Spring。
2,project:default属性指定默认执行target(命令行), basedir定义构建目标路径 basedir="."指定为当前项目路径(SystemUtils.getUserDir()),其实对于构建一个独立的项目或者一个模块下的多个项目,这样设定相对路径是一个很好的做法。
3,target(目标):一个build.xml最为核心的标签,它可以调用其它的target,也可以调用一些扩展的target,target的名称不可重复,在名称编写上也最好遵照某些规则建立分组,这样在一个Ant文件的Outline视图中可以很清晰的分辨结构和依赖关系。目前官网上的文档的task of list命令基本上都属于target的范畴。
4,特别说明关于上述build.xml中工具包任务的定义,也就是antcontrib,它定义了一些非常有用的扩展功能,比如for for each,更有意思的是try catch,try catch真没用过,有兴趣的可以试试。
task浅析:
1,property :资源标签,它能定义单个属性,也可以加载多个属性。定义单个:<property name="life" value="time" />;加载多个:<property file="build.properties"/>
2,taskdef :定义扩展,定义扩展有两种方式,一种是引入扩展标签,也就是上面的xml当中的格式,另一种可以自定义为<typedef name="urlset" classname="com.mydomain.URLSet"/> 和具体的处理类挂钩,后一种我不太了解。
3,关于delete 与 mkdir一类有关文件的操作,包括fileset这类提供作用域的标签。这些在Ant官网中介绍的非常详细,命令众多,就不一一列举了(关于删除命令,个人推荐delete,因为它的容错性要高一些,在目录不存在时不会有任何动作)。
4,javac,Ant整个构建体系当中最为核心的task之一,比较值得关注的属性有debug="on",source="1.5" ,前者打开debug,后者定义编译级别。
5,jar,打包命令,既然有jar,自然就有zip,war,ear了,它们大同小异。
6,fock 一个重要的属性,在javac junit很多相对比较耗资源的任务当中都有这个属性,只要开启,一般的意思就是采用独立jvm执行编译或运行测试等等之类,对于出现内存不足的错误很有效果。当然小项目不需要这么做,大一点的项目也可以通过调整Ant和虚拟机的内存来实现。
5 相对复杂的Ant文件
实现的功能:编译,打包,测试运行,代码检查,覆盖率检查。
1) 主要插件:
junit
checkstyle
clover
2)build.properties
#依赖资源包根目录
dependency.lib.path=../../web-libs
dependency.resource.path=${dependency.lib.path}/resources
#编译源依赖包
dependency.lib.main.path=${dependency.lib.path}/main
#编译测试依赖包
dependency.lib.test.path=${dependency.lib.path}/test
#工具包目录
dependency.lib.tool.path=${dependency.lib.path}/tool
test.jar.names=junit-4.10,spring-test-3.0.5.RELEASE
#源目录
src.main.java=src/main/java
src.main.resources=src/main/resources
src.test.java=src/test/java
src.test.resources=src/test/resources
src.webapp.folder=src/main/webapp
#构建主目录
build.target.dir=target
#构建源编译目录
build.target.bin.dir=${build.target.dir}/bin
#构建测试编译目录
build.target.test.bin.dir=${build.target.dir}/test-bin
#Junit 测试记录
build.target.junit.record.dir=${build.target.dir}/test-record
clover.db.dir=${build.target.dir}/clover-db
#测试报告
build.target.test.report.dir=${build.target.dir}/output
junit.test.report.dir=${build.target.test.report.dir}/junit.report
checkstyle.report.dir=${build.target.test.report.dir}/checkstyle.report
clover.report.dir=${build.target.test.report.dir}/clover.report
#构建目标包名称
build.target.assembly.name=common-web-demo
3)build.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="common-web-demo" default="build" basedir=".">
<description>common project automatic build script</description>
<!--加载资源文件 -->
<property file="build.properties" />
<!-- 源目录classpath-->
<path id="main.classpath">
<fileset dir="${dependency.lib.main.path}" includes="**/*.jar" />
</path>
<!--测试目录classpath -->
<path id="test.classpath">
<fileset dir="${dependency.lib.main.path}" includes="**/*.jar" />
<fileset dir="${dependency.lib.test.path}" includes="**/*.jar" />
<fileset dir="${dependency.lib.tool.path}" includes="**/*.jar" />
</path>
<!--工具包classpath -->
<path id="tool.classpath">
<fileset dir="${dependency.lib.tool.path}" includes="**/*.jar" />
</path>
<!--工具包任务定义 -->
<taskdef resource="net/sf/antcontrib/antlib.xml">
<classpath refid="tool.classpath" />
</taskdef>
<target name="clean">
<delete dir="${build.target.dir}" />
<mkdir dir="${build.target.dir}" />
</target>
<!-- 主目录编译清理 -->
<target name="main.clean">
<delete dir="${build.target.bin.dir}" />
<mkdir dir="${build.target.bin.dir}" />
</target>
<!-- 主目录编译 -->
<target name="main.compile" depends="main.clean">
<javac srcdir="${src.main.java}" destdir="${build.target.bin.dir}" source="1.5" target="1.5" debug="on">
<classpath refid="main.classpath" />
</javac>
<copydir src="${src.main.resources}" dest="${build.target.bin.dir}" />
</target>
<!--主目录打包 -->
<target name="package.main.jar" depends="main.compile">
<jar destfile="${build.target.dir}/${build.target.assembly.name}.jar">
<fileset dir="${build.target.bin.dir}" />
</jar>
</target>
<!-- === 循环构建 === -->
<target name="build" depends="clean,package.main.jar">
</target>
<!-- 测试目录清理 -->
<target name="test.clean">
<delete dir="${build.target.test.bin.dir}" />
<mkdir dir="${build.target.test.bin.dir}" />
</target>
<!-- 测试编译 -->
<target name="test.compile" depends="test.clean">
<javac srcdir="${src.main.java}" destdir="${build.target.test.bin.dir}" source="1.5" target="1.5" debug="on">
<classpath refid="test.classpath" />
</javac>
<copydir src="${src.main.resources}" dest="${build.target.test.bin.dir}" />
<javac srcdir="${src.test.java}" destdir="${build.target.test.bin.dir}" source="1.5" target="1.5" debug="on">
<classpath refid="test.classpath" />
</javac>
<copydir src="${src.test.resources}" dest="${build.target.test.bin.dir}" />
</target>
<target name="test.record.clean">
<delete dir="${build.target.junit.record.dir}" />
<mkdir dir="${build.target.junit.record.dir}" />
</target>
<path id="test.run.classpath">
<path refid="test.classpath" />
<pathelement path="${build.target.test.bin.dir}" />
</path>
<!-- 运行Junit测试 -->
<target name="junit.test" depends="test.compile,test.record.clean">
<junit printsummary="true" fork="true" showoutput="true" dir="${basedir}">
<classpath>
<path refid="test.run.classpath">
</path>
</classpath>
<formatter type="xml" />
<batchtest fork="true" todir="${build.target.junit.record.dir}">
<fileset dir="${build.target.test.bin.dir}">
<include name="org/wit/ff/dao/*Test.class" />
</fileset>
</batchtest>
</junit>
</target>
<!--生成测试报告目录 -->
<target name="report.clean">
<delete dir="${build.target.test.report.dir}" />
<mkdir dir="${build.target.test.report.dir}" />
</target>
<!-- 生成Junit报告目录 -->
<target name="junit.report.clean">
<delete dir="${junit.test.report.dir}" />
<mkdir dir="${junit.test.report.dir}" />
</target>
<!-- Junit 测试报告 -->
<target name="junit.report" depends="junit.report.clean,junit.test">
<junitreport todir="${build.target.junit.record.dir}">
<fileset dir="${build.target.junit.record.dir}" includes="**/TEST-*.xml" />
<report todir="${junit.test.report.dir}" />
</junitreport>
</target>
<!-- checkstyle报告 -->
<target name="checkstyle.report.clean">
<delete dir="${checkstyle.report.dir}" />
<mkdir dir="${checkstyle.report.dir}" />
</target>
<target name="checkstyle.report" depends="checkstyle.report.clean">
<taskdef resource="checkstyletask.properties" classpath="${dependency.lib.tool.path}/checkstyle-5.3-all.jar" />
<checkstyle config="${dependency.resource.path}/test_ii_checks.xml" failureProperty="checkstyle.failure" failOnViolation="false">
<formatter type="xml" tofile="${checkstyle.report.dir}/checkstyle.xml" />
<fileset dir="${src.main.java}" includes="**/*.java" />
<fileset dir="${src.test.java}" includes="**/*.java" />
</checkstyle>
<style in="${checkstyle.report.dir}/checkstyle.xml" out="${checkstyle.report.dir}/checkstyle.html" style="${dependency.resource.path}/checkstyle.xsl" />
</target>
<!-- clover覆盖率报告 -->
<taskdef resource="cloverlib.xml" classpath="${dependency.lib.tool.path}/clover.jar" />
<taskdef resource="cloverjunitlib.xml" classpath="${dependency.lib.tool.path}/clover.jar" />
<target name="clover.db.init" >
<delete dir="${clover.db.dir}" />
<mkdir dir="${clover.db.dir}" />
</target>
<!-- clover数据库,其实就是类执行信息存储单元 -->
<target name="with.clover" depends="clover.db.init">
<clover-setup initstring="${clover.db.dir}/coverage.db">
<statementContext name="log" regexp="^log\..*" />
<statementContext name="iflog" regexp="^if \(log\.is.*" />
<methodcontext name="main" regexp="public static void main\(String\[\] args\).*" />
</clover-setup>
</target>
<target name="clover.report.clean" >
<delete dir="${clover.report.dir}" />
<mkdir dir="${clover.report.dir}" />
</target>
<!-- 生成Clover测试报告-->
<target name="clover.report" depends="clover.report.clean">
<!-- 非常简洁的循环语句 -->
<foreach target="clover.single.report" param="prop" list="html;xml" delimiter=";" />
</target>
<!-- 类型参数化的clover 报告 -->
<target name="clover.single.report">
<clover-report initstring="${clover.db.dir}/coverage.db">
<current outfile="${clover.report.dir}/clover.${prop}" title="${build.target.assembly.name}" summary="true">
<format type="${prop}" filter="log,iflog,main" />
<fileset dir="${src.main.java}">
<exclude name="org/wit/ff/dao/Base*.java" />
<exclude name="org/wit/ff/dao/impl/Base*.java" />
<exclude name="org/wit/ff/model/*Entity.java" />
<exclude name="org/wit/ff/dao/*DAO.java" />
</fileset>
<fileset dir="${src.test.java}">
<exclude name="org/wit/ff/test/Base*.java" />
</fileset>
</current>
</clover-report>
</target>
<!-- html 格式clover 报告 -->
<target name="clover.html">
<clover-html-report outdir="clover_html" title="${build.target.assembly.name}" />
</target>
<!-- xml 格式clover 报告 -->
<target name="clover.xml">
<clover-report>
<current outfile="coverage.xml">
<format type="xml" />
</current>
</clover-report>
</target>
<!-- 运行所有目标-->
<target name="test" depends="with.clover,build,junit.report,checkstyle.report,clover.report" />
</project>
结束语:
关于Ant依赖的思考
通常情况下,当我们使用Ant构建项目的时候,我们需要依赖某个目录下的包来完成构建,对于很多很多个使用Ant脚本构建的项目而言,可能整个依赖包的目录会非常的庞大,虽然我们可以依据模块将依赖分为多个子目录,但是在实际的运行构建中,通常还是依赖了多余的jar,甚至有时候依赖了很多不需要的jar。实际上要解决这个问题,我们可以使用原生的Ant精确定义classpath,但是如果依赖过多,ant文件太过庞大,远不如**/*.jar好管理,很多情况下,模块的划分也是为了更简化classpath的定义。但是这些其实远远不够,因为项目之间的依赖,如果也是用Ant来解决的话看起来会有些复杂了,也难以控制,因为你不得不修改脚本或者修改properties文件。并且会产生很多人为因素的问题,比如依赖包需要同步时还得拷贝。
关于Ant的精确依赖,其实有一种方法。eg:首先将依赖配置到一个properties文件,然后通过Ant引入;其次使用antcotrib的定义任务for将所有的依赖拷贝到本地或者本项目的目录之下(hudson在构建Ant项目的时候就是采取这种策略)。最后按照普通Ant脚本编写就可以了。
虽说如此,但Ant的依赖控制难以精确的确是事实,并且它作为一个自定义程序化构建的工具,还是需要不少的脚本开发工作。虽然Ivy(Spring源码也采用这种构建方式)的出现也能对依赖进行很好的管理,但是一个更有力的工具,Maven显然更具竞争力。
一段旅程,远去所有昨天的昨天,如记事本翻开新的一页,永远的不漏痕迹的把记忆藏在一片洁白中。然后绚烂的开始新的故事。