在多次的构建过程中,一个好的构建工具或构建过程,不应有不必要和冗余耗时的工作花费在那些尚未改变的代码上.换句话说,它应该做的就是把那些变动过的地方添加进来进行重新构建.假如你的构建工具或构建过程没有以上所述的表现,那么可以考虑是否能避免做无用功,从而优化你的构建.
举个例子,假设有一个自上而下的构建过程,即此工程是从持久层到更高一层的构建.通常,这类工程因为那些所用的代码生成器和反转引擎工具会导致非常长的构建时间.如图1所示,首先,构建过程通过数据库治理系统(比如,MySQL)运行一段sql脚本,生成一个数据库(译者注:针对某些可以生成新的数据库的数据库系统),添加测试数据.之后,Middlegen任务将生成CMP(container-managed Persistence)实体;接着,XDoclet 任务继续生成romote 接口,local接口 和 home 接口 以及值实体(value object)和上一步生成的CMP实体bean的部署描述。(获取更多Middlegen和Xdoclet的情况,请参阅资源)。接着,产生的代码加上开发者所写的java源代码一起生成class文件,最后把这些class文件,和其它的资源一起打包成.jar,.war..ear后缀的文件。(译者注: jar (Java Archive), .war (Web Archive) and .ear (EnterPRise Archive))
图1. 构建的步骤和输出
现在假设,某个正在从事此系统代码设计的开发人员,需要稍微变动某个if-else块,并且希望看到变动以后的运行结果。所以他必须重新构建,那么构建过程可能要清空之前构建好的所有的东西,从零开始构建。对于开发者而言,这意味着大量的无聊的等待,而所有这些的出现仅仅因为做了一些微小的变动,因为变动而必须要更新编译修改了的类,以便更新所用的相关jar或war文件。
在这篇文章中,我将介绍一些技巧.通过这些技巧你可以通过只仅仅构建那些被更新变动了的部分,从而使得构建变得快速,也就相应的节约了时间。
持续集成
在开始我们讨论之前,我们先对持续集成达成一下共识,所谓的持续集成就是说只要工程的代码库发生了变动,那么这些变动将被构建和测试,并随时得到相关的报告。所做的一切将降低团队整合开发过程的成本和时间。这个过程需要以下条件:
* 一个源码的版本控制系统比如CVS,这样的话你可以把代码放到某一中心地进行维护。
* 一个全自动的构建和测试过程。(比如,用Ant)
*一个可选的,但我们强烈推荐的自动的持续集成工具,类似CruiseControl
我们参照下图(图2所示)看一下一个完全的采用了以上原则的开发团队及其开发人员所做的一切.每个开发者都有工程的本地拷贝.在他被分配新的任务之前,首先他必须根据已经提交给了代码库的变动来更新自己当前的本地拷贝,完成最新下达的任务(比如改变一些代码),在接下来他自己的开发过程中,他有可能会修改代码从而更新自己的工作目录,然后进行构建,测试,假如测试无误的话,那么就可以把自己的代码的变动提交给代码库.
自动化的持续集成工具监控着代码库的变动(CVS),在代码库发生变动时, 持续集成工具会开始一个新的构建,以判定这个变动是否能成功地整合到了代码库.假如变动不能成功的整合,提交变动的开发者就会收到惶跬ㄖ畔?那么他必须撤销自己的提交操作,使代码库的代码恢复到提交前的样子,接着解决整合出错的问题,解决完后再次提交.
图2. 持续集成系统中的各个角色
在这篇文章中,我们主要关注的情形是:一个开发者操作着自己的本地工作目录,他对工程做了一些改动,并想尽快知道这些改动的结果,以及反馈信息. 在完成最后一个任务并把它提交给代码库之前,开发者希望做几个快速的增量式构建.通过使用这篇文章所介绍的技巧,你可以加快你的构建过程,自然而然地节约了开发时间.
注释
加快构建服务器的构建过程本身有它专门的工具和技巧,比如介于多服务器的集群构建(clustering builds)
在进一步探讨之前,我们定义一些对我们的讨论很重要也很有必要的基本术语和概念.
*全构建 (干净构建) 是指从零开始构建,执行构建所要求的全部的步骤.它把所有的资源当作是从未见过的全新的资源来操作,它会完全忽略之前的操作.
*增量式构建: 一种优化了的构建,由最近一次构建以来所产生的变动触发.它只对那些变动过但是目前为止尚未被构建的资源进行构建.
*依靠性检测 所谓的依靠型检测是指查找当前的工程资源和上次的构建生成的产品的异同,并确定资源只是被修改还是是一个新的或其它的资源。通过这种检测,构建工作就会只针对那些需要重新构建的资源,从而体现所谓的增量式构建。
大多数情况,依靠性检测是基于代码的时间标签和这些代码相关的产品。也就是代码的修正时间标签和已经生成了的产品的修正时间标签做比较。假如现在产品的时间标签比生成此产品的代码的时间标签陈旧,那么这个产品就会被标识出来,以便于下一次的重构建。
然而,基于zip的任务(zip,jar,和其他)在依靠性检查中表现得更好。假如我们这些任务的更新参数设为“yes”,那么这些zip文件就会被自己包含得所有入口文件所更新(假如zip文件已经存在)。新的文件将会增加进来,而已过时的文件将会被更新到新的版本。
我将依靠性检查和构建优化分成两个层次:
*Task级 比如,编译任务只编译那些被修改过的资源以及这些资源所依靠的类。
*Target级 完全略过那些不必要的任务的执行。这种优化,将不会进行一个一个地检测所有任务资源的变动的多余工作。
Make构建工具
Make的文件是遵从依靠性原则的。Make是Unix系统下一个能够自动并可以起优化程序结构作用的工具。“make”的效用就是用于自动决定一个大程序中哪些是需要被重编译,从而触发命令进行编译。为了进行Make的工作,必须先写一个称为“makefile”的文件,这个文件将描述你的程序中文件之间的关系,以及更新每一个文件的命令行。“makefile”就是由所有的规则组成的。每一条规则都是解释用什么方法以及在什么时间去构造那些特定文件,而这些文件是某个非凡文件的 target。每一规则由三部分组成:一个或多个的 target,零个或多个的先决条件,零条或多条的命令。
接下来是一个很简单的makefile文件的片断:
Listing 1. Sample makefile
Prog1: main.o file1.o
cc -o prog1 main.o file1.o
main.o: main.c mydefs.h
cc –c main c
Make 程序运行时,先读当前目录中的“makefile”文件,并开始执行第一个 target。Make会检测每一个执行 target的” target依靠”(或称为prerequisites)属性,看看这个 target的执行所依靠的其他 target的是否也是作为一个 target出现。Make程序会顺着这条依靠链,查找依靠性 target(dependencies属性中提到的 target),这个过程是一个递归的过程,返回的条件是查找到的当前的 target没有执行所必须的先决条件,或者这个 target的先决条件没有不受控制.当查找依靠链到达链的末端,程序将以递归的形式依次执行 target中的规则的命令行.
Ant 和Make区别于他们对执行过程的不同看法。Make需要你说明资源依靠,非资源依靠以及转换这些的命令行。Ant 则需要你说明构建步骤,以及这些步骤的顺序(很像一个流水线)。
无论任务自身是否可以执行依靠性检查,Make构建器拥有显式的依靠性检测的机制,用户都可以通过编写makefile,使得构建器执行之。然而,相比Ant,Make并非平台独立。纵观两点,他们都各有千秋,假如能各取所长,那么将皆大欢喜。
技巧和准则
在我们回顾了一些概念和定义之后,我将对增量式构建提出一些如何加快构建以及如何优化构建的技巧和准则。请注重我只是简要的介绍这些技巧,也就是说这篇文章只是一个起点。更多的关于工具本身的介绍,可以参考资源.
注释
Jonathon Rasmusson 在他的文章中讨论了长构建以及解决这些问题的技巧,”解决长构建指引”( "Long Build Trouble Shooting Guide.") 在这篇文章中,他关注于如何提速以及解决自动测试过程的问题。
避免不必要的 target执行
一方面要保证你的构建 target的正确的以及符合逻辑的依靠关系,同时避免在依靠性执行环节发生的不必要的 target执行。忽视 target之间的依靠关系而进行所谓的优化,是极其错误的习惯,因为它要求编程者具体地记住一系列非凡顺序的 target,以得到正确的构建(请参阅Eric M. Burke 写的“十五个最佳Ant使用习惯“(“Top 15 Ant Best Practices“),发表于ONJAVA.com 2003年12月)。实际中应该是让构建文件自己记住正确的依靠关系和同时执行最优化的构建。
回到之前提到的自下而上的构建过程,每次我们执行构建,并不需要去执行SQL命令,Middlegen,Xdoclet等等。不过我们希望保持 target之间那种正确的依从关系,然而,某些情况下的依靠性检测本身却极其耗时(比如,基于数据库来检查实体Beans是否正确),假如可能的话,我们希望彻底地跳过这些工作。
我介绍一个很简单的技巧,通过这个技巧你可以略过那些不必要的 target:检查那些将有可能被忽略的 target的最后执行动作的时间标签和它所依靠的 target的最后执行动作的时间标签,从而决定 target的输出是否是最新的。举例说明一下,假如有 targetA,它的执行依靠与 targetB和C,假如B 和C在A上次执行以来重新执行过,那么就有必要再次执行A以保持数据的一致性,反之则可以跳过A的执行。这种规则依据的条件时,所有的A的输入都有由B和C提供,并且在 target执行期间B和C的输出没有经过人为的修改, target之间的完全衔接。
为了能增加这个功能,我们使用Ant工具的“toUCh“ 任务来实现。 “touch” 任务将创建一个新的临时文件,假如这个文件已经建立,那么它只要去更新临时文件中不必要的 target和它所依靠的 target的执行的时间标签。接着,在执行不必要的 target之前,通过”uptodate”任务我们检查最近一次 target执行所生成的时间标签异与最近一次通过更新 target而执行的依靠性 target生成的时间标签。这个任务将会设置一个跳跃属性,我们把这个属性作为不必要执行的 target的unless属性值,从而使Ant跳过这个 target的执行。
现在让我们先回到我们给出的例子。很明显,当我们改变数据库schema时,我们的这个例子将要执行Middlegen target,这样就可以从数据库产生实体Bean。另一方面,通过改变SQL脚本文件来改变原数据库schema,并通过执行SQL target来执行这种差异。为了把数据库schema在没有改变的情况下不执行Middlegen target的这种逻辑嵌入到构建中,我们需要对比SQL target上一次执行的时间标签和Middlegen target上一次执行的时间标签。假如SQL target的执行时间标签不比Middlegen target执行的时间标签早,我们可以跳过Middlegen target的执行。
Listing 2. Sample Ant file that skips unnecessary targets
<project name="sample-build" default="" basedir=".">
<target name="init-skip-properties" description="initializes the skip properties" depends="init">
<uptodate srcfile="create-database.timestamp" targetfile="middlegen.timestamp"
property="middlegen.skip" value="true"/>
</target>
<target name="create-database" description="runs sql script file on dbms to create db"
depends="init-skip-properties">
<sql
src=" MySQL.sql"
...
/>
<touch file="create-database.timestamp"/>
</target>
<target
name="middlegen"
description="Runs Middlegen to create Entity Beans "
depends="create-database"
unless="middlegen.skip" >
...
<middlegen
<cmp20
...
</cmp20>
</middlegen>
<touch file="middlegen.timestamp"/>
</target>
</project>