分类:
2008-09-08 18:22:45
在多次的构建过程中,一个好的构建工具或构建过程,不应有不必要和冗余耗时的工作花费在那些尚未改变的代码上.换句话说,它应该做的就是把那些变动过的地方添加进来进行重新构建.如果你的构建工具或构建过程没有以上所述的表现,那么可以考虑是否能避免做无用功,从而优化你的构建.
举个例子,假设有一个自上而下的构建过程,即此工程是从持久层到更高一层的构建.通常,这类工程因为那些所用的代码生成器和反转引擎工具会导致非常长的构建时间.如图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 ( 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
...
/>
description="Runs Middlegen to create Entity Beans "
depends="create-database"
unless="middlegen.skip" >
...
在例子中我们完成了target级的优化。如果一个 target文件或一个 target集合比他们的源文件还新的话,Ant的”uptodate”任务将设置一个属性。”touch”任务将改变文件的修正时间,或者可能是同时创建一个修正时间。
在我们样例的构建过程,我们可以在”create-database” target中做另外一个优化和自动化处理,这个”create-database” target是在SQL脚本改变时要运行的。如此一来,我们将对比SQL脚本文件的标签时间和SQL target最后一次执行时所产生的临时文件的时间标签。
需要牢记在心的是,自动的跳过”create-database” target的执行必须仔细考量,因为一个多余的SQL文件的改变(比如,敲了一个回车或加了一个空格)都有可能重建数据库甚至有可能损坏现有的数据,而这些在工程中发生都是我们所不愿看到的,特别是团队开发时使用一个通用的数据库服务器。在这种情况下,把”create-database” target独立出来,手工的单独执行这个 target。但是在一个数据库schema改变比较频繁而你又处于开发底层的工程项目中,这种执行以及自动化的跳过”create-database” target将有助与你,敲更少的键盘,而获得更加自动化的构建。
我们通过加入下列这句来跳过”create-database” target的执行。
init-skip-properties target:
同时改变”create-databas”的 target定义,如下所示:
unless="create-database.skip" >
在你想要比较 target的最近一次执行的时间标签和此 target所依赖的两个或两个以上的 target的最后一次执行的时间标签的情况下,利用”condition” target可以实现你的要求。下面举例,Ant脚本将对比 target3与 target1和2:
isting 3. Ant's condition task
要铭记在心的是,这种跳过不必要的 target执行的技巧更适合于构建的某些部分,这些部分就是一阶段接一阶段的代码是自动生成的,也就是说,在这些阶段在 target执行期间,数据的输出没有人为的去变动。如果想获取更多的Ant target信息,请参阅资源。
小技巧
对于那些熟悉CruiseControl的人,你可以通过设置配置表CVS标签的property/ondeleteproperty属性值,使得是CruiseControl服务器上的构建工作体现出增量式和条件化。这些属性可以在CVS标签中特别标明的模块产生变化或被删除等动作自动被置值。之后你就可以通过这些属性是否被置值,来有条件的进行构建工作。获取更多关于CruiseControl的信息,请参阅资源
在构建中使用快速和敏捷的任务
如果可能的话,尽可能的使用快速的任务来替代,不要在你的构建中夹杂着那些显得迟钝的任务。如果一个任务没有做依赖性检测或者做的检测显得不准确不适当,那么试着找到一个更为简洁的任务版本,即使有困难,也要试着延展当前的任务在尽可能的情况做敏捷的依赖性检测。这样的决心,可以使你的构建真正的变成快速的增量式构建。
使用Jikes来快速的编译代码
在这个单元,我将讨论在构建中发生最多的代码的编译任务。想获得一个更加快速得编译,我们不是用javac而是使用Jikes来实现,因为它表现的更为快速和敏捷,并且依赖性检测执行的很好。唯一遗憾的是Jikes的移植性较差,因为它不像javac一样用java编写,而是用来编写的。
把Jikes设置为工程默认的编译工具,同时把构建配置文件的构建编译器属性设为Jikes。如果你设置了构建编译的”完全依赖检测”(full dependency check)这个属性属性,那么Jikes将执行完整的依赖性检测。Jikes的全依赖性检测显得更可靠,因为它同时也会去检测那些已被更新的类所使用的类,以及间接检测。
为了更好的说明发生在Jikes中的全依赖,我们假设有三个类,分别为A.java B.java和C.java。类A对类B有某种依赖,而类B对类C有某种依赖。Jikes的全依赖分析机制将使我们在修改类C而后如果执行类A的Jikes编译的话,也会同时进行类C的编译。如果没有选择完全依赖,那么编译器只会重新编译直接依赖的那些类。在我们运行Jikes时如果没有选择完全依赖这个选项,那么类A的Jikes编译不会触发类C的编译工作。
Jikes是如此快速的进行一次重编译(清除掉原来的类文件,重新进行编译),所以备受推崇。我还要提到一点的就是,在使用Make的时候你可以用Jikes生成依赖性信息。
Ant 使用普通的编译器时,只通过资源的名字和类文件来确定是否需要重构建。它并不关注源代码本身,所以它也不清楚类层次以及类的命名等不同于源代码的地方。你可以利用Ant的”depend” 任务去改变这种情况。
“depend “任务是基于某种原则,而不仅仅通过存在物或修正的次数。它的工作方式是先判断那些类对比于其源码已经过期,然后把那些依赖于已经过期了的类统统的移除。为了决定类的依赖性,”depend” 任务分析类文件而不以任何方式去解析源代码。这种分析通过编译器编译时编码到类文件的参考信息来执行。这种方法比直接去解析源码快的多。
一旦”depend”任务挖掘出所有的类的依赖性关系,它把这种关系转化为某种信息,这种信息就是对于任何一个类,是否有其它的类对它有依赖性。这样就会产生一个“影响表”,通过这个表可以判断那些类是受已经过期了的类的影响的而成为无效文件的。这些无效的类文件会被移除,以此触发相关的类的编译工作 。这个”depend”任务支持“关闭”属性,这个属性控制着”depend”任务是否只是考虑类与类之间有直接关系的,或可以是传递关系,甚至可以是间接的关系的。这个任务可以用来完善那些编译显得不是那么优秀而且缺乏合适的依赖性分析的编译器.
注释
对于Ant的”javac”任务,有必要确定输出和源代码处于同一个目录结构,也就是说,源文件的目录结构和它们的package必须是一样的,否则,依赖性检测将不会工作,因为它是在那个相对与源文件夹的文件夹中查找一个类文件的Java源文件,而且这个文件夹也是相对与输出的基本文件的文件夹就是放置类文件的文件夹。当你每一次执行”javac”任务时,编译器将都随之执行一个完全的重编译。
注意
你可以从javac中获得”depend”任务的功能,通过设置支持依赖性属性的编译器的依赖性属性置,但是众所周知的是常规的javac编译器的依赖性选项是个臭虫。
需要说明一下的是有些编译器的依赖性检测显得更加优化而且更为准确,比如Eclipse的增量式编译甚至可以只是重编译一个方法。
把你的工程分解成高聚合,低耦合的模块
经常性的检测你的代码,提取出package中和layer中的依赖性关系。之后你可以指出那些依赖是错误的并据此重新构建你的系统设计。把你的系统细化,分解成一个个高聚合,低耦合的组件,同时把可以共享的或通用的组件作为一个独立的模块。Java的类必须正确地放置于包中并合理的部署于系统结构的层中。处于不同层中的Java类彼此之间必须拥有正确的外部依赖性。比如,用户接口层的代码就不应该对商业层的代码有什么依赖性。
这种分解的习惯有非常多的优势:它积极倡导面向对象的编程方法。编程质量,诸如外部扩展性,代码重用性,系统可维护性都受到设计的包中类之间相互依赖性的影响。同时它也减少了构建时间。在某个层中编译时,通过去除其它层中的错误的依赖关系,避免了在其它层中无谓的代码重编译工作。
同时一个组件如果独立出来了,那么单独开发一个组件的开发者只需运行和测试属于自己组件范围内的东西。这也就保证了不用去执行其他模块的所带来的重构和测试。很多免费的工具和类包具有这种分析特性。比如,你可以使用JDepend工具,这种工具将分析包与包之间的依赖关系并通过这些关系生成报告。这个报告包括某种对称性,如传入的耦合(包中使用一个其它的包)和传出的耦合(被其他包所使用)。你可以通过命令行的方式或Ant去执行它。它同时提供图形化和命令行式的输出。在Ant中使用它只需简单的把下面的代码片断加入到你的Ant文件中。(首先你必须到网上去JDepend包,请参阅资源)
Listing 4. Run JDepend from Ant
这段代码将产生一个包含分析结果的XML文件
并行的执行任务或 target
你可以使用”parallel”任务在Ant中并行任务。并行的任务各自执行一个线程.因此可以尽可能的获取进程的资源以降低构建时间.
Listing 5. Ant's parallel task
这个任务一般只是用于测试。程序服务器插入一个线程而测试执行的是另外一个线程。想获取更多关于并行任务的资料,请参阅资源。需要注意的是,这些并行执行的任务之间不能存在依赖性的关系。
使用”apply”任务执行系统命令更新源文件
如果你只是想在一些源文件被更新时执行某一个系统命令,你可以使用”apply”任务。某种程度上他类似make的规则定义。如果你特别的指定一个嵌套的mapper和dest属性,那么每一个源文件的时间标签将与在嵌套的mapper中定义而在dest中搜索所得的 target文件的时间标签作对比。然后,命令只在更新源码时执行。在Ant中调用系统命令,会使构建文件的移植性收到限制。
Listing 6. Ant's apply task
想要获取更多关于”apply”任务的信息,请参阅资源
结论
增量式快速构建对工程的进度有极大的影响力,特别时团队开发采用XP的持续集成原则时。
下面我们对文章提到的关于如何加快构建与如何优化构建的技巧进行总结。
1.保持任务之间准确的逻辑的依赖关系,并且使你的构建略过那些不必要的 target的执行。
2.使用诸如Jikes的快捷和简洁的任务。
3.把你的系统分解的小巧,且具有高聚合,低耦合的特性的组件。
4.在更新源文件时使用Ant的”apply”任务来执行系统命令。
5.如果可能的话,尽量地并行执行任务。
你也可以开发一个Ant启动程序 ,这个程序内在地实现了跳过 target的技巧,这也将使得你的原Ant文件只需更少的修改.