Archive for the ‘技术视野’ Category

随想:企业系统集成

Thursday, July 24th, 2008

从某种意义上说,我们现在做的系统是在刚开始看来是苦不堪言的:一个总页面数量不超过两位数的web项目需要跟超过10个外部系统进行集成,集成的协议包括WebService In/Out(安全与非安全的)、FTP、Email、XML/XSD/XSL等等。然而5个月后第一个版本准备发布的时候,我们发现居然解决了大部分的集成问题(我们对此有准备并且,有最好的组员!)。在与第三方集成的过程中,与WebService、XSD等标准协议的集成相对比较容易,而与那些封闭协议则苦不堪言,耗费大量时间并且整个过程毫无乐趣可言。

例如,需要将PDF通过FTP发布到一个第三方网站。由于PDF不具备自描述特性,传输PDF的同时还需要传一个文本文件来描述这个PDF文件,例如作者是谁,出版时间等等。为了阐述这个文本文件的写法, 第三方系统提供了少则十多页,多则数个DOC文档、PPT演示等让我们学习如何使用这些API。研究和学习使用这种“土制”API充满了挫折感:再丰富的文档也不能涵盖开发的各个方面,通过自身系统的领域模型产生出这样的一个个文本文件的过程并不有趣——想象一下手写webservice文件的过程,更何况这些土制API完全没有规则可言,几乎类似于汇编。这些不是最严重的,更严重的是,对于开发者而言,这些知识——如果他们是知识的话——毫无价值。无论是这些API的维护方还是使用方,一旦转到其他的项目,这些耗费精力学习和掌握的东西立刻价值为零。

相对而言,采用WebService方式的要好的多。一个好的WebService接口明确定义了输入和输出类型。在简单的了解各个属性的业务含义后,开发阶段只需要跟WebService的WSDL交互,开发和测试都很容易。在支持开放标准的IDE帮助下,土制标准带来的无谓时间和精力损耗降到最低,利用开放标准带来的系统之间的耦合也随之降低。其他的,如XSD,也同样具备描述系统接口契约的能力,使用方也能够充分使用工具支持,来完成业务功能。

以前无数次听说开放标准会使系统造价更经济。现在看起来确实如此。观察我前一个项目,一个Outlook插件,运行在Outlook进程中的WPF Rich Client程序。微软乱糟糟的技术在Outlook中得到了集大成。由于对通信内部状态的未知,我们不得不写了很多的Hack方法来绕过各种限制,得到的恶果是严重的性能问题我们不得不推迟上线。痛定思痛,在下一个项目中坚决采用了Web技术,而通过iCalendar协议与Outlook交互。这种方式下,系统之间通过开放标准来隔离彼此的变化,两边的编程模型清晰而简单,没有无谓的猜测。

当然,很多时候我们不得不使用土制协议,如我们,使用的协议是使用了20多年的,要修改绝非一朝一夕的事情。对协议进行直接使用是很愚蠢的,采用接口隔离是一般的做法;另外,为QA提供一个对应的假的实现对于开发阶段的测试也会相当有帮助。

一些想法:

- 把应用往小而专的考虑比大的好。如果不能,说明分析不够。

- 一旦两个系统之间发生了两个以上的同类型交互,一定要警惕——如果他们不能合并到一起通过一个统一的API来交互,那么一定是你的分析出了问题。

- REST很好,但在古板的企业系统中,WebService、XSD带来的强类型的好处,甚至比Rest更好。

- 如果在你的系统中使用了非标准的协议,例如,我们的项目中使用了Buffalo, 那么请确保这个协议的使用范围被限制在某一个层次之内。Buffalo只是用在Service层与Web层之间的传输,在API的封装下几乎可以被忽略。良好的Service、Web分层结构使得Buffalo不那么重要,虽然实现上他确实很重要。

- 说服你的客户采用标准协议。如果不能,继续说服,直到说服为止。否则,惨的不仅仅是你自己,还包含客户。与非标准技术斗争很麻烦却一点挑战性也没有,对于程序员职业生涯而言更是毫无价值;而且在浪费客户的钱。

- 一旦项目被要求做微软系列产品的插件,在没有决定之前,尝试说服客户不要这么做;如果不能,那么做的越少越好;如果还不能(比如被要求在Outlook meeting面板画个新窗口之类),趁早想退路。

富客户端最佳实践之首要:异步

Tuesday, July 1st, 2008

异步操作是改善的用户体验的王道。这个原则用在富客户端开发上,显得更加重要。采用Java/.NET或者其他具备线程操作能力语言的富客户端开发提供了真正的异步执行的能力。

理解并且将这个原则贯穿于整个开发过程并不容易。异步编程往往期待一个基于回调的编程方式,这种编程方式需要在写代码的时候对可能的用户交互进行更多的思考,而不仅仅是实现功能。从编程实践上,这种方式往往牵涉到计算线程与UI绘图线程的交互操作,当有很多的操作同时出现的时候,异步先后执行的无序性也让调试和跟踪变得很麻烦。为异步代码编写测试也相当有挑战。

1. 起步

先看一个例子,界面上有一个按钮,每点击一次,界面上显示当前服务器时间。假定我们使用C#和WebService来实现这个服务器调用:

public void button1_clicked() 
{
   DateTime serverNow = timeServiceProxy.now();
   label1.Text = serverNow;
}

毫无疑问,这段代码是工作的。然而有个可用性问题:当这个webservice调用耗掉很多时间的时候,客户端会一直冻结住。用户感觉就像整个应用程序死掉一样。这时因为C#只有一个绘图线程——事实上,其他语言也一样。当把运算线程与绘图线程放在一起的时候,结果就是绘图线程被锁住。而消息循环往往与绘图线程在一起。消息无法循环了,自然用来响应窗口事件的各种行为如鼠标点击、窗口拖动等也就无法响应。

那么,如何改善?

很简单,将运算放到另外一个线程中。用Java实现大致如下:

private JLabel label;
 
public void buttonClicked() {
  final Date now;
  <strong>Thread t = new Thread (new Runnable(){
    @Override
    public void run() {
        now = serviceProxy.now();
        label.setText(now.toString());
    }
 
  });</strong>
  t.start();
}

这个基本可以工作。其原理是将运算、耗时的工作放到另外一个线程中。在Java Swing中有一些方便的类来简化这个工作,例如SwingUtilities.invokeLater和SwingUtilities.invokeAndWait. 他们都用来在不阻塞UI线程来执行运算操作,并且与UI组件进行交互的方式。.NET WPF中的Dispatcher提供了类似的功能,而BackgroundWorker提供了更细致的控制能力,我们稍后谈。

2. 反馈

仅仅将异步执行放到独立的线程执行是不够的。用户往往希望在后台进行耗时操作的时候,前端能够显示一些提示信息。最简单的提示信息是在界面上的某个地方显示“正在操作,请稍后”。

依然沿用刚才的Java代码,实现方式很简单:

private JLabel label;
 
public void buttonClicked() {
  final Date now;
  <strong>label.setText("请稍后,正在操作...");</strong>
  Thread t = new Thread (new Runnable(){
    @Override
    public void run() {
        now = serviceProxy.now();
        label.setText(now.toString());
    }
 
  });
  t.start();
}

基本原理就是,当开始耗时操作的时候,在某个地方显示等待消息;当操作结束后,取消等待消息。

继续,如果在进行耗时操作的时候出现异常,也应当进行相应的反馈,代码如下:

private JLabel label;
 
public void buttonClicked() {
  final Date now;
  label.setText("请稍后,正在操作...");
  Thread t = new Thread (new Runnable(){
    @Override
    public void run() {
       try {
          now = serviceProxy.now();
       } <strong>catch(Exception ex) {
           label.setText("访问失败..");
       }</strong>
 
        label.setText(now.toString());
    }
 
  });
  t.start();
}

3. 通用异步处理过程

上述是基本原理。然而在实际的编程中,如此原始的方式很难吸引聪明程序员的兴趣。在.NET中,提供了BackgroundWorker, 相关的API有:

worker = new BackgroundWorker();
worker.DoWork += delegate(object sender, DoWorkEventArgs e) {...}
worker.RunWorkerCompleted += delegate(object sender, RunWorkerCompletedEventArgs e){...};

BackgroundWork提供最重要的两个事件是DoWork和RunWorkerCompleted事件。前者提供了异步执行耗时运算的能力;后者为结果运算成功后与UI进行交互提供了回调,并且提供了如果运算出现异常,提供相应的异常信息。这个思路同样可以借鉴到Java以及其他的方式中。

4. 上升到框架级别

BackgroundWorker的出现可以在一定程度上通用化异步编程,然而,富客户端情况下,线程资源同样珍惜。每次新创建一个类似于BackgroundWorker类似的管理器,意味着每次都会创建新的线程。一个可以参考的思路是,自行开发一个线程池,来管理异步执行的线程。在更复杂的情况下,可以实现对于很多任务进行排列的算法。这已经超出本文的范围。当能够实现到前3步的时候,第四步的提出和实现只是时间问题。

构建基于慢速网络的实时应用

Thursday, January 3rd, 2008

HTTP连接往往被考虑为慢速的。通常不会考虑在之上构建所谓实时应用。然而玩过网络游戏的人都知道,除了类似于CS之类实时性要求非常高的外,延迟在500毫秒以下是可以继续游戏的,而750毫秒以下也可以勉强凑合,如果放到网页中,需要用户之间实时交互的应用延迟在1秒左右完全可以接受。据我的检测,国内绝大部分ISP到用户桌面的延迟远远小于这个数字。技术催生需求,这种情况下我们可以考虑利用这些特性来做一些有趣的应用。那么在这种情形下,我们该如何编写具备实时性的客户端代码?

在前面讨论的双向流模拟同步HTTP连接中,我提到了Bidirectional-streams Over Synchronous HTTP (BOSH)。然而经过验证,这种通用的设计在项目设计中并不吃香。原因之一是过于复杂,无论是服务器端还是客户端。如果有成熟的服务器端还好,否则要自行实现双通道HTTP连接的控制,这其中设计大量的低级Socket以及线程操作,相当需要技巧。在此之上,如果有更复杂的伸缩性需求,整个实现将会使梦魇。实现的复杂性同样会出现在客户端。客户端需要控制消息的拆包、封包,更高级些,能够精巧的控制Http连接(基本上是XMLHTTP的连接)个数。这些也需要相当的功底。

有一种更加简单,并且容易实现的方式,经过我的实验,能够完美的应用到新的应用中,能够满足实时性的要求。基本原理就是:

当上一个请求返回之前,不要发起下一个请求。一旦上一个请求返回,立即发起下一个请求。

这是一个非常简单的设计。但非常有效。写成代码就是:


function forever_request() {
new Ajax.Request('url', {onCompleted: function(response) {
//do your stuff
forever_request();
}})
}

这个确实很简单,但这不是构建实时应用的全部。实时意味着,大多数的用户操作需要广播到所有当前可见用户的桌面上。理想情况下,界面只是一个哑终端,只是如实的表现服务器端传递过来的消息。forever_request方法有两个作用:用来发送消息以及获取服务器响应。增强后的forever_request方法就是:


function forever_request() {
new Ajax.Request('url',
parameters: pendingMessages.pickAll().serialize();
{onCompleted: function(response) {
responseMessage(response.Text);
forever_request();
}})
}

responseMessage方法负责对从服务器端收到的消息进行展示。例如,某一次的请求中,他得到了类似于userA MOVE 3,4的消息,客户端只需要将userA移动到3,4的位置。

pendingMessages负责收集所有的用户操作。为了节约网络流量,并非每一次的用户操作都将发起一次网络请求,而是将其压入到pendingMessages中。例如,在3秒内,用户说了一句话,点击了另外一个用户查看详细信息,pendingMessages中就有了类似于如下的记录:

SAY 大家好啊
GETINFO userB

在下一次请求,这些消息被发送到服务器端,用户可以得到响应。

这是基本的实现了。可以看到这种实现非常简单,任何一个具备基本javascript技巧的人都可以在半天内写出实现来。然而这仍然不是全部。构建基于慢速网络的实时应用需要我们对流量、并发数非常在意。只要用户察觉不出来,尽可能少的数据,尽可能少的与服务器交互。现在带宽资源非常昂贵——共享10M与独享10M之间的价格差别可能让你咂舌。我们可以引入ConnectionManager对forever_request方法进行统筹调用,例如,当用户不怎么活跃(Idle)的时候降低更新量,尝试将更多的message pending并发送,等等。

buffalo 2.0 最终版正式发布

Tuesday, April 24th, 2007

[Update] InfoQ中文报道:
http://www.infoq.com/cn/news/2007/04/ajax-framework-buffalo2

从2.0-alpha1发布至今,经过长达半年多的测试阶段,buffalo 2.0正式版本发布。2.0最大的关注点在于性能的提升和完全自行实现的java到javascript协议转换。根据评测,2.0版本要比前一阶段版本最高提升30%性能。这得益于新的协议实现以及为大规模AJAX调用而进行的优化。

对于一直使用alpha版本的用户,此次升级很简单,只需要将相应的jar和js进行替换即可。从1.2.x版本升级的用户,升级也很简单:

* 删除所有burlap*.jar, buffalo*.jar, 替换为buffalo.jar
* 替换新的buffalo.js
* 将继承自BuffaloService的类,对session等信息的使用替换为对RequestContext.getContext().getXXX的使用。(注意,目前有开发者报告在resin 2.1服务器上偶尔会出现丢session的现象)

2.0正式版本的发布意味着完全自我独立的协议实现,为后续特性的开发打下了基础。

继续感谢:一直使用、提出Bug以及解决办法、在论坛帮助他人的朋友们,没有你们,buffalo无法走到今天。

置身事外:能力与勇气

Friday, March 23rd, 2007

一台完美的晚会,演员为了观众而投入的表演,观众被表演而吸引,或捧腹或感动,而主持人一定是不温不火的那一个。他看起来不属于舞台:既不需要投入的表演,也不能忘我的欣赏节目,他最需要做的,就是按照既定的节奏,引导整台晚会逐步完成。观众笑,他可以笑但不能毫无顾忌;演员演得不尽人意,他通常也不能冲上去救场。他只能冷静的(无论从外表看起来他与观众、演员、嘉宾交流多么投入)按照时间规划,客观、严格的完成节目。

看起来,好像主持人是置身事外的。作为一台晚会,他没有参与其中最重要的部分。但从没有人质疑主持人的重要性,同时人们对这种分工也觉得很自然。但如果场面变一下:主持人跟观众一样感动得没了分寸,又或者觉得演员表演欠佳,他冲上去临时换着扮演,破坏演员节奏,恐怕最终结果不会那么乐观。劳动分工的精确含义在于,充分相信那个角色在当时场景下所做的努力,并尽最大努力帮助你的伙伴,不受干扰,节奏平稳的完成他的工作。

我现在依然看到,很多软件项目中,项目经理往往是一个悲情角色。他往往需要对整个项目负责,技术很强(开发背景),同时还要把握需求,控制需求,还需要跟客户(或者业务部门)交流,以及汇报。早些年参加软件工作的时候,我一直很羡慕项目经理的职位,呼风唤雨,加班(那时一直认为加班是一件很光荣的事情),随意抽调人,出差。后来有机会做了一些类似的工作,才发现这个职位几乎不能由人来胜任。项目经理要日常管理的同时,还需要是一个技术领袖,能够审核开发人员的代码。在这种情况下,项目经理一人之力撑起了一把伞,其他角色心安理得的躺在伞下,不用担心任何问题。

敏捷项目中,程序员更希望项目经理是一个看不见的角色。项目最终看起来是什么样子?由BA/QA决定。项目最终质量如何?由那帮写代码的决定。项目经理要做的,是置身事外。这里的事,就是具体的开发。冷眼旁观,但不评价,不插手,充分信任。置身事外需要勇气,即便在客户的高压下,也能够不加班的勇气;需要能力,需要辨别风险需求的能力。作为项目交付这台大的晚会,作为主持人,要做的就是保护你的程序员,是他们在任何情况下,能够持续稳定的产出可工作的、高质量代码。

勇气

作为极限编程中的原则(或者价值观)之一,在其他方面同样有效。项目经理要面对的最大的风险往往是需求的变更。作为一个正常的项目,大部分来自客户的变更都是有意义的。客户至上的理念在这里并不通用。在敏捷叠帽子游戏中,大部分的参与者都认识到时间与质量之间有一个平衡,总得拿些东西来换的。然而面对真正的客户,我们是否有勇气对一个看似合理的需求变更说不?说不很可能意味着客户不高兴,或者永久失去了一个客户。我们永远需要记住的是,客户需要100%,我们在规定时间内交付合格的哪怕50%都比交付不合格的99%要有意义得多。

能力

劝说客户是需要技巧的。与客户交流是需要技巧的。这些技巧程序员天生不具备。缜密的逻辑思维让他们不能在若干种模棱两可的商业外交语中如同编程代码一样游刃有余。这需要一个真正逻辑清楚并且沟通较好的人来完成。不要期待这样的人也能够写出比hello world级别更高的代码。通常情况下,他们只需要了解三层架构等等概念性的东西。与客户沟通,项目管理,分析项目开发进度,找到影响开发速度的地方并持续改进开发过程,适当激励才是项目经理需要做的事情。这些事情可以通过个人魅力,但也是需要技巧的。

置身事外

置身事外不是放纵。最大意味着,信任。一个团队中一定有一两个编程方面突出的人。保护这些激情并引导成为成就感,而不是架起一座大伞他们可以躺在伞下睡觉。不断强调整体团队与荣辱与共,这样项目的成功才能成为所有人的成功,而不是一个人的成功。

作为管理者或者推动者,是一个相当不能进入状态的角色,很多时候他们需要警惕破坏团队的因素并不断改进。上面的每一点都可以展开说去,每一点都是知易行难。我逐渐发现构建一个新的团队的过程远比引入新技术复杂的多。但不怕慢只怕站,持续改进,你的团队将成为最可怕、效率最高的团队。

大隐于市,小隐于野

Sunday, May 14th, 2006

上午9点的时候,一个朋友来找我;一不小心我电话关机了,于是他很郁闷的走了一个小时的路到火车站,悻悻然准备改签到中午的火车。赶紧打电话拦住他,请吃饭后便陪他去买书。

朋友家开了一个小的租书摊,要更新若干网络小说。我一听有书可以看,兴致很高:说不定我也可以买几本书消遣了,而且会以很低的价钱。他瞥了我一眼,不屑的说:那些书你不会有兴趣的。

坐上出租,朋友说了一个相当古怪的名字——我想再让我走一遍我也不知道在哪里——出租车绕过玉祥门,进入自强东路,然后我便不知道东南西北了。出租车经过几个很窄的街道后,我都快要睡着了,朋友说,到了。下车抬眼一看:很深的一个弄堂模样的店堂,里面全是卖榔头、钳子、水泵之类的五金店铺。没搞错吧?我问他。他哼了哼声,说:你知不知道什么叫“大隐隐于市,小隐隐于野”?我恍然:难道这个图书批发的地方,夹杂在这些五金店铺里面?半信半疑,我随他走到了弄堂尽头,正愁没有路走了,他往右一拐,跨上了不足一米宽的楼梯。上了二楼,往前一看,仍是一面巨大的水泵广告牌,一些铺子开着门,一些店铺却关着。我正想说一些风凉话,没想到朋友右转一下,已经和一个中年妇女搭上话了:老板,我来看一些书。

我这才注意到进了一间大约20平米的店铺:四面墙、地上全是书。大致瞟了一眼,几乎全是网络小说,而且是那种一写就十几册的那种。注意到大部分书都印刷不太精细,封面大同小异,当然也不乏一些印刷精美的。趁着朋友挑书的间隙,我到其他店面转了一圈,发现新出版的各种文学、经管、典籍类书籍居然都有,而且印刷精美。只是不知道定价如何,转了一圈回来,最后交钱才发现,那些小说,才是定价的四分之一。

回来的路上,想着“大隐于市”的这个问题,不仅觉得有点意思。记得前几年在西安的时候,一个朋友带我去吃烤肉。一路上左拐右拐,走过一条长长的黑胡同,忽然豁然开朗,只见前方一个酷似民房的房间里,一群食客正大快朵颐,门口的烤肉摊上白烟袅袅,飘来阵阵香味。要了几串一尝,果然香而不腻,瘦而不柴,一口咬下去满嘴留香,跟外面街边的不能同日而语。是否这也算“大隐于市”呢?我虽然时常念叨着去新疆吃烤肉,但是虽非“隐”,但对于我来讲“于野”,想要达到目标,轻易还是不行的。

我知道这些解释粗俗的很。今天买书的际遇明眼人一看便知,那些卖书买书并非因风雅而隐;吃烤肉的那一次其实仔细想想,从复杂的胡同中找到一个吃烤肉的地方,这种找寻的快乐本身就要比来自舌头的要多,并且在从新疆吃完回来后,更觉天下吃烤羊肉的地方非新疆昌吉莫属。百度知道的解释显然比我的这层理解要高雅许多。然而,在这个商业竞争极其激烈的今天,所谓大隐小隐,无非为了满足某些利益需求。然而,一些“隐”出现于市的时候:比如一些私人开的小玩意儿,仅仅为了满足小众需求,大隐于市。若兴趣相投,又得朋友引荐寻得门径,找寻的过程加上愉快的体验,岂非快哉。

一年~buffalo 1.2.3发布

Friday, April 28th, 2006

从buffalo 1.0发布到现在1.2.3, 一年。特征见 http://www.javaeye.com/jira/browse/BUFFALO.

最大的支持:

* 支持对同一buffalo对象的remoteCall重复调用。现在你可以这么写了:

buffalo.remoteCall...
buffalo.remoteCall...

* 核心功能(remoteCall, bindReply) 支持Safari, Opera

* 为远程调用对象添加了Buffalo.BOCLASS属性。这个特性可以使得对象可能在服务器端与客户端进行无差别传递。

关于buffalo的安全问题

Wednesday, March 22nd, 2006

感谢董董以及各位用户提出这个问题。这个问题在以前的时候虽然留意过,但没想到会给实际的运行用户带来这么大的影响;现在看来,很有必要为buffalo加上安全控制的部分了。

考虑到一些实际运营的系统已经上线,以及buffalo升级新版本的速度无法满足现在的需要,现在提出一些解决方案供参考。由于时间关系,这些方案没有经过实际的代码验证,但在方向以及结构上没有问题,供急切的同志使用。

1 问题提出

当一个应用部署到www.domain-a.com的时候,buffalo的特定servlet被暴露出来,这个servlet只接受buffalo请求(xml格式)。我们知道xmlhttp无法跨域访问,但在IE浏览器中,如果本地存在访问这个远程buffalo服务的网页并运行,你会发现没有任何警告就能访问远程。这是相当危险的操作,因为很多时候buffalo不仅完成读取数据,还完成修改数据的操作。恶意用户如果知道服务方法的命名,很容易模拟请求发送,造成损害。

这个场景同样存在于将页面置于http://localhost/
的情况。IE会认为这是本地操作,可以访问远程,只是会弹出一个警告是否允许与远程数据交互。但这种情况不会出现在www.domain-b.com上——也就是说,两个远程域之间的交互是不可以的。

然而,在Firefox上——根据我的测试,无论是本地,还是localhost, 都无法与domain-a.com交互。他的安全限制更严格。

2 如何解决问题?

现在我们知道这个问题归根结底是xmlhttp的问题。几乎所有的带有java->javascript序列化的框架都存在这个问题,dwr,
json, buffalo都有。http://getahead.ltd.uk/dwr/security
这里讨论了dwr的安全问题,文档中很巧妙地将这个问题回避过去,但是这里

http://iremia.univ-reunion.fr/intranet/wiki/Wiki.jsp?page=DWRandAcegi

又提供了对应的解决方案。这个解决方案针对buffalo也同样有用。

问题解决的基本原理是根据cookie来进行校验(或者session,都可以)。我们知道,从file://a.html发送到http://domain-a.com的请求,如果此时我们获取request.getCookie,是无法获取的,这可以作为判断的依据;session也一样;他们都只对特定域名以及当前应用下的请求保留session或者cookie状态,来自其他地方的请求不包含这些状态,因此可以进行区分。区分开之后,就可以拒绝这些请求,要么抛异常,要么转到其他页面。

实现方式上,根据buffalo service的部署方式的不同,可以分为两种:
i) 写在properties文件中。我估计没有多少系统这样做因为创建服务实例效率低下。但如果这么做了,可行的办法就是改写代码,在代码中加入访问来源地判断(request.getCookie).
这是简单的做法,如果代码不多可以这么完成。一些静态植入的AOP方案可以更优雅的解决这个问题,如AspectJ, AspectWerkz等

ii) 写在Spring配置文件中。这种方式下比较容易。步骤如下:
– 将原来的service移入到ProxyFactoryBean中
– 在ProxyFactoryBean中添加一个拦截器
– 拦截器完成了方法调用的拦截
– 其他代码不变

例如:原来有一个UserService:

现在改为

添加一个ProxyFactoryBean并设置拦截器:

myBuffaloSecurityIntereptor:

class MyBuffaloSecurityInterceptor implements MethodInterceptor
Object invoke(MethodInvocation i) throws Throwable {

HttpServletRequest request = context.getRequest();
if (request.getCookie()…) {
return i.proceed();
} else
throw SecurityException;

}

Request对象的获取方式可以加入一个filter每次请求的时候绑定到ThreadLocal中。

上述方案供参考。

3 考虑更多…

由于http是无状态的协议,xmlhttp的引入使得他远程调用的意味更加浓厚。这样的话,许多客户端可以对远程服务进行hack.
这就超出了buffalo所能控制的范围。Prototype中直接回避了这个问题而只提供AHAH的方式使得这些问题能够被传统的网站安全技术来处理。

安全方面,acegi更加全面但也带来相当的复杂性和学习成本;在这个案例中,应用的安全不是最重要的,相反访问的安全才最重要。上述的方案能够防止服务器方法被恶意执行,但无法防范被恶意重复访问。当然这可以通过常规的安全策略来控制了。

让SpringMVC也支持自定义url mapping

Friday, February 10th, 2006

[#SPR-703] Make Controller aware of the url mapping – Spring Framework

提供了一个RegexUrlMappingHandler, 可以支持类似于以下的使用:

[xml]

blogArchiveController [/xml]

在Spring controller中:

[java]
Map objects = new HashMap();
objects.put(“year”, parameters.get(0));
objects.put(“month”, parameters.get(1));
objects.put(“tag”, parameters.get(2));
return new ModelAndView(“/archive.jsp”, objects); [/java]

使用buffalo作为webwork的验证机制-实现

Wednesday, February 8th, 2006

要的人比较多,废话少说,放代码:

ValidationError.java, 主要是错误信息的一个DTO
[java]
public class ValidationError {
private String name;
private String value;

public ValidationError() {

}

public ValidationError(String name, String value) {
this.name = name;
this.value = value;
}

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}

}
[/java]

ValidationService.java, buffalo要使用的service, 代码不复杂,没有注释,原理见前一篇文章
[java]
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.opensymphony.xwork.Action;
import com.opensymphony.xwork.ActionContext;
import com.opensymphony.xwork.ActionProxy;
import com.opensymphony.xwork.DefaultActionInvocation;
import com.opensymphony.xwork.DefaultActionProxy;
import com.opensymphony.xwork.ValidationAware;
import com.opensymphony.xwork.config.entities.ActionConfig;
public class ValidationService {
public List validate(String namespace, String action, Map parameters) {
List errorList = new ArrayList();
Action requestedAction = null;
HashMap ctx = new HashMap();
ctx.put(ActionContext.PARAMETERS, parameters);
ValidatorActionProxy proxy;
try {
proxy = new ValidatorActionProxy(namespace, action, ctx);
proxy.execute();
requestedAction = proxy.getAction();
} catch (Exception e) {
e.printStackTrace();

return null;
}

if (requestedAction instanceof ValidationAware) {
ValidationAware va = (ValidationAware) requestedAction;
Map fe = va.getFieldErrors();
for (Iterator iterator = fe.entrySet().iterator(); iterator
.hasNext();) {
Map.Entry entry = (Map.Entry) iterator.next();
String name = (String) entry.getKey();
List errors = (List) entry.getValue();
for (Iterator iterator1 = errors.iterator(); iterator1
.hasNext();) {
String error = (String) iterator1.next();
errorList.add(new ValidationError(name, error));
}
}
}

return errorList;

}

public static class ValidatorActionInvocation extends
DefaultActionInvocation {
protected ValidatorActionInvocation(ActionProxy proxy, Map extraContext)
throws Exception {
super(proxy, extraContext, true);
}

protected String invokeAction(Action action, ActionConfig actionConfig)
throws Exception {
return Action.NONE; // don’t actually execute the action
}
}

public static class ValidatorActionProxy extends DefaultActionProxy {
protected ValidatorActionProxy(String namespace,
String actionName,
Map extraContext) throws Exception {
super(namespace, actionName, extraContext, false);
}

protected void prepare() throws Exception {
invocation = new ValidatorActionInvocation(this, extraContext);
}
}
}
[/java]

buffalo-service.properties
[txt]
validationService=your.package.ValidationService
[/txt]

更改webwork模板中的simple/form.vm:
[html]

[/html]

最后,加入一个validate.js。用的是buffalo 1.1版本(写的时候的版本)。
[javascript]
var currentForm = null;

var getContextPath = function() {
var ctxPath = "";
$A(document.getElementsByTagName("script")).findAll( function(s) {
return (s.src && s.src.match(/buffalo\.js(\?.*)?$/))
}).each( function(s) {
var path = s.src.replace(/script\/buffalo\.js(\?.*)?$/,'');
ctxPath = path;
});

return ctxPath;
}

function validate(theForm) {
var buffaloURL = getContextPath()+"BUFFALO";
var buffalo = new Buffalo(buffaloURL);
buffalo.events["onLoading"] = function() {}
var parameters = {};
for (var i = 0; i < theForm.elements.length; i++) {
var e = theForm.elements[i];
parameters[e.name] = e.value;
}
currentForm = theForm;
var actionName = theForm.name;
if (actionName.indexOf("do") != 0) {
actionName = "do"+actionName.charAt(0).toUpperCase() + actionName.substring(1);
}
Form.disable(theForm.id);
buffalo.remoteCall("validationService.validate",
[theForm.getAttribute("namespace"), actionName, parameters], callback) ;
return false;
}

function callback(reply) {
Form.enable(currentForm.id);
var insideTable = currentForm.getElementsByTagName("TABLE")[0];
clearErrorRows(insideTable);
clearErrorLabels(currentForm);
var errors = reply.getResult();
if (errors == null) {alert(null); return; }
if (errors.length > 0) {
for (var i = 0; i < errors.length; i++) {
var error = errors[i];
var element = currentForm.elements[error.name];
addError(element, error.value);
}

} else {
currentForm.submit();
Form.disable(currentForm.id);
}
}

function clearErrorRows(table) {
// clear out any rows with an "errorFor" attribute
var rows = table.rows;
var rowsToDelete = new Array();
for(var i = 0; i < rows.length; i++) {
var r = rows[i];
if (r.getAttribute("errorFor")) {
rowsToDelete.push(r);
}
}

// now delete the rows
for (var i = 0; i < rowsToDelete.length; i++) {
var r = rowsToDelete[i];
table.deleteRow(r.rowIndex);
}
}

function clearErrorLabels(form) {
// set all labels back to the normal class
var elements = form.elements;
for (var i = 0; i < elements.length; i++) {
var e = elements[i];
var cells = e.parentNode.parentNode.cells;
if (cells && cells.length >= 2) {
var label = cells[0].getElementsByTagName(“label”)[0];
if (label) {
label.setAttribute(“class”, “label”);
}
}
}

}
function addError(e, errorText) {
try {
var row = e.parentNode.parentNode;
var table = row.parentNode;
var error = document.createTextNode(errorText);
var tr = document.createElement(“tr”);
var td = document.createElement(“td”);
var span = document.createElement(“span”);
td.align = “center”;
td.valign = “top”;
td.colSpan = 2;
span.className=”errorMessage”;
span.appendChild(error);
td.appendChild(span);
tr.appendChild(td);
tr.setAttribute(“errorFor”, e.id);;
table.insertBefore(tr, row);
var label = row.cells[0].getElementsByTagName(“label”)[0];
label.setAttribute(“class”, “errorLabel”);
} catch (e) {
alert(e);
}
}
[/javascript]

调用流程:实际上表单提交前buffalo将整个表单送到了后台并利用ValidationService执行了一次action,看有没有错误,如果没有错误才真正提交,否则通过js对页面进行相应的提示。

注意事项:1, 一些拦截器需要ActionContext中含有servlet等信息,由于ValidationService不带有这些信息,因此这些拦截器可能会在校验的时候报错;2, webwork的某些校验器的错误提示信息有些并不返回,因此不是所有的校验错误都能返回,但绝大多数都能满足了;3, 不要期待你写的actionError信息也能通过这种方式返回——当然你可以自己写,但在这个实现中,只能返回校验错误信息.