ant——你要是不会出门都不好意思跟人打招呼的那个ant每个人都用过
它是一个build tool用xml来描述target用xml来设置每个task的属性
ant的好处我们都体会到了
什么都是xml而xml地球人都知道
功能强大从编译java文件到checkin cvs反正几乎你想得到的功能它都能作
扩展容易如果你发现某个功能ant没有自己实现一个Task类就是
一些功能设计得很合理比如javac和java自动检查时间戳和依赖关系检查等等
但是用多了发现缺点也不少
什么都是xml而xml的语法有些时候显得很繁琐
xml用来描述逻辑异常笨拙
所有的逻辑都只能在java里用Task实现要做一些跨越不同Task之间的通讯很困难比如先读取第一个文件的时间戳再读取另一个文件中储存的时间戳再根据两个时间戳之间的距离判断下一步调用哪个task或者target
xml的代码重用困难很难定义一些常用的xml element作为库然后再不同文件甚至项目中重用
对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){thisv=v;}
}
PrintCommand负责打印一句话
java代码:
class PrintCommand implements Command{
private final String msg;
public Object execute(CommandContext ctxt){
ctxtgetLogger()log(msg);
return null;
}
PrintCommand(String msg){thismsg=msg;}
}
FailCommand负责报告错误
java代码:
class FailCommand implements Command{
private final String msg;
public Object execute(CommandContext ctxt){
throw new CommandException(msg);
}
FailCommand (String msg){thismsg=msg;}
}
如此等等这样的基本元件还有很多比如file copy javac zip jar等
但是如果仅仅如此那么这个工具的能力最多也就和ant一样
我们最需要的是把不同的command组合起来的能力
而组合最常见的就是顺序执行
java代码:
class SeqCommand implements Command{
private final Command c;
private final Command c;
public Object execute(CommandContext ctxt){
cexecute(ctxt);
return cexecute(ctxt);
}
SeqCommand (Command c Command c){
thisc = c;
thisc = c;
}
}
上面这个简单的Command就负责按照顺序执行连续的两个command
除了顺序执行还有错误处理我们也许会希望当某个步骤执行失败时去执行另外一个动作为此我们需要先定义一个接口来描述错误恢复
java代码:
interface CommandRecovery{
Command recover(Throwable th)
throws Throwable;
}
当某个command失败的时候这个接口会被调用实现这个接口可以有选择地对某一种或者几种错误进行恢复
然后定义具体的错误恢复逻辑
java代码:
class RecoveredCommand implements Command{
private final Command c;
private final CommandRecovery c;
public Object execute(CommandContext ctxt){
try{
return cexecute(ctxt);
}
catch(Throwable th){
return crecover(th)execute(ctxt);
}
}
RecoveredCommand (Command c CommandRecovery c){
thisc = c;
thisc = c;
}
}
有trycatch就有tryfinally我们也可以定义一个command让它保证某个关键动作必然运行
java代码:
class FinallyCommand implements Command{
private final Command c;
private final Command c;
public Object execute(CommandContext ctxt){
try{
return cexecute(ctxt);
}
finally{
cexecute(ctxt);
}
}
FinallyCommand (Command c Command ){
thisc = c;
thisc = c;
}
}
前面的顺序执行我们是直接扔掉了前一个command的返回值但是有些时候我们也许希望根据第一个command的返回值来决定下一步的走向为此仿照CommandRecovery接口定义CommandBinder接口
java代码:
interface CommandBinder{
Command bind(Object v);
}
然后定义BoundCommand类
java代码:
class BoundCommand implements Command{
private final Command c;
private final CommandBinder c;
public Object execute(CommandContext ctxt){
final Object v =return cexecute(ctxt);
return cbind(v)execute(ctxt);
}
BoundCommand (Command c CommandBinder c){
thisc = c;
thisc = c;
}
}
先透露一下这个BoundCommand非常重要就是它负责在不同的command间传递信息
基本上的框架搭好了下面假设我们用一个类似groovy的脚本来写某个target我们的目标是先取得当前时间然后打印出这个时间然后调用javac最后在程序结束后打印程序结束的信息
java代码:
new BoundCommand(
new GetTimeCommand()
new CommandBinder(){
public Command bind(Object v){
final Command c = new PrintCommand(build time is +v);
final Command javacc = new JavaCCommand();
final Command done = new PrintCommand(build successful);
return new SeqCommand(c new SeqCommand(javacc done));
}
}
);
上面的代码先调用GetTimeCommand取得当前时间然后把这个实现传递到这个匿名类中去这个匿名类根据这个时间创建了下一步的command c
接下来它调用两次SeqCommand来表达两次顺序执行
最终当这个command被执行的时候它就会完成我们上面要求的几个步骤
不错挺好达到了在步骤间任意传递信息的要求甚至我们也可以重用某些command或者函数
唯一一个问题这个代码他妈的比xml还恶心!
这还是很简单的情况如果我们综合顺序错误处理分支等等代码会丑陋得不忍卒睹
看来不是随便什么script都可以胜任的
那么让我们先静下心来反过来想想我们到底希望有什么样的语法呢?
写伪码应该是这样
java代码:
time < getCurrentTime
print time
javac
print success
我们的目标是用脚本语言把前面繁杂的java代码屏蔽起来让语法简洁的脚本自动调用上面那些臃肿的代码
幸好我手头有一个脚本语言可以达到类似的语法
java代码:
do {time=now} $
infoprint time>>
javac {classpath=; fork=; compatibility=;} >>
infoprint build successful
这些do >>等函数其实是用SeqCommand BoundCommand等实现的只不过表面上看不到了
更加复杂的逻辑比如包含顺序执行也包含错误处理的
java代码:
auto (infoprintln build done) $
do {time=now} $
infoprintln (build starting at + time)>>
do {t = readFile file} $
do {t = readFile file} $
let
diff = t t;
writeFile file diff
end
这段脚本要先读取当前时间然后打印build start然后先后从file和file读取两个数然后把这两个数的差额写入file 最后无论成功与否打印build done
auto函数的意思是当后面那些东西执行完毕后无论是否出现exception都要打印build done
你如果感兴趣可以试着用java或者groovy写写看看结果多么可怕
如此一个完整的build框架就建立起来了我们只要填空式地给系统加入各种command实现一个灵活优美的build tool就出炉了
最后预告一下基于这个思想的open source 项目Neptune即将启动欢迎有志之士参加
你可以参与这个框架核心的搭建(跟我合作)也可以编写独立的各种Command来丰富框架的功能