该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2005-06-30
它是一个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。 我们定义这样一个接口: interface Command{ Object execute(CommandContext ctxt); throws Throwable; } 我们计划让所有的task(我们这里叫它们command)都实现这个接口。 CommandContext负责传递一些象log之类的信息。 这个execute返回一个Object,这个值作为这个Command的返回值,可以用来和其它的Command通信。 我们允许这个函数抛出任何异常,这个framework将会处理这些异常。 然后,定义一些基本的Command,比如,ReturnCommand负责直接返回某一个值 class ReturnCommand implements Command{ private final Object v; public Object execute(CommandContext ctxt);{ return v; } ReturnCommand(Object v);{this.v=v;} } PrintCommand负责打印一句话 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负责报告错误 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组合起来的能力。 而组合,最常见的,就是顺序执行。 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。 除了顺序执行,还有错误处理。我们也许会希望,当某个步骤执行失败时,去执行另外一个动作,为此,我们需要先定义一个接口来描述错误恢复: interface CommandRecovery{ Command recover(Throwable th); throws Throwable; } 当某个command失败的时候,这个接口会被调用。实现这个接口,可以有选择地对某一种或者几种错误进行恢复。 然后定义具体的错误恢复逻辑 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,让它保证某个关键动作必然运行 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接口: interface CommandBinder{ Command bind(Object v);; } 然后定义BoundCommand类: 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,最后在程序结束后,打印程序结束的信息: 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都可以胜任的。 那么,让我们先静下心来反过来想想,我们到底希望有什么样的语法呢? 写伪码,应该是这样: time <- getCurrentTime print time javac print "success" 我们的目标是:用脚本语言把前面繁杂的java代码屏蔽起来,让语法简洁的脚本自动调用上面那些臃肿的代码。 幸好,我手头有一个脚本语言可以达到类似的语法, do {time=now} $ info.print time >> javac {classpath=...; fork=...; compatibility="1.4";...} >> info.print "build successful" 这些do, >>等函数其实是用SeqCommand, BoundCommand等实现的,只不过表面上看不到了。 更加复杂的逻辑,比如包含顺序执行,也包含错误处理的: 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来丰富框架的功能。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2005-06-30
加精以示鼓励,具体的开发工作就不参与了
|
|
返回顶楼 | |
发表时间:2005-06-30
非常高兴看到ajoo的精彩帖子,与我心有戚戚焉!
我一直非常讨厌ant的xml格式的build文件,也一直呼吁,不要用xml去写带有控制逻辑的东西,带有逻辑的东西应该交给script来完成。 ajoo从jaskell脚本,到yan container,现在到build tool,一步一个脚印,在Java平台上面尝试使用script的方式去做各种事情,虽然我没有能力参与ajoo的项目,但是希望能够在宣传上面贡献一把力量。 |
|
返回顶楼 | |
发表时间:2005-06-30
Ant里面支持bsh,我现在正在将BSF和Xwork整合起来,参照了Webwork里面的一部分。
我对用Script挺有兴趣的,我觉得ajoo的想法可以建立在xwork的基础上,重用xwork的机制可以省很多事情。因为我一开始也考虑自己做个command的执行框架,构思构思就觉得越来越像xwork,索性直接用xwork了,还可以支持很多重要的功能,比如说AOP等等,而且线程是安全的。 |
|
返回顶楼 | |
发表时间:2005-06-30
我认为build tool的关键价值在其提供了许多常用的任务模板,Ant的缺陷就在于组装这些模板时不太方便。只要任务模板足够丰富,用啥实现都无所谓,script也好,java也好。
象我这种懒人,既然已经会了Ant,而且大部分我希望build tool能解决的问题ant都能比较好的解决,我是懒得再去学其他的玩意。 如果我不会Ant,但是会java,我希望的解决方式类似于: JavacTask javac = new JavacTask();; javac.setClasspath(...);; javac.setFork(...);; javac.execute();; JUnitTask junit = new JUnitTask();; ... Task能解决的东西就交给Task解决,Task不能解决的东西,如流程判断等等就交给java语言本身来解决,也不用怎么学习新的东西。再说了,IDE还能提供自动完成哩 再其次,就是一种学习新的script语法,啥时候再出来新玩意就再去学…… 最惨的,就是重头开始,自己一个一个实现每种任务…… 我建议如果真的要做这个项目,还是在Ant外面套一个script的壳比较好,可以调用壳自己提供的任务或是Ant任务,比Ant好的地方就在于语法和流程控制,也许比较容易推广。 |
|
返回顶楼 | |
发表时间:2005-06-30
包装一下Ant, 使用 Javascript 调用 Ant 就好了
|
|
返回顶楼 | |
发表时间:2005-06-30
ajoo这段脚本的可读性其实弱于xml
|
|
返回顶楼 | |
发表时间:2005-06-30
没看太明白前面command的介绍,后来又转到script去了.
1.你想将这些 "顺序,错误处理,分支等等"封装成一个个Command,比如SeqCommand?这样巨难看. 2.还是你的意思是吧"顺序,错误处理,分支等等"这些东西交给脚本,而command 只做如JavacCommand,Jar,BoundCommand(用于Command间的通讯,另一种方案:有了CommandContext,那么Comand可以向CommandContext询问另一个command的执行结果)? |
|
返回顶楼 | |
发表时间:2005-06-30
ajoo
看了你那段位代码脚本,我不得不打击你一下。 实在是很难看。 似乎命令脚本可以带来工作效率的提高,但是其也有很多不足的地方,比如复杂的格式语法,缺乏必要的语法检查... 给你一个Task接口,以及相应的ant meta info Object。你可以用java做你想做的任何事情。至于因为你做的逻辑比较复杂而带来繁琐的代码,那时你的应用问题,而不能怪及到ant身上吧。 |
|
返回顶楼 | |
发表时间:2005-06-30
nihongye 写道 没看太明白前面command的介绍,后来又转到script去了.
1.你想将这些 "顺序,错误处理,分支等等"封装成一个个Command,比如SeqCommand?这样巨难看. 2.还是你的意思是吧"顺序,错误处理,分支等等"这些东西交给脚本,而command 只做如JavacCommand,Jar,BoundCommand(用于Command间的通讯,另一种方案:有了CommandContext,那么Comand可以向CommandContext询问另一个command的执行结果)? 应该是第二个。 就是说java只负责这些原子command,组合,顺序,分支交给脚本。 确实,用java写组合分支巨难看,用换汤不换药的groovy也好不到哪里去。所以我才考虑用一个支持do-notation的脚本语言。 底层,仍然用SeqCommand等类,但是语法的复杂性被脚本屏蔽了。 |
|
返回顶楼 | |