翻译Ant权威指南的第5,6章.
第五章 用户自定义任务(Task)
通过用户定制化来扩展(extend)Ant的概念已经并将一直成为Ant最重要的和被其大力声称的特征.
Ant的建造者们给我们提供了一个足够健壮(robust enough)的系统来和当今可用的语言,工具协同工作,
并具备兼容未来出现的语言和工具的能力.举个例子,当Ant在2000年早些时候出现的时候,
并没有适合和C#配合的Ant任务(Task)存在,而现在却有了.大量的用户已经开发出大量的Ant任务(Task)
来配合各种第三方工具(third-party tools)的使用,从组件(groupware)产品如StarTeam(一项控制系统),
到应用服务器如Bea的Weblogic或是JBoss组织(JBoss Group)的JBoss.
这些改变和改进的发生对于Ant的核心处理引擎(core processing engine)并没有什么变化.
扩展Ant而不改变他的核心引擎是非常重要的,因为那意味着Ant核心引擎能通过扩展的开发(extension development)
被分离的改善和改进.这样,在两个领域可以并行开发,结果是改变的速度会比Ant作为一个独立封闭(monolithic)的系统快的多.
所有的任务都是Java类(Java class),并且任何一个程序员都可以通过写一个新的Java任务类来扩展Ant的功能.
这些就是用户自写任务(User-Written Task),并且利用了随Ant一起发布的核心任务的相同接口(These are user-written tasks,
and take advantage of the same interface to Ant used by the core tasks shipped with an Ant distribution).
用户自写任务和核心任务的唯一的区别只是他们的作者(author)和包位置(package location)(有时候这也是相同的!).
另外,他们在系统的同一个水平位置发挥作用(function on the same level playing field).
这一章,我们给你展示一下如何通过写你自己的任务来扩展Ant.
5.1 用户自制任务的要求.
Ant有两个任务,java和exec,他们能够执行任何Java类或系统中可执行命令行程序(command-line executable).
这种能力可能会使你怀疑定制任务还是否需要.技术上讲,你能使用这两个任务执行任何类会运行任何程序.
结果变得明显,某些自定义任务事实上并没有发明什么只不过是一个可执行的程序包装器(excution wrapper),
他运行Java类或程序就像java和exec任务执行的方式一模一样.差别是用户定制类可以和Ant引擎更紧密的工作.
定制类能够提供更加详细的(detailed)信息,处理错误也更加精确.
另外一方面,java和exec任务在处理未可预知的错误的能力,给用户产生详尽的通知的能力是有限的.
不管某个事件或错误的性质如何,对于他们处理都是一样,给你的控制权都非常有限.
一个定制任务,多数情况下,比起直接用Java和exec来执行都是一个扩展Ant来解决某个问题的更好的解决方法.
工作过程(build)中的错误(errors),事件(events)和消息(messages)会被任务初始化并被Ant引擎管理着.
Ant对事件作出反应并在一种用户可控制的行为下进行,把他们发送(propagate)到他自己监听器(listeners),
或用户自定义的监听器上(更多用户自定义监听器参见第六章).
对任务如此细致(fine-grained)的管理对终端用户(end users)来说再好不过了
(软件开发者需要更多信息关于他们的工程工作过程是如何进行的).
同样对于其他用户编写新任务来扩展现有任务也有益处,继承他们的工作能力又通过一系列相关操作产生一致的行为.
单单这些特征就可以使得定制任务成为一件好事.更何况,这些定制任务还有更多的用途.
任务善于抽象简单操作并使他们通过统一的接口加上额外的功能更加强大.
一些Ant任务甚至具备处理由于不同平台而产生的普遍使用的命令行功能间的差异的能力.
举个例子,不同平台上,不同Shell间的拷贝(copying)和删除(deleting)文件以及目录由于命令名和命令行参数的的改变
而变得非常痛苦.而通过使用任务来抽象文件操作,Ant消除了这个痛苦,并给用户提供了一个统一的接口.
在Ant中,只有一种方式来拷贝和删除一个文件,并且他不管Ant运行在哪个平台上都能一致的工作.
这并不是抽象(Abstracting)带来的唯一好处.没有了命令行工具的特征集(feature set)的限制,
一个抽象的任务可以给你提供一个提升的特征集.
一个Windows下的del命令不能实现删除所有的以.java结束的文件同时保留所有以Abstract开头的文件.
而Ant下的delete任务却能够做到,这表明了比起命令行来,他具备更大的灵活性.更好的是,他能在任何平台上完成此事.
任务设计关心的是工作需要,从不会限制他本身于工具的特征内,因为工具的设计注重于命令环境(shell)和系统需要.
使用可用定制任务类中的强大功能,你可以改善几乎任何一项工具的使用.不要把定制任务想象成Ant缺陷的有益补充(Band-Aid).
Ant和他的任务模型更像"乐高玩具".增加Ant任务可以增加并加强Ant的属性集,但并不会是他臃肿.
Ant自始至终都保持着模块化和可扩展性.
5.2 Ant任务模型(Task Model)
理解定制任务,首先应该理解任务模型.Ant,作为一个以Java为基础的程序,
使用了Java类的层次结构(hierarchy)和反射能力(reflection capabilities)来完成工作.
所有的Ant任务直接或间接的继承自抽象类org.apache.tools.ant.Task.
Ant引擎在这一个层面上管理所有的任务,因为他仅仅操作Task对象.
对于这个引擎,每一个任务都和其他任何任务一样,继承自这同一个类并且拥有相同的核心方法与属性.
XML解析和方法名设计的绑定使得Ant可以使用Task所有的子类.
另外,Ant以一种固定的方式处理所有任务--也就是说,Ant在同一个循环中执行所有任务.
当然编写简单的任务并不需要理解这个模型和处理细节,但是对于复杂任务可能会表现出不可预期的行为(exhibit undesirable behaviors).
-----------------------------------------------------------------------------------------
- 编写定制数据类型 -
- 除了任务之外,Ant模型还要处理数据类型(DataType).数据类型的一个例子是path任务. -
-path任务没有处理直接的行为. -
-相反,他建造了一个数据集,数据来源于(based on)XML中给予的规则和其他信息. -
-作为Ant1.4,用户可以技术性的拥有定制用户数据类型的能力. -
-但是,声明数据类型的方法(typedef任务)是一个bug,且不能工作. -
-Ant Release1.5版本中解决了这个问题. -
-----------------------------------------------------------------------------------------
5.2.1 任务的组成部分
一个任务包括两面.一面面向Ant的终端用户,这时任务仅仅是一个工作文件(buildfile)中的XML,别无他物.
为这一面你需要解析(dissect)这个XML并识别出该任务的各个部分.对于任务的编程人员,任务却变得不一样.
虽然XML还存在,但是却仅仅作为要编写的Java的代码的指南.可Java代码仅仅是冰山一角.
技术上来说,对于一个任务还有许多层面.
5.2.1.1 公用超类
对所有任务类,必须继承自超类(某种意义上说,就是Task).
Ant引擎严格对Task对象操作,忽视任何开发者附加在子类上的任何东西.但是,这并不意味着你可以忽略Task的类层次结构.
理解这个结构可以帮助你尽量摆脱他对你工作的阻力(ignoring it hampers your effort).
Task的子类不仅要表现工作文件中的任务,还要表现其他任务中的类中有用的功能.有时候,一个子类甚至不是一个任务.
例如,如果你的任务需要用到文件集合(file sets)和模式(patterns),你应该继承 org.apache.tools.ant.main.taskdef.MathchingTask.
这个类实现大部分文件集合和模式的操作,减除你自己来实现要付出的乏味的努力.
这使得你能够得到利用巨人般的功能强大的好处,就像利用这个任务和其他任务类.
(it does you good to stand on shoulders of powerful giants such as this and other task classes).
你应该知道一些和你需求相似的任务设计.Ant中一个很好高效率重用的例子就是zip家族的任务.
JARS扩展了zip的打包模型(zip-packaging model),
jar任务继承自zip,吸取(borrowing)了大部分zip的功能而仅仅实现Jar相关特征的操作.
更进一步,WAR文件是一个拥有标准目录结构和一个附加的必备文件(部署描述文件web.xml)的JAR文件.
因此,war任务继承自jar.在war的例子中,保留了继承来的每一点功能,只是实现了创建标准目录结构和检验War中描述文件的合格性的功能.
在这章的末尾,我们将分析jar任务和他的层次结构作为定制任务的一个例子.
5.2.1.2 属性(Attributes)
属性是名-值对形式描述的特殊XML标签.编程角度来说,Ant从XML中解析并加载属性的名值对,并把他们传递给各个任务对象.
Ant会把这些字符串重定义为原始数据类型对象,文件对象或者类对象.一个典型的应用,属性值表示布尔类型,扮演任务处理过程的标志.
例如,任务javac中的属性debug就是一个布尔类型,这个值为"on"时,javac编译类时会给出debug信息.为"off",则一般的编译.
5.2.1.3 内嵌元素(Nested Elements)
内嵌元素或多或少都和属性有着相互的替代的能力.他们可以是任务或是任务或是数据类型.作为属性,任务可以很明确的处理他们的内嵌元素.
不幸的是,处理内嵌元素并没有处理名-值对那么简单.
内嵌元素的复杂度可能变得非常令人迷惑,因为没有一种确定的模型使你能够设计你使用的内嵌元素.
理论上,你定制的任务可以把任何一个既有的任务作为他的内嵌元素.例如,你可以把javac看作一个内嵌元素.
但是,这样一个内嵌元素只有当你明确的处理javac相关类Javac的使用时才能正常工作.
你必须知道并处理javac实现时候的所有制约条件,弊端.不是那么简单(no small feat).
就算你这么作了,javac也可能执行的操作使得你不可能利用他作为一个内嵌元素.这是因为没有一种标准的方法来执行这些任务.
即使没有任何东西可以阻止你编程使用象javac一样的任务作为内嵌元素,你也将发现当整个工作中止时他并不不可用.
任务使用内省的(introspective)调用来处理内嵌元素,就像处理属性一样.
区别在于内嵌元素有相应的类包含数据和功能在他里面.而属性只有名-值对.
一个元素需要他的类被实例化,他自己的属性被解析被处理,并且他的主要的功能将被执行.在这个过程中,错误随时会发生.
通过对比和对照一个任务对他属性的使用和对内嵌元素的使用,可以证明属性和内嵌元素的差别.考虑copy任务:
<copy destdir="newdir/subdir">
<fileset dir="olddir">
<include name="**/*.java"/>
</fileset>
</copy>
copy任务带有属性destdir和内嵌元素<fileset>.copy任务处理destdir很简单.Ant给任务类传递一个文件对象指向该目录.
一次调用,这个属性就被设定.对比一下Ant是如何处理<fileset>元素的.有三种方式可以让Ant把改元素传递给任务类.
在每一中方法中,Ant必须保存fileset数据类型贯穿整个任务的生命过程.
(因此,在这个层次上,数据类型和任务对于Ant引擎来说都是唯一的).Ant对这些任务和数据类型的处理是一个递归的过程.
我们试图阐明的关键点是:Ant对数据类型的处理大都包含在对元素属性的处理过程中.
属性虽然比数据类型更易使用和理解,但是可读性和灵活性却不够.例如,Paths变成了难看有难维护的属性.
Path的值可能很长,并且如果path结构变化的话要每次都随着改变.内嵌的path元素就更可读且更易维护了.
他们由于能使用复杂的文件模式串来表现路径而显然更强大.(例如,*.*可以在path数据类型中工作,在path作为属性时却不能).
象生活中的每一件事,决定在实现属性还是内嵌元素之间有一个衡量的问题.
虽然我们在使用数据类型的时候得到了易维护性和易读性,比起使用属性却丢失了初始化开发阶段.有许多方法使用内嵌元素(准确的说是三种调用方式),
并且每一种方式都可能导致错误或是难以调试的异常行为(odd behavior).因为这个缘故,一些任务的作者提供同时支持两者的方法,
例如有classpath属性又有classpath内嵌类型.
记住这点可能使用户的迷糊,因此清分别给予文档化.你需要明确定义如果用户同时定义相同值的属性和内嵌数据类型时该如何处理.
Ant并不知道如何区别他们的不同并在未定义的顺序下操作两者.
5.2.2 Ant和任务间的通信
既然你已经理解了一个任务所包含的各个部分,我们现在转而注意Ant工作引擎和任务间的通信机制.
编写定制任务时,你需要理解三种通信机制.
Project类
project是在每个任务中都可以访问的公共实例.这个类表现了整个工作文件和在那包含的一切事物.
通过他,你可以访问所有的任务(tasks),目标(targets),属性(properties)和工作文件的其他部分.
工作异常(Build Exceptions)
工作异常,通过BuildException类来实现,为任务触发错误条件给Ant工作引擎提供一种途径.
日志系统(The logging system)
日志系统,通过project类可以访问,提供给任务一种方式来展示过程的信息给用户查看.
以下三节分别细致的描述每一种机制.
5.2.2.1 Project类
一个提供任务和Ant引擎之间通信的最大的便利的类:Project类.由于父类Task中包含一个Project类的实例,所以使得这种通信成为可能.
可以在任何任务中象使用任何变量一样使用他.Project类提供了许多强大的功能,请关注他都能作些什么,但也请注意你意外的滥用这种力量的地方.
(你不会有意的滥用,是吗?).同样,请注意你能用Project所作的聪明的事情在下一个发行的Ant中可能不被支持.
请保留一个可选的设计计划或者准备维护你自己的Ant版本.
Project类表示了整个工作文件.这个类允许你访问工作文件中的每一个任务,目标,属性,甚至一些定义工作文件如何执行的核心设置.
开发者很少使用这种访问便利,即便使用他的功能和能力都已经具备了.
首先,任务开发者使用Project来提供通过log方法的调用来访问引擎的核心审核系统(auditing system).
另外,Project为所有任务定义系统范围内的常量和全程方法.这些常量可以作为系统呼叫的参数,比如logging的参数.
全局方法提供各色功能从转换路径为本地化形式到提供布尔转换器来转换任务的布尔属性值.
在一个任务内,Project类的字段名相当明显,就是project.以下是任务中一些公共的方法调用和一些可用的常量:
project.getGlobalFilterSet()
返回一个关于本次工作的全局FilterSet对象.
可以定义一个全局的过滤集合,对每个任务排除或包含一系列文件使得可以在上面进行文件和目录操作.
如果你的任务需要遵守这个全局的过滤器,你可以通过调用project.getGlobalFilterSet()得到.更多关于FileterSet的信息请查看Ant API JavaDoc
project.getBaseDir()
返回<project>节点中的basedir属性值.如果你的任务需要从工程目录下进行文件操作,这是一种获得该目录路径的最好的方式.
project.translatePath()
把路径转换成当前操作系统的本地化路径格式.工作文件的作者可能就是一般的写文件路径和文件的名,而忽略诸如目录分割字符的不同点.
当你的任务需要在一个实际的文件上进行操作,你需要本地格式文件和目录串从而避免错误.
Project类中的translatePath()方法把原始的路径转换成符合当前操作系统的路径.
Project类能识别当前的操作平台,并且把文件名和目录转换成正确的形式.例如:
File f = new File(dir,project.translatePath(filePath));
这个例子展示了如何产生一个文件.这个任务建立一个文件,并不需要任何进行平台识别的代码来产生一个有效的路径(如:Windows或Unix).
相反,任务的编程者调用translatePath(),因为知道不管什么平台下,他都能在JVM下正常工作.
project.toBoolean()
检验一个布尔值.带有布尔属性的任务携带着诸如yes|no,true|false,on|off的值.通过toBoolean()函数可以解析.
这样消除了重写简单的从字符串值到布尔值转换的需要,也不用花费额外的精力为所有任务提供一致的接口.
所有带有标志类的属性的任务能使用三种布尔值的组合.例如,project.toBoolean("yes")和project.toBoolean("on")都返回true.
除了本节中展示的使用project类从工作引擎中获得信息之外,你还可以使用他把信息传递给工作引擎.但是这种应用通常是破坏性的,使用他比较危险.
Project类保存了许多工作引擎操作的设置值,这就意味着你可以在合适的地方做点改变.但仅仅是及其特别的例子,否则最好还是什么都别作.
我们提到这种能力仅仅是让我们的知识更加全面,而不是推荐你去实现时运用他.
与工作引擎通信的最安全的最好方式是使用工作异常和日志信息.
这是因为一个任务要做出的通信类型是那些通知信息型的,而不是任何可能造成破坏性的东西.
这就意味着如果错误发生了,应该提供状态消息作为运行时反馈或者安全稳妥的失败.
5.2.2.2 工作异常
工作异常通过BuildException类抛出,并为任务提供一种机制去发送错误情形给Ant工作引擎.
在一个任务中的任何地方你都可以抛出BuildException.
引擎对于他加在某个任务上的方法调用都预期有BuildException发生.
看这个例子,他展示了一个BuildException的抛出过程:
if(!manifestFile.exists()){
throw new BuildException("Manifest file: "+manifestFile+"does not exist.",getLocation());
}
如果在任务试图使用指定的特定的文件不存在的时候,任务进入错误状态并宣告失败.
他通知Ant引擎此次失败通过抛出一个包含错误消息和一个位置(Location)对象(使用getLocation()方法来提取)的BuildException.
Location类包含工作文件的名称和引擎当前正在解释的行号.
在某种程度上,他同样是一个类似于Project的类通过他任务可以从引擎那得到通信.
但是大部分开发者限制使用来自Location类的信息去构建BuildException中的消息.
抛出一个BuildException会马上中止这个任务.只有所有的任务成功,包含这些任务的目标才算成功.
有了BuildException,Ant就知道什么时候失败一个任务,他的目标,以及整项工程.
对于这样一个规则:只有所有任务成功一个目标才算成功.一个异常通常是任务中的failOnError的属性偶然使用而出现.
任务使用这个属性能避免抛出一个BuildException,因此工作能继续进行.当然,没有什么能象这样的自动化,
并且你作为任务作者,对实现这项特征负有责任.以下是从Cvs类中抽取的部分代码表现如何实现failOnError.
XML:
<cvs failOnError="true"
cvsroot=":pserver:anonymous@cvs.phpwiki.sourceforge.net:/usr/phpwiki"
dest="${src.dir}"/>
实现:(从Cvs.java源码中摘录(excerpt)出来)
/**
* The task's instance variable, representing the failOnError flag
* If true it will stop the build if cvs exits with error.
* Default is false.
*/
private boolean failOnError = false;
...
// Sets the instance variable through the attribute
public void setFailOnError(boolean failOnError) {
this.failOnError = failOnError;
}
// some method code, blah blah blah
// Throw a build exception from this method, only
// if the task is supposed to fail
public void execute( ) throws BuildException {
// more code...
// Handle an error, but only throw an exception when
// failOnError is true
if(failOnError && retCode != 0) {
throw new BuildException("cvs exited with error code "+ retCode);
}
// more code...
}
简单而言,如果failOnError属性值为false,Cvs类将不会抛出BuildException并为包含该任务的目标构建一个错误状态.
另外,这样有个益处是,不是什么都不作,错误的条件至少可以产生一些日志信息使得终端用户知道什么东西除了问题.
举个例子,更好的实现:
//some method code,blah blah blah
//Throw a build exception only if the task is supposed to fail
if(failOnError && retCode != 0){
throw new BuildException("cvs exited with error code "+retCode);
}
if(!failOnError && retCode != 0){
log("cvs existed with error code "+retCode);
}
5.2.2.3 日志系统
Project类允许任务获得系统范围的关于工作文件的信息.
他同样提供方法访问工作引擎的审核系统.这些方法是log()形式的各种变种.
所有的信息是否显示决定于叫做消息级别的引擎范围的设置.
消息在下面的五种级别上显示,按照详尽程度(verbosity)排序:
错误级别 (Error)
警告级别 (Warning)
通知级别 (Info)
详细级别 (Verbose)
调试级别 (Debug)
这些级别指导Ant以什么样的状态来展示消息.
例如,如果你告诉Ant仅仅显示通知级别的消息,那么发给Ant的所有错误级别,警告级别和通知级别的消息将会记录到日志中.
消息级别的值可以通过以下的Project类中公共的静态域取道:
Project.MSG_ERR
Project.MSG_WARN
Project.MSG_INFO
Project.MSG_VERBOSE
Project.MSG_DEBUG
VERBOSE和DEBUG级别看起来很相似,但他们事实上并不相似.
当你运行Ant时,你能通过单独的参数指定VERBOSE和DEBUG级别的消息.指定DEBUG级别的消息结果会导致不会显示VERBOSE消息,相反也一样.
log()函数把消息发送到工作中已注册的监听器.该监听器然后根据他的设计处理消息串.默认的日志监听器把所有的日志打印到控制台.
log()函数有三种形式可用:
log( message)
在任务中,消息通过Project类的log方法传递.默认情况,对log()的调用是通知级别(通过MSG_INFO变量指定).
下面的例子发送同样的通知消息到工作引擎在默认级别MSG_INFO上.
project.log("This build step has completed successfully with "+numfiles+" processed");
log("This build step has completed successfully with "+numfiles+" processed");
例子显示,有一个默认的log()方法(定义在Task类中)可用而任务不用使用他们的Project实例变量.
使用默认的log()是种好的想法,因为将来的Ant发行版本中任务层次对Project类的的访问会被取消.
log(message,level)
log()方法的另外一个形式有第二个消息级别的参数.在发送DEBUG和VERBOSE消息时很有用.例如:
//Use the project variable's log method to log messages
project.log("For loop to process files begins",Project.MSG_DEBUG);
//Use the log method from Task to log messages
log("For loop to process files begins",Project.MSG_DEBUG);
注意到,有两种方法调用log().除了Project类,Task类也有两个参数的log()方法的实现.
你应该尽量使用Task中的两个参数的方法log(message,level).
log(message,level,task)
Project类中的第三种形式的log()方法有第三个参数,一个Task对象.有应该不要在自写的任务中调用这个方法.
他是给工作引擎用的;我们在此提到仅仅是为了介绍的完整性.
5.3 任务的生命周期(The Task Life Cycle)
复杂任务,操作多个文件,依赖内嵌任务,使用多重库(例如,可选的ejbjar任务),要求深入理解任务和Ant的关系.
把这看作是一次警告.这一节将深入探讨一个任务的生命周期的完整细节.如果你觉得你的定制任务没有这样复杂,那么请跳过这一节直接看我们的例子.
你之后什么时候都可以回过头来看这节.理解引擎和任务生命周期对你成为一个专家级的任务编写者是相当重要的,那对于编写相对简单的任务这并不要求.
Ant处理所有的任务都是一样的.Ant在固定的阶段设定属性并处理内嵌元素.我们能预测一个任务是如何操作的并进而设计他.
任务的生命周期可被划分成两个主要阶段:解析时和运行时.
解析时阶段开始于Ant从XML文件中读取任务(把引擎想象成一个一个元素地解析XML文件).运行时阶段开始于解析时阶段成功完成之时.
5.3.1 解析时阶段(The Parse Phase)
Ant在他地XML解析期读入一个元素节点时解析一个任务.
任务名,属性,内嵌元素都被包装成单个XML元素对象并存储在Ant的存储空间DOM.
在解析时阶段,如果任务的XML描述不合规范或者任务构造时有操作抛出异常,这步操作就会失败.
下面是Ant在解析时阶段会产生的动作的完整清单:
1.实例化任务类
Ant,使用XML元素的名称,来实例化一个任务的相应类.记住,这个时候属性并不会被设置且不会和工作系统产生联系.
2.建立引用指向project对象和父目标对象.
任务使用引擎为他们准备好的可用的对象和引擎进行通信.在这个阶段,Ant建立对这些对象的引用,并使他们可用.
3.增加id引用
Ant在一个内部的表中存储带有id属性的任务列表.如果任务带有id属性,那么这个阶段仅仅对别的任务和数据类型重要.
尤其是对于那些执行一些并行处理(parallel processing)的任务和数据类型很重要.关于并行处理,参考第7章.
4.调用init()
任务对象中的init()方法这个时候被调用.记住,任务属性此时还是不可用的.另外,任务所需其内嵌元素的信息也是不可用的.
附注,许多已经发行的任务不实现这个方法.
5.内嵌元素被解析并被处理,使用addXXX(),addConfiguredXXX(),和createXXX()方法等.
在理解整个生命周期过程中这或许是最重要的一步(也是最难的一步).
凭直觉,你也许会认为Ant定义和处理任务属性是在解析时阶段,但事实并非如此.
Ant知道运行时阶段才会关心任务属性.这也意味着非法的属性定义直到运行时才会被发现.但是,Ant却在解析时处理内嵌元素.
因此他会捕捉到非法的内嵌元素在捕捉到非法属性之前.
那么,内嵌元素如何处理?他在你的任务中调用createXXX(),addConfiguredXXX(),或addXXX(),其中XXX被定义为内嵌元素名称.
那么这三个方法之间有什么区别?这主要取决于你计划怎么使用内嵌元素以及内嵌元素相关对象本身的性质特征.
如果你的任务需要自己来实例化这个对象,或者这个对象本身没有构造函数,那么请使用create;把它看作"你的任务创建的内嵌对象".
如果你的任务需要引用一个已经实例化的对象,那么使用add;把他看作"Ant把这个对象的引用加到你的任务对象中".
如果你需要Ant在传递这个对象的引用之前完整的处理这个元素节点,请使用addConfigured;把它看作"Ant把已配置的对象引用加到你的任务对象中".
如果还不清楚,请查看已有的任务实现吧.顺便说一句,Ant最先调用createXXX().如果你有对某特定元素的多个函数实现.Ant会统统调用的.
这样的结果将很可怕,所以请尽量不要这么作.
5.3.2 运行时阶段(The Runtime Phase)
运行时阶段实际是对一个任务的总结结束.他开始于解析阶段的成功完成.当你的任务进入运行时阶段时,别的目标和任务可能已经成功运行了.
可能你想确保你的任务运行前所期待预想的行为和应有状态设置都应该已经完成了,但这是一种奢望!
你的任务应该自动进行,并能够作为工作的第一个或最后一个任务进行.
以下是任务运行时发生的事情清单:
1.任务的所有属性被设置
把这些属性想象成任务的特征.Ant传递这些值给任务通过对每个属性调用方法setXXX(),其中XXX是属性的名称.
如果少了某个set方法,Ant的错误会抛出并且任务和工作宣告失败.
2.处理XML文件中的CDATA文本
XML给予你存放原始文本的能力,使用<![CDATA[]]>构造.你可以发送原始文本给你的任务.
Ant调用方法addText(String msg),传递一个String对象代表XML中的字符数据.
下面是一个CDATA标记的例子:
<taskname>
<![CDATA[Naturalized language to be displayed by an Ant task]]>
</taskname>
当Ant读到CDATA标记时,它就会在你的任务中调用addText("Naturalized language to be displayed by an Ant task").
如果你的任务类以及它的父类都没有实现addText方法,而你又包含了CDATA的标记,那么工作会失败.没有默认的方法来处理字符数据.
许多作者并不使用CDATA属性.原始字符数据典型的被应用于需要处理消息任务或需要处理没有控制字符的文本.
例如,script任务使用CDATA表示实际的脚本.因为如果不使用CDATA的话,象<,[等字符都是典型的编程语言操作符,这肯定会在XML中造成问题.
3.所有的内嵌元素的属性被设定
Ant解析所有元素的属性,当读到这些XML时.但知道运行时才作这些工作.这对所有的元素都是一样的,包括任务的内嵌元素.
你几乎不需要担心内嵌元素的属性状态,因为你只有直到任务执行(此阶段的下一步)时才会用到,到那时,所有属性都是可用的.
4.调用execute()
到此,所有的数据收集和数据校验都已经完成.使用execute(),你的任务执行他所设计的行为.从这个时候开始,你必须处理或抛出所有的错误条件.
Ant不会期望会有错误码返回,从而一旦发生不会在调用你任务中的其他方法.
再说一遍,为了写一个任务你并不需要完全理解任务的生命流程.理解它可以最大程度的帮助你发现为什么有些你想做的事情在你任务中没有发生.
很少的情况下,你必须精通整个生命流程才能让某些事情发生.尽量避免它,因为任务执行的过程细节以后可能悄无声息的发生变化.
除非你维护自己的Ant版本.
你会发现你自己坚持一个发行的版本可以运行,而另一个就不行.
生命周期很重要,因为它允许Ant和所有的任务一致的工作.引用别的任务的想法和代码变得简单和那么普通.
5.4 分析一个例子:jar任务
讲了那么多理论之后,让我们看看实际运用会发生什么(When rubber meets the road).
要开发你自己的Ant任务,只需要写一个Java类来实现你的设计.
任务复杂还是简单那要看你了,唯一重要的是你的Java类必须和Ant对象模型中阐述(set forth)的协定(convention)一致.
作为编写任务的一个例子,我们分析已有的任务:jar.jar任务已经覆盖了所有我们需要的主题.
jar任务类是深层次的一部分,同时展示(demonstrating)了通过继承实现的重用(re-use).
它继承自zip,而zip继承自MatchingTask.jar任务没有自己的execute()方法实现,而是依赖zip类中的实现.
这表示了关于你自己的实现中,某些要求是那么的松.
jar任务同样使用了大量的属性和内嵌元素,成为提供给我们的一个关于如何处理所有这些特征的好的例子.
使用已有的任务作为例子可以加强这个概念:用户编写任务和Ant发行中包含的任务之间没有什么区别.
分析jar可以给我们一些设计任务眼光.它有着唯一且容易理解的设计目标.我们有的这个对象重用的任务设计对于未来的扩展也是开放的.
War和Ear继承自Jar,得到了同样的益处.但我们并不能涉及到jar任务的各个特征和各个方面.需要更多的信息,就花点时间看发行的源码吧.
学习更多的任务实现,而不仅是jar任务,可以帮助你成为一个强大的Ant任务开发者.
------------------------------------------------------------------------------------------------
Where to look: The source for Jar, Zip, and MatchingTask is found in
the source distribution of Ant (http://jakarta.apache.org/builds/jakartaant/
release/v1.4.1/src). We analyze the jar task with code snippets from
these source files. If you fail to follow some of our decisions or don't
understand how a code snippet fits in with the descriptions, feel free to
follow along with the full source code at hand.
------------------------------------------------------------------------------------------------
这样说吧,我们的分析在创建一个工作任务方面并不全面(comprehensive).我们简单涉及并解释一下设计和编写jar任务的主要点(major points),
但是对于诸如处理Jars的输入流等实现细节并没有涉及.他只是一份分析而不是教程.
如果你根据此分析来尝试编写并编译代码,你会发现最后有些东西并不工作.
在简洁(concise)和完整(complete)的矛盾上,我们选择了简洁,不完整(sacrificing),当然也是一份成熟的用户自写任务指导.
不过,我们的分析精确的描述了写好jar和其他任务所必备的工作(effort),如果你认为我们提供的不够,没有比你去学习任务的源码是更好的推荐.
开始,我们假设Ant没有jar任务.没有它,我们第二章的例子工程就没有任务来创建这些类的Jar包了.
使用java或exec来运行命令行jar工具真是太讨厌了,也容易犯错(在本章的导论中有讨论).
5.4.1 设计jar任务
对于一个要创建Jars的任务有什么要求?比较好的开始是从命令行工具jar起步.
最小的情况下,我们的任务应该拥有此工具在创建JAR方面的全部特征(而不是此工具的所有特征).
这个区别是很重要的.我们不用重复实现jar工具,我们只是为我们的工作而创建一种可用操作,仅仅符合我们工作的需要罢了.
命令行工具仅仅能达到这样目标.我们的工作要求我们创建JARS,因此我们应该关心JAR的创建,而不是别的.
我们之后应该定义一种需求,例如,解包JARS文件,我们应该需要这些特征的实现.
命令行工具可以生成一个zip兼容的文件加上一个特殊的文件夹叫做META-INF.它存放作一个特殊的文件叫做MANIFEST.MF.
不用深究更多细节,我们把JARS称作智能zip文件:此文档不仅可以把一组文件打包成一个文件,并且有一个包描述类型.
最小情况,我们的任务应该创建JARS并带有(allow)用户自写的清单文件,如果有的话.
从整个工作的观点看,我们的设计应该允许我们使用多重目录和文件类型的多组文件来创建JARS.
既然一个JAR包含了类文件路径的目录结果,我们可能需要改变有多少组文件被存储到一个JAR文件中.
经验丰富的Ant用户用文件集合和文件模式来识别这件事情.(这章后,相信你也能做到.)
对现有的任务的匆忙研究也揭示出了一些相似的有关文件集合的设计,例如copy和zip.
简单而言,以下是我们的jar任务的要求:
拥有(duplicate)命令行工具的JAR创建能力
给定一个命名,一个清单文件名(manifest),一组文件或目录,命令行工具就可以生成JARS.我们的任务也要做到这一点.
可以对一系列文件,目录和文件模式(Pattern)进行操作
许多任务都有能力处理用户自定义文件集合信息,也能处理用户定义的文件模式.我们应该准备让我们的任务具备这项功能.
通过XML描述,添加/修改清单文件
这是一个可扩展其对应工具功能的任务的例子.不仅是维护一个单独的清单文件,我们允许清单设置成为工作文件内部的设定,当然也就是XML节点.
从我们的需求分析中,我们应该有了XML形式的任务语法的大概模样.
当你为你自己的任务定义语法时,因为你的继续前进而使得设计变化,那请不要对此感到惊讶.
我们的任务的XML设计:
<jar jarfile="somefile.jar" manifest="somemanifest.mf" basedir="somedir">
<fileset dir="somedir">
<include name="**/*.class"/>
</fileset>
<manifest>
<attribute name="SomeAttribute" value="SomeValue"/>
</manifest>
</jar>
5.4.2 权衡优先工作
假设除了使用Jar任务,我们已经没有了别的办法来完成这项工作,现在我们知道,我们需要编写一个定制任务.
还有一点我们必须研究的是,必须确保我们是第一个作这件事的人.大量的定制任务存在的同时,Ant的发行版本里面却已经拥有了一些.
从Ant1.4开始,Jakarta项目组就开始在Ant的Web站点上维护一个列表,该列表包含了许多常用的用户自写的任务,
因此所有人都可以知道什么任务已经存在了.(参看:http://jakarta.apache.org/ant/external.html).
除了这个Web站点,我们应该通过Ant邮件列表,Web或世界新闻网(USENET)来查找看我们需要的任务是不是已经有人开发了.
将来也许会出现一个任务库,有点CPAN类库系统的东西.
我们没有发现Jar任务.下一步,我们看看有没有什么已有的任务实现了和Jar任务差不多(resemble)的内容.
实际行为中,你也许缺乏足够的经验来发现你要实现的任务和已有的任务之间的关系.
仔细查看(review)一下第7章和第8章,看看你需要的任务功能或者部分功能是不是在一些已有的任务中被实现了
(exist in some other currently existing task).
象我们早先提到的那样,JARS仅仅是简单的ZIP文件加上一个清单文件(Manifest file)和一个不同的文件扩展名.
就是因为这样,我们注意一下Zip任务,以期发现可能的重用.
zip任务执行了一个简单的操作,来实现从一组文件(pattern)和规则(rule)中生成一个简单的包文件.
事实上,这个操作仅仅在清单上有点不同,以及文件名(.zip和.jar)不一样而已.
就此决定!我们从zip继承我们的对象.
这就是我们Jar类的手稿(signature).
package org.oreilly.ant.tasks;
//need to import it to derive from it
import org.apache.tools.ant.taskdefs.Zip;
/**
* Implementation class for the <jar> task in Ant.
*
* In your task, be sure to show examples of your task in use
* here. Also, if you plan on having others extend your implementation,
* describe how some of your methods apply and how your task works in
* general.
*/
public class Jar extends Zip{
//Implementation code
}
当我们从Zip继承了,我们继承的类自动成为Ant任务框架(task framework)中的一部分.
最原始的任务框架类,org.apache.tools.ant.Task,它定义了一个任务所必须的最根本(rudimentary)的方法.
这些方法,除了那些你在任务实现中提供的,也可以是(allow)一个任务通过工作文件(buildfile)中XML节点来获取的属性,和工程中定义的其他的属性.
org.apache.tools.ant.taskdefs.MatchingTask继承了org.apache.tools.ant.Task,并实现了任务需要的文件和目录操作的方法.
例如copy和zip任务,继承了MatchingTask,也就继承了这些方法.
第四章包含了一个关于模式和文件组织的完整解释.
这里的关键是需求重用能力(re-usability).有一个任务对象模型意味着拥有一般公用(common)功能的任务都可以继承自同一个父任务对象.
权衡优先工作不仅仅意味着寻求代码执行的重用,且要寻求对象的重用.对象模型如此强大,也解释了为什么Ant在不到两年的时间里扩展的如此迅速.
设计上的努力工作以及最初的研究把后来的实现成本降到最低(pays off in the end).框架的有益改变使得所有的任务得益:几乎不需要什么维护.
5.4.3 实现属性设定方法
Ant通过任务作者定义的一组设定方法(setter method)来给任务设定属性.
方法命名依照一个相似于JavaBean属性设定器(setters)的命名规则:set+大写字母开头的属性名称.
这些方法必须是public可见的,给调用者不返回任何东西(return nothing to the caller).
参数通常是一个字符串,但可以是任何下面列表列出的对象,任何原始对象(他们会从String对象转换而来),
或者是任何用户定义的类型,不过它必须有以一个字符串对象作为参数的构造函数.
有效的属性设定参数类型有:
String
这种参数最常见.Ant把从XML中解析出来的原始值(raw value)传给设定方法.
File对象
如果Ant知道(determine)设定方法有一个文件对象参数,
它将会试图以<project>元素中的basedir属性定义的目录为文件目录,来建立一个文件.
Class对象
如果属性是一个完整的有效的(qualified)类名.Ant将试着通过类加载器(classloader)来加载这个类.
在Ant1.4.1发行版本中,还没有这么作的任务例子.
用户自定义对象
如果你的新类有一个构造函数,它仅有一个String类型的参数,那么你可以应用这个类型在任何的设定方法实现中(signature).
作为一条规则,最好应该这个的类作为你的任务中的一个私有的成员.
类的实现和可见性应该和包含它的类保持一致.
这样,你阻止了那些由于在Jar文件中的类列表看到你的对象的人使用这个类来作为一个任务的错误事情发生.
记住,我们的jar任务中,我们并没有实现所有的属性设定方法,仅仅是Zip任务中没有处理的,
或者是那些我们在Jar中和Zip中有不同处理的方法(这实际就是对父类的此方法重载).
表格5-1列出了我们的jar任务的属性(详见早些时候给出的jar任务的XML例子).
表格 5-1 JAR相关的Jar任务中特有的属性
属性名 描述 是否需要在Jar任务对象中实现
jarfile 打包后的JAR文件的名称 是,它在Zip任务对象中是不可用的
manifest 用来验证和包含的的清单文件名 是,它在Zip任务对象中是不可用的
basedir Jar文件的来源的根目录 不,Zip对象已经为这个属性实现了设定方法
接下来是setJarfile()属性设定方法的实现.它以一个File对象为参数.Ant通过内省功能检测到并试图通过XML来的属性值创建一个文件对象.
创建文件的错误会从Ant自身抛出,你不用担心无效文件名等问题.
同样,既然我们的方法来自Zip,我们仅仅需要调用zip类的setZipFile()方法,而这个方法设定任务实例的File对象.
/**
* Set the value of the JAR filename
* The instance variable is zipFile
*/
public void setJarFile(File pValue) {
log("Using Zip object 'setZipFile' to identify the JAR filename",MSG_DEBUG);
super.setZipFile(pValue);
}
另外一个例子,我们将为你显示一下jar类中才有的属性设定方法:manifest.象setJarFile(),setManifest()方法有一个File对象的参数:
/**
* Set the manifest file to be packaged with the JAR
* The manifest instance variable can be used to add new
* manifest attribute entries with nested elements of the
* jar task.
*/
public void setManifest(File manifestFile) {
// 该属性是一个文件
// 检查文件是否存在
// 如果不存在,抛出一个BuildException,工作失败
if (!manifestFile.exists( )) {
throw new BuildException("Manifest file: " + manifestFile +" does not exist.", getLocation( ));
}
// 设定清单文件的实例变量
this.manifestFile = manifestFile;
InputStream is = null;
// load the manifest file. An object to handle manifest files
// was written by Conor MacNeil and is available with Ant. This
// object guarantees that the manifest file is properly formatted
// and has the right default values.
try {
is = new FileInputStream(manifestFile);
Manifest newManifest = new Manifest(is);
if (manifest == null) {
manifest = getDefaultManifest( );
}
manifest.merge(newManifest);
// Let's log this operation for task developers
log("Loaded " + manifestFile.toString( ), Project.MSG_DEBUG);
} catch (ManifestException e) {
// ManifestException is thrown from the Manifest object
// Just like the Manifest object, a custom object exists
// to warn about manifest file errors.
log("Manifest is invalid: " + e.getMessage( ), Project.MSG_ERR);
throw new BuildException("Invalid Manifest: " +manifestFile, e,getLocation( ));
} catch (IOException e) {
// IOException is thrown from any file/stream operation,
// like FileInputStream's constructor
throw new BuildException("Unable to read manifest file: " +manifestFile, e);
} finally {
// Since we're done reading the file into an object, let's close
// the stream.
if (is != null) {
try {
is.close( );
} catch (IOException e) {
// do nothing but log this exception
log("Failed to close manifest input stream", Project.MSG_DEBUG);
}
}
}
}
5.4.4 实现内嵌元素处理
处理内嵌元素的实现代码是完成一个任务的最复杂的部分.类似于属性,有处理内嵌元素通过遵守约定命名规则而命名方法来完成.
Ant读取没有内嵌元素相关的任务对象并试图调用三个方法中的一个.在这个例子中,方法命名规则是addXXX(),addConfiguredXXX()和createXXX(),
其中XXX是内嵌元素的大写字母开头的串(例如,addFileset()处理一个任务的<fileset>内嵌元素).要知道实现什么样的方法是困难而迷糊的.
Ant如何处理各个不同内嵌元素之间也有一些细微的差别.
下面的列表提供了一个关于对一个内嵌元素什么时候实现addXXX(),什么时候实现addConfiguredXXX()或createXXX()的松散定义.
典型的,你需要选择一个最适合你的的技术来实现相关的方法.即使理解如何定义来适合你的需求是困难的.
但是,之后我们对jar任务的分析会帮助你扫除这些困难.
addXXX()
当你"加了"一个内嵌元素,你实际在告诉Ant在调用你的addXXX()方法之前实例化这个类.
如果内嵌元素对应的类没有默认的构造函数,Ant将不能完成这件事,一个错误将会被抛出.
如果有,Ant就会把实例化好的对象传给你的任务对象,这样你就可以按照你的意愿处理这个对象(例如把它保存在一个Collection中等等).
我们推荐你能等到你的任务的执行阶段才真正使用内嵌对象(调用方法或取值),
要是能阻止由于内嵌元素属性未被设定而产生问题的可能性那是很好的.
addConfiguredXXX()
现在你在想,"我需要在执行阶段之前使用内嵌元素!".幸运的是,Ant提供一个替代方法来添加对象.
addConfiguredXXX()方法指示Ant不仅仅实例化这个这个类,且要在把这个对象传递给任务前对其进行配置.
换句话说,Ant保证了给定的内嵌元素内的属性和内嵌元素在它到达任务对象前被设定好.
这样技术性的打破了任务的生命流程,使用这个方法就没什么危险,尽管影响不大.尽管Ant为你配置这个节点,但Ant并不会马上完成配置.
你将会发现父任务属性在调用addConfiguredXXX()时是null空值.如果你试图使用这些属性,你将会产生错误,致使工作结束.
方法的参数类型是有限制的,就像addXXX方法一样,如果有问题的这个对象没有一个默认的构造函数,你就不能把它当作addConfiguredXXX方法的参数.
createXXX()
如果Ant调用createXXX()方法,它给予了任务对象解析内嵌元素的足够控制权.不是把对象传递给任务,Ant希望任务来返回这个内嵌元素对象.
这样会带来一些边际效益;最显著的,它消除了对内嵌元素具备默认构造函数的要求.不利的(downside)一面是你有理解元素对象初始化时行为的责任.
你可能没有文档或源码在手边,因此这可能是一件可怕的事情(formidable).
这些是一些松散的定义因为没有什么编程上的可以让你使用他们.只要你对相应的内嵌元素实现了其中一个函数,Ant就能为你的任务和其内嵌元素.
但是,当你查看Ant的源代码,尤其其他的用户编写的任务,你将会找到开发者违背了这些定义,事实上,是把他们混合起来了.
没有任何严格而快速的规则来针对元素处理的方法,总存在替代方法来违背这里设定的定义.
jar任务要求有指定一组模式来包含和不包含各种文件和目录的功能.它同样要求提供一种添加JAR的清单文件的方法.
在我们的设计中,我们选择实现处理内嵌元素的能力.第一个要求,模式处理,已经是MatchingTask中的一部分加以实现了.第二个要求,为清单文件指定属性,
需要在我们的jar的实现中有明显的处理.重新看看这个任务的XML,特别关注内嵌元素:
<jar jarfile="test.jar" manifest="manifest.mf" basedir="somedir">
<manifest>
<attribute name="SomeAttribute" value="SomeValue"/>
</manifest>
<fileset dir="somedir">
<include name="**/*.class"/>
</fileset>
</jar>
从这个XML例子中,我们制作了一个表格(见表格5-2)针对jar任务的内嵌元素.我们指定他们的描述和标志这个类是否必须实现此相关的功能.
记住,内嵌元素都有他们自己的相关类.在这个分析中,我们假设那些类都写完了并能工作.他们的实现在概念上和Jar任务的的实现相差无几.
表格 5-2
内嵌元素 描述 是否需要在Jar任务对象中实现
Manifest 加上Jar清单文件 是的,在Zip对象上没有可用的
Fileset 为Jar文件建立包括或不包括的文件和模式 不需,因为在MatchingTask对象已经实现了,Zip又继承了.
JAR清单文件(Manifest File)
清单文件是一直都未被充分使用的JAR规范.通过清单文件,你可以增加对一个档案都包括些什么的内容描述.通常,这些描述是一些版本号或类库名.
在工作文件中指定一个清单文件可以消除(alleviate)自身代码内来管理清单文件的需求.
在写原始的jar任务时,开发者提供一个清单对象来管理清单信息(例如它的属性和属性的值)也能够以jar文件写入磁盘.
另外,清单对象知道并能传递内嵌属性.出于我们的目的,我们假设改类已经存在并且在稳定工作.
起初,我们好像需要Ant在普通"内嵌元素"处理阶段来处理manifest节点.然后是一般任务生命流程.
但是,等待处理manifest节点意味着从此节点而来值和数据就要等到生命流程的执行阶段才可用.这要求我们实际上实现Jar任务对象的execute()方法.
而这一直是我们避免作的.我们需要在处理阶段前让Ant处理好manifest节点.进入addConfiguredManifest()方法(Jar类中的):
public void addConfiguredManifest(Manifest newManifest) throws BuildException{
if(manifest == null) {
throw new BuildException();
}
manifest.merge(newManifest);
}
addConfiguredXXX()家族方法告诉Ant在传递节点时处理节点而不是要等到运行期才处理.
在我们的例子中,newManifest参数应该是一个完全设置好(fully populated)的Manifest对象.
这个方法没干什么仅是完成了对根本错误的检查和把现有的清单文件内容进行与参数文件内容进行合并.已存在的清单文件来自于Jar任务中的manifest属性.
如果没有现有的清单文件存在,merge方法强制Manifest生成一个新的.该方法是Manifest对象的特征.
文件模式匹配功能在Ant的大量任务中存在,因此理解他的实现非常重要.你不需要自己编写代码来处理文件模式(file pattern).
要想了解全部的关于文件模式的处理,请参加Ant发行版本里的Zip和MatchingTask的源码.下面是fileset内嵌元素的处理方法,addFileset():
/**
* Adds a set of files(nested fileset attribute).
*/
public void addFileset(Fileset set){
//Add the FileSet object to the instance variable
//filesets,a Vector of FileSet objects.
filesets.addElement(set);
}
谈论完了生命流程和内嵌元素的复杂,你肯定在想事情更加复杂了,是吗?唯一简洁的事情是,Ant严格依赖面向对象的设计和内省机制.
面向对象编程的特点意味着设计有时可能复杂,但是却换来了容易编码和代码维护便利的便宜(trade-off).
XML中标签-类(tag-to-class)的关系的概念是使得前期代码很简短.当你在写一个类似于jar的任务,你可以想象FileSet对象包含了所有的事情.
你仅仅需要关注(Worry About)这个设计优美的接口.
既然Jar类需要维护一组FileSet对象,它就需要什么东西来保存他们.谢天谢地,Java有丰富的集合类-在这个例子中,我们使用一个Vector.
当然,我们实际上对包含FileSet对象的Vector的处理是相当复杂的.幸运的是,我们仅仅必须在一个地方写下这种实现,那就是在execute()方法内;
对于jar任务,我们甚至不需要自己完成它.
5.4.5 实现execute()方法
execute()方法实现了任何任务的核心逻辑.写一个任务时,实现execute()方法部分时最容易的部分.Ant会在处理该任务的最后一个阶段调用它.
execute()方法既没有参数也没有返回值.Ant在任何任务上的调用,这是最后一个方法.因此,到此时,你的任务类应该拥有它处理工作所需的所有信息.
早些章节,我们提到了Zip已经完美的实现了execute()方法.我们不需要再为Jar类写什么.并不是我们要回避,这正好是一个高效代码重用的良好例子.
为了解释为什么我们不需要写自己的execute()方法,我们进一步分析Zip的execute()方法.我们在我们的分析中不会设计Zip特有的操作,因为我们重点在于
怎么写Ant任务,而不是编程性质的构建和管理JARS.
我们把对execute()方法的分析分成三部分:校验,执行实际工作,错误处理.这是简单而一般的方法来描述如何实现一个任务的核心操作.
在写你自己的任务时,请牢记这三部分,因为他们帮你设计一个更好的任务.在进入execute()方法的各个部分之前,让我们看看这个方法的样子(signature).
public void execute() throws BuildException{
这里没什么特别的.没有参数和返回值可以关注.错误通过BuildExceptions传播,和任务的其他接口方法是一样的.
5.4.5.1 校验
我们分析的第一部分关于(concerns)校验.我们需要校验Jar任务属性的值.另外,我们必须检验在这些属性值上面,我们的任务能否完全跑起来.
有效的参数是非空的,能表示任务的属性的参数值.对大部分情况来说,这种校验发生在setter()方法中.Ant调用各setter方法是无序的,说不清楚哪个会最先被设定.
任何属性间的关系校验必须在execute()方法中完成.所有运行期的校验也必须在execute()方法中完成.
在下面的代码片断中,我们检验任务所必须的的属性和元素.在我们的例子中,我们仅需要basedir属性和fileset元素.
if(basedir == null && filesets.size() == 0){
throw new BuildException("basedir attribute must be set"+
"or at least one fileset must be given!");
}
下面,我们检验Zip文件的名字是有效的(非空)-或,我们的例子是JAR文件.
if(zipFile == null){
throw new BuildException("You must specify the "+/
archiveType+" file to create!");
}
这就是校验了,没什么更多需要做的,事实上,但这些小问题可以阻止未来的错误发生.如果任务的校验做的很好的话,数小时的工作可以节省.
5.4.5.2 作实际工作
我们分析的第二部分是使用Ant提供的创建文件集合的功能的对象来创建JAR文件.这里,我们引进两个帮助对象,FileSet和FileScanner.
两者代表了不同的方法来存储文件和目录的集合,但他们在功能上并不相同.FileSet对象直接和fileset元素节点及其子节点之间相关.
FileScanner是一个可以在当前文件系统中作平台分析(platform-agnostic analysis)的对象.
他能比较文件集合或和其他扫描器进行比较来判断文件是否改变或丢失了.
一旦Ant处理fileset节点,FileSet对象有许多功能强大的方法来从设置好的对象上获取信息.
下面部分使用基目录属性(basedir)和文件集合来建立一组扫描器(scanners).在这个例子中,我们建立一组扫描器来和档案文件比较,如果存在的话.
他是一种实时的检查,如果可能则能消除不必要的工作.getDirectoryScanner方法来自MatchingTask类.
// Create the scanners to pass to isUpToDate( ).
Vector dss = new Vector ( );
// Create a "checkable" list of the files/directories under the base
// directory.
if (baseDir != null) {
// getDirectoryScanner is available from the MatchingTask object
dss.addElement(getDirectoryScanner(baseDir));
}
// Create a "checkable" list of the files/directories
// from the FileSet, using the FileSet's characteristics
// We pass the project object in so the list can include
// global filters set in the project's properties.
for (int i=0; i<filesets.size( ); i++) {
FileSet fs = (FileSet) filesets.elementAt(i);
dss.addElement (fs.getDirectoryScanner(project));
}
// Create the FileScanner array for the isUpToDate method
int dssSize = dss.size( );
FileScanner[] scanners = new FileScanner[dssSize];
dss.copyInto(scanners);
// quick exit if the target is up to date
// can also handle empty archives
if (isUpToDate(scanners, zipFile)) {
return;
}
下一代码段有一个try-catch块,用来捕捉IOException,还有一个finally块用来关闭ZIP档的文件流.(我们下一部分分析catch块).
这一部分把基目录下文件集合中的文件加到ZIP/JAR档的文件输入流中.addFiles方法并不是那么重要,它使用FileSet对象获取各个文件名
并把他们放到输入流中.
try {
// Add the implicit fileset to the archive.
// The base direcory is set via the basedir attribute
if (baseDir != null) {
addFiles(getDirectoryScanner(baseDir), zOut, "", "");
}
// Add the explicit filesets to the archive.
// addFiles is made available with the Zip object
addFiles(filesets, zOut);
}
try块的这部分提供建立档案和流的实际功能在这一章并没有展示.简洁的说,它使用帮助对象来建立显式的文件清单和档案文件.
它清除了任何临时文件以及关闭流和文件对象.如果有什么错误发生,zip对象将抛出一个BuildException,导致工作失败.
那就是为什么文件和流相关的关闭操作要放在finally子句中.这些文件和流必须关闭不管有没有错误发生.让我们更多的关注下一节.
5.4.5.3 错误处理
分析的第三部分是关于错误处理的.你也许想我们先前的校验处理了所有的错误,但是并非如此.
既然我们要和文件和流打交道,IOException的威胁就出现了.我们通过BuildException把错误返回给Ant,因此诸如表示一个错误,空对象和IO异常的,最终都变成一个BuildException.
为了更精确和更好和用户的交互(communication),应该分析你的错误并生成描述性错误信息.
这些消息将会出现在工作日志中,因此,他们应该是易读的,你可以通过提供一致的文本结构,所以你和用户都在日志中进行搜索.
下面的片断是对应上节try块的catch块.一个IOException是否应该在处理流或文件的时候发生,这段代码生成了一个描述性的消息.
包括档案被删除之前对档案的一些测试结果的显示.BuildException由消息,原始错误异常和异常位置构成.从新调用它,Ant会维护一个名为location的对象作为一个执行指针.
它包含XML档的行号和错误发生地的工作文件名.
} catch (IOException ioe) {
// Some IO (probably file) has failed. Let's check it out.
// Create a descriptive message
String msg = "Problem creating " + archiveType + ": " + ioe.getMessage();
// delete a bogus ZIP file
// This essentially rids us of the partially created zip/jar
if (!zipFile.delete( )) {
msg += " (and the archive is probably corrupt but I could not delete it)";
}
// This functionality deals with updating jars
if (reallyDoUpdate) {
if (!renamedFile.renameTo(zipFile)) {
msg+=" (and I couldn't rename the temporary file "+renamedFile.getName( )+" back)";
}
}
// the message has been built. Send it back to Ant.
throw new BuildException(msg, ioe, location);
}
5.4.6 编译此任务
编译一个任务包括使用当前Ant的库文件,ant.jar,和一些基本的包结构.许多人把他们的任务放在org.apache.tools.ant.taskdefs.optional包下,
虽然Ant并没有要求必须这么作.选择一个最适合你的包和工程结构.除非你写很多任务,否则以后改变包结构是很容易的操作.
你总能编写一个工作文件来编译你的任务.这里有一个简单的,可以助你开始.
<!-- Build the custom tasks in this project directory. We'll
assume that all the custom task classes are packaged under
the 'src' directory and that the results will wind up in
'dist'. Users must change the value for the Ant directory
and include any further libraries they choose to use with their
tasks.
-->
<project name="customtasks" basedir="." default="all">
<property name="src.dir" value="./src"/>
<!-- Note the absolute directory. CHANGE THIS BEFORE BUILDING -->
<!-- It would be possible to use environment variables, but we do not assume they are set -->
<property name="ant.dir" value="/opt/ant"/>
<property name="ant.lib" value="${ant.dir}/lib"/>
<proptery name="build.dir" value="./build"/>
<property name="dist.dir" value="./dist"/>
<!-- Compile all of the task object classes -->
<target name="all">
<mkdir name="${build.dir}"/>
<javac srcdir="${src.dir}" destdir="${build.dir}">
<classpath>
<fileset dir="${ant.lib}">
<include name="**/*.jar"/>
</fileset>
</classpath>
</javac>
<copy todir="${dist.dir}">
<fileset dir="${build.dir}"/>
</copy>
</target>
</project>
此编译文件在子文件夹src下和相应的包目录下找到你的任务对象来编译.然后拷贝编译结果的类文件到dist目录下的合适的包结构.
一旦我们有类了,我们仅仅需要部署和定义任务了,使他对于Ant是可见的.我们使用taskdef标签来完成此事.
对于本章的jar版本,一个工程建立可以象下面这样工作:
mytasks/
build.xml
dist/
build/(temp build directory)
src/org/myorg/tasks/*.java
保持简单.如果你仅仅写一个任务,就没什么必要这样过头(go overboard)的去管理你的工程.一旦我们编译了jar任务,我们可以把它打成JAR包放在dist目录下.
比起目录和工作,创建一个新的Jar来打包你的任务是件容易的事情.剩下的就是部署任务使得你的工作文件可以用到它.
5.4.7 部署和声明任务
我们的任务有两中部署方法,类文件或者打成包的JAR文件,他们之间的差别仅仅是维护方式的不一样.若要给一些比较的话,所有的内置(build-in)任务都是作为JAR部署的.
他们是ant.jar的一部分.在那个档中有一个文件,default.properties.在这里面,维护者为Ant声明了每个可用的任务.作为一个属性文件,他是一系列名值对的列表.
我们可以扩展这个列表来声明我们的任务.
如果你想添加任务到Ant的源码树上,理论上你能修改default.properties文件,添加你的新任务.在这个例子中,不是单独编译你的任务,你必须重新编译整个Ant,生成新的Ant的JAR包.
这种方法最适合于Ant系统范围的发行,这样你需要一个小组中的所有开发者来维护这些代码并且还需要一个相同的工作环境.
你的小组必须维护自己的内部版本,但是也可能已经维护了其他的工具,因此增加一个任务不会导致大的变动.
这有个例子,如果你想添加任务foo(相关对象是org.apache.tools.ant.taskdefs.optional.Foo)到Ant的核心任务组中,打开文件default.properties,
此文件在src/main/org/apache/tools/ant/taskdefs,在文件中添加一行:
foo=org.apache.tools.ant.taskdefs.optional.Foo
结果是,下次你编译Ant,你的任务类和它的声明将变成核心任务列表中的一员.如果你对编译Ant想了解更多的内容,参看Ant源码发行中的
docs/manual/install.html#buildingant.
如果你不采用上述方法,你必须采用在每个工作文件中都加上taskdef节点的方法来对Ant进行声明这个任务.
你可以把这个声明放在工作文件的工程级或目标级下,这依赖于你的任务的功能范围.
工程级的任务在每个目标中都可以用,而目标级的任务仅仅在此目标内可用.在此目标级声明的例子中,声明的位置也是很重要的.你不能在声明它之前使用它.
下面就是一个例子用来定义任务Jar和指定Jar任务的实现类的位置:
<taskdef name="jar" classname="org.apache.tools.ant.taskdefs.Jar"/>
<tasldef>节点有一组属性,从而他可以决定使用那些属性.一般来说,使用name和classname属性来定义任务的名称和类的实现.
你同样可以指定一个配置文件,里面包含了一组任务名称和任务类.参看第七章中完整的介绍taskdef各种属性细节.
5.5 各种任务话题
作为每六个月就更改一次的事物,Ant决不(by no means)处于最完美的状态.一些行为并不会一时能看清楚.有瑕疵,有bugs还有一些文档中看不到的特征.
以下章节介绍你在写自己任务时需要留心的一些话题.如果你想尝试(live dangerously),实现你的任务,部署它,看看会发生什么.
当你遇到一些你解释不了的问题,回到这一节看看是否这些话题能帮的上你.一些话题,例如System.exit()的问题,除非JVM特性发生变化,否则将一直存在.
其他问题如奇怪属性(magic properties),随着一些新的任务模型的出现将可能消失.当然,你在尝试的时候,尽量试着避免这些问题.
5.5.1 奇怪属性
几个月前,javac任务出现了.许多人说它很棒,也有许多其他人点头赞成.这个时候,在主流的Java平台上(Solaris,Linus,Windows)至少有三种不同的编译器可用.
这些编译器有javac(以及其不同的编译模式),IBM的jikes和Symantec的sj.
不是把编译器类型作为<javac>节点的属性,开发者决定应该有一个全局的设置,来影响javac任务的使用.这个全局设定应用于每个javac的工作或其他继承自javac的任务工作.
例如,只要改变一行,Ant使用者就可以从jikes切换到javac.这很好对吗?是,也不是.
一个全局的编译器标志好在它能够保证产生的字节码的一致性.平均说来,你不能用jikes编译一部分,而用javac编译另一部分.
实际上,诸如编译标志的标志方法是一个不错的主意.但是,不利的一面是它使得全局一样(all-encompassing).如果我们需要象上面说得功能呢?Ant会说:哦,不行!
从这同一角度看,你这样的设计就是不好的.那么为什么我们要担心奇怪属性呢,即使是在我们了解了这个顺序之后.
实现中是否产生奇怪属性的问题依赖于在Ant任务模型中如何看待设计漏洞问题.所有的任务都有对Project对象的引用.
简单说来,Project对象是Ant引擎中最具威力的对象.它拥有对所有属性,目标,任务,数据类型等的引用.有了Project对象,任何任务能看到任何属性(包括奇怪属性),
即使任务被标记为不可见状态.只要你理性的使用这种强大,用只读行为,一切都还挺好,从编程角度讲.
为了证明我们关于奇怪属性不是好的想法的说法,让我们看看从工作文件编写者的角度来看这样的问题-尤其是按照工作文件的XML标记.
在XML中,任务是自包含的元素,一个任务的可见范围开始一个开标签中止于闭标签.当你引进可以影响任务操作的属性时,而这些属性却在开闭合标签之外定义,
你就打破了这种XML的可读性以及牺牲了任何可见的直觉的范围概念.
你可能认为很多的属性替代(例如,attribute="${some.predefined.property}")是我们描述的问题的一部分.但是我们要讨论的不一样.
即使你在任务闭合圈外定义属性甚至你在工作文件外定义属性,在任务的XML标记中还是很明显可以看出你使用属性的地方.
使用属性作为任务的属性值或任务内嵌元素的某属性值.
任何一种情况,一个属性在工作文件中要为谁赋值是很明显.相反,你定义一个奇怪属性,却不在用到它.没什么强迫你必须把声明和任务使用它连续起来.
当然,你总可以往工作文件中添加XML说明,但是Ant并不要求你写说明.Ant仅当一个任务需要这个属性时才要求你设定这个属性.
小的工作文件中,你往往不需要注意奇怪属性的问题.在这些工作文件中,范围(scope)往往构不成问题.
在大型项目中,尤其是使用重叠工程目录和工作文件的情况下,奇怪属性可能造成问题.
可以在一个主工作文件中设置奇怪属性,在重叠的其他工作文件中使用他们.换句话说,一次工作的行为可能因为不明显的属性声明而不同.
这就产生了混乱且造成不易追踪的错误.
对于javac,你没什么可以作的来改造源码以维护自己的Ant版本,这也是你应该避免的.
当你使用javac的奇怪属性,好好写点注释好让使用者明白为什么使用这个编译器代替那个.当你写自己的任务时,尽最大努力避免引用工程级的属性.
5.5.2 System.exit()的问题
对于很多美好事物来说,银云边上都有黑幕.其中的一个就是在Java程序中错误的使用System.exit().
拷贝C的编程模型,Java开发者使用System.exit()方法来结束他们的程序,或者是一个不可处理的错误发生了,用户强行要求退出.
System.exit()返回错误码给系统(准确的说是JAVA虚拟机).传统上,0表示成功或没有错误,任何非0值意味着失败.
问题是,System.exit()直接告诉虚拟机而不管类被实例化,也不管调用栈有多少层.任务误以为Java程序能处理这个退出调用,事实上,他们不能.
Java虚拟机处理这个调用.因此,这是如何影响Ant的,又如何影响你?
如果一个任务使用了System.exit(),Ant引擎就会死掉了,因为他的JVM死掉了.这种效果相当于关闭电脑电源,那么此次工作是不会又返回码而结束的.
工作停止了,作为你是一个任务作者,你不应该调用那些使用了System.exit()方法的类.
如果你能避免这个,你需要使用exec或java任务,或者把这些任务的实现借鉴到你的任务中.
exec和java中止JVM处理,意味着System.exit()调用永远都不可能发生在Ant的JVM内.如果考虑你需要实现象这样的想法,请阅读java任务和中止方法.
第七章和附录B.当然你看java任务的源码类,Java更好.
调用System.exit()也许对古怪,非预期的行为负责.
例如,如果你使用java调用网上新的XSLT程序,在执行过程中却意外死掉了,很可能就是这个程序中调用了System.exit(),而成为罪魁祸首.
记住,为将来的引用,System.exit()并不是你的朋友.它仅仅应该只在任何类的main()方法中存在.
(第五章翻译完成,待续第六章关于BuildEvent的相关内容.)