ant――你要是不会,出门都不好意思跟人打招呼的那个ant,每个人都用过。
它是一个build tool,用xml来描述target,用xml来设置每个task的属性。
ant的好处我们都体会到了
1。什么都是xml。而xml地球人都知道。
2。功能强大。从编译java文件到checkin cvs,反正几乎你想得到的功能它都能作。
3。扩展容易,如果你发现某个功能ant没有,自己实现一个task类就是。
4。一些功能设计得很合理。比如javac和java自动检查时间戳和依赖关系检查等等。
但是,用多了,发现缺点也不少
1。什么都是xml。而xml的语法有些时候显得很繁琐。
2。xml用来描述逻辑异常笨拙。
3。所有的逻辑都只能在java里用task实现。要做一些跨越不同task之间的通讯很困难。比如:先读取第一个文件的时间戳,再读取另一个文件中储存的时间戳,再根据两个时间戳之间的距离判断下一步调用哪个task或者target。
4。xml的代码重用困难。很难定义一些常用的xml element作为库,然后再不同文件甚至项目中重用。
5。对module的支持有限。
仔细想想,其实,需求发展到逻辑重用,模块管理,不同task通讯等,已经离描述数据这个xml最擅长的领域越来越远了。
如果把task作为基本的组成元件,那么,上面提出的几点需求,都是关注于对这些基本元件的管理和组合。或者说,glue。
到此,口号呼之欲出,那就是:script。
很多script,作为一个完整的语言,是做glue的最理想选手。
下面谈谈我对一个基于script的built tool的构想。
首先,这个build tool仍然需要允许通过java来自定义task。
我们定义这样一个接口:
java代码:
interface command{
object execute(commandcontext ctxt)
throws throwable;
}
我们计划让所有的task(我们这里叫它们command)都实现这个接口。
commandcontext负责传递一些象log之类的信息。
这个execute返回一个object,这个值作为这个command的返回值,可以用来和其它的command通信。
我们允许这个函数抛出任何异常,这个framework将会处理这些异常。
然后,定义一些基本的command,比如,returncommand负责直接返回某一个值
java代码:
class returncommand implements command{
private final object v;
public object execute(commandcontext ctxt){
return v;
}
returncommand(object v){this.v=v;}
}
printcommand负责打印一句话
java代码:
class printcommand implements command{
private final string msg;
public object execute(commandcontext ctxt){
ctxt.getlogger().log(msg);
return null;
}
printcommand(string msg){this.msg=msg;}
}
failcommand负责报告错误
java代码:
class failcommand implements command{
private final string msg;
public object execute(commandcontext ctxt){
throw new commandexception(msg);
}
failcommand (string msg){this.msg=msg;}
}
如此等等。这样的基本元件还有很多,比如file copy, javac, zip, jar等。
但是,如果仅仅如此,那么这个工具的能力最多也就和ant一样。
我们最需要的,是把不同的command组合起来的能力。
而组合,最常见的,就是顺序执行。
java代码:
class seqcommand implements command{
private final command c1;
private final command c2;
public object execute(commandcontext ctxt){
c1.execute(ctxt);
return c2.execute(ctxt);
}
seqcommand (command c1, command c2){
this.c1 = c1;
this.c2 = c2;
}
}
上面这个简单的command,就负责按照顺序执行连续的两个command。
除了顺序执行,还有错误处理。我们也许会希望,当某个步骤执行失败时,去执行另外一个动作,为此,我们需要先定义一个接口来描述错误恢复:
java代码:
interface commandrecovery{
command recover(throwable th)
throws throwable;
}
当某个command失败的时候,这个接口会被调用。实现这个接口,可以有选择地对某一种或者几种错误进行恢复。
然后定义具体的错误恢复逻辑
java代码:
class recoveredcommand implements command{
private final command c1;
private final commandrecovery c2;
public object execute(commandcontext ctxt){
try{
return c1.execute(ctxt);
}
catch(throwable th){
return c2.recover(th).execute(ctxt);
}
}
recoveredcommand (command c1, commandrecovery c2){
this.c1 = c1;
this.c2 = c2;
}
}
有try-catch,就有try-finally,我们也可以定义一个command,让它保证某个关键动作必然运行
java代码:
class finallycommand implements command{
private final command c1;
private final command c2;
public object execute(commandcontext ctxt){
try{
return c1.execute(ctxt);
}
finally{
c2.execute(ctxt);
}
}
finallycommand (command c1, command 2){
this.c1 = c1;
this.c2 = c2;
}
}
前面的顺序执行,我们是直接扔掉了前一个command的返回值。但是有些时候,我们也许希望根据第一个command的返回值来决定下一步的走向。为此,仿照commandrecovery接口,定义commandbinder接口:
java代码:
interface commandbinder{
command bind(object v);
}
然后定义boundcommand类:
java代码:
class boundcommand implements command{
private final command c1;
private final commandbinder c2;
public object execute(commandcontext ctxt){
final object v = return c1.execute(ctxt);
return c2.bind(v).execute(ctxt);
}
boundcommand (command c1, commandbinder c2){
this.c1 = c1;
this.c2 = c2;
}
}
先透露一下,这个boundcommand非常重要,就是它负责在不同的command间传递信息。
基本上的框架搭好了,下面,假设我们用一个类似groovy的脚本来写某个target,我们的目标是先取得当前时间,然后打印出这个时间,然后调用javac,最后在程序结束后,打印程序结束的信息:
java代码:
new boundcommand(
new gettimecommand(),
new commandbinder(){
public command bind(object v){
final command c2 = new printcommand("build time is "+v);
final command javacc = new javaccommand();
final command done = new printcommand("build successful");
return new seqcommand(c2, new seqcommand(javacc, done));
}
}
);
上面的代码,先调用gettimecommand,取得当前时间,然后把这个实现传递到这个匿名类中去,这个匿名类根据这个时间2,创建了下一步的command c2。
接下来,它调用两次seqcommand来表达两次顺序执行。
最终,当这个command被执行的时候,它就会完成我们上面要求的几个步骤。
不错,挺好。达到了在步骤间任意传递信息的要求。甚至,我们也可以重用某些command或者函数。
唯一一个问题:这个代码他妈的比xml还恶心!
这还是很简单的情况,如果我们综合顺序,错误处理,分支等等,代码会丑陋得不忍卒睹。
看来,不是随便什么script都可以胜任的。
那么,让我们先静下心来反过来想想,我们到底希望有什么样的语法呢?
写伪码,应该是这样:
java代码:
time <- getcurrenttime
print time
javac
print "success"
我们的目标是:用脚本语言把前面繁杂的java代码屏蔽起来,让语法简洁的脚本自动调用上面那些臃肿的代码。
幸好,我手头有一个脚本语言可以达到类似的语法,
java代码:
do {time=now} $
info.print time >>
javac {classpath=...; fork=...; compatibility="1.4";...} >>
info.print "build successful"
这些do, >>等函数其实是用seqcommand, boundcommand等实现的,只不过表面上看不到了。
更加复杂的逻辑,比如包含顺序执行,也包含错误处理的:
java代码:
auto (info.println "build done") $
do {time=now} $
info.println ("build starting at " + time) >>
do {t1 = readfile "file1"} $
do {t2 = readfile "file2"} $
let
diff = t2 - t1;
writefile "file3" diff
end
这段脚本要先读取当前时间,然后打印build start;然后先后从file1和file2读取两个数;然后把这两个数的差额写入file3, 最后,无论成功与否,打印build done。
auto函数的意思是:当后面那些东西执行完毕后,无论是否出现exception,都要打印"build done"。
你如果感兴趣可以试着用java或者groovy写写,看看结果多么可怕。
如此,一个完整的build框架就建立起来了,我们只要填空式地给系统加入各种command实现,一个灵活优美的build tool就出炉了。
最后,预告一下,基于这个思想的open source 项目neptune即将启动,欢迎有志之士参加。
你可以参与这个框架核心的搭建(跟我合作),也可以编写独立的各种command来丰富框架的功能。
闽公网安备 35060202000074号