最近在网上看到一篇N Alex Rupp写的Beyond MVC: A New Look at the Servlet Infrastructure文章意思大致是说MVC被Struts等框架错误地应用到了Servlet架构中我想只有对Struts有足够的了解再加上在MVC方面有足够深的功力才敢发此言论不是经常听人说最熟悉自己的人是你的敌人本人功力尚浅没有引领风潮的能力而且生活还得继续只能先来熟悉熟悉Struts
申明 强烈建议在阅读本文之前先阅读一下N Alex Rupp老兄的文章如果你赞同他的看法可能你会觉得研究Struts就没什么意义了
说明本文所讲的Struts知识基于Struts 版本除非特别说明本文中的Struts都特指Struts 这个版本
精细之处一利用Token解决重复提交背后的前提
我们知道可以利用同步令牌(Token)机制来解决Web应用中重复提交的问题Struts也给出了一个参考实现服务器端在处理到达的请求之前会将请求中包含的令牌值与保存在当前用户会话中的令牌值进行比较看是否匹配在处理完该请求后且在答复发送给客户端之前将会产生一个新的令牌该令牌除传给客户端以外也会将用户会话中保存的旧的令牌进行替换这样如果用户回退到刚才的提交页面并再次提交的话客户端传过来的令牌就和服务器端的令牌不一致从而有效地防止了重复提交的发生对应于这段描述你可能会在你的Action子类中有这么一段代码
if (isTokenValid(request true)) {// your code herereturn mappingfindForward(success);} else {saveToken(request);return mappingfindForward(submitagain);}
其中isTokenValid()和saveToken()都是orgapachestrutsactionAction类中的方法而具体的Token处理逻辑都在orgapachestrutsutilTokenProcessor类中Struts中是根据用户会话ID和当前系统时间来生成一个唯一(对于每个会话)令牌的具体实现可以参考TokenProcessor类中的generateToken()方法
不知道大家有没有注意到这样一个问题因为Struts是将Token保存在Session的一个属性中也就是说对于每个会话服务器端只保存而且只能保存一个最新Token值对于这一点我的同事就提出了疑问那如果我在同一个会话中打开两个页面那么后提交的那个页面肯定不能提交成功了他还给出了一个实际的例子比如现在需要把两个客户A和B的地址都改为某个值那用户就可能同时打开两个页面修改A修改B提交A提交B按照Struts中的处理逻辑B的修改提交就肯定不能成功但是这个提交操作对于用户来说并不存在操作不正确的地方
在这里可能有人要问怎么可能在同一个会话中打开两个页面呢?重新打开一个IE浏览器不是重新开始了一个会话吗?不错这种情况下是两个会话不存在任何问题但是你还可以通过菜单文件-新建-窗口(或者快捷键Ctrl+N)来复制当前窗口这个时候你会发现该页面与原有页面同处在一个会话当中其实能够发现这个问题得归功于我的那位同事对IE习惯性的操作方法
这下我的那位同事不满意啦他于是开始动手修改Struts中的实现方式让每个页面(至少某类页面)在服务器端都保存有一个唯一的Token值这样前面所讲的客户AB同时修改的限制就不存在了但是不久我的那位同事就开始意识到他正在走向一条危险的道路首先如果每个页面都在服务器端保存一个Token值则服务器端保存的数据量将越来越大而且如果考虑这种同一个会话中打开多个页面的情况的话就好像打开了潘多拉魔盒将会给自己带来无穷无尽的麻烦比如首先打开页面P然后利用Ctrl+N得到页面PP提交P提交目前为止一切正常但是如果此时在PP中点击后退按钮然后再提交P P呢情况会是怎样?如果在P中提交完后执行其它操作而在P中回退后提交情况又是怎么样呢?如果有PPP那情况又是如何呢?太复杂啦!我想你也会和我们有同感你需要考虑许多种可能的组合而且有的时候结果并不是你想象中的那样简单
此路不通还得回来看看Struts其实经过以上一番折腾我们可以发现在Struts中的Token机制背后隐藏着这样一个前提不允许你(客户端)在同一会话中打开多个页面注意是同一会话如果打开两个IE浏览器那已经是两个会话啦不受该限制其实这个看似不合理的规定却自有其道理一是它极大地简化了Token的实现二个这种限定也符合大部分人的使用习惯
精细之处二页面流转控制中的职责分配
我们知道Struts的执行过程大致如下首先控制器接收到客户端请求将这些请求映射至相应的Action并调用Action的execute方法这中间可能还涉及到ActionForm的创建和填充Action的execute方法执行完以后返回一个ActionForward对象控制器根据该ActionForward对象将请求转发至下一个Action或JSP最后产生视图响应客户在大的层面上Struts是采用了MVC这种架构没什么特别之处但从一些小的地方我们还是可以看出Craig R McClanahan老兄的一些考虑我们看到Action与控制器之间传递的是ActionForward对象由于Action的execute方法要求返回一个ActionForward对象所以你会经常在Action子类中看到如下语句
return (new ActionForward(mappinggetInput()));
或
return (mappingfindForward(success));
其实返回的就是一个ActionForward对象在Action中我们根据程序执行的不同情况决定接下来的页面走向(比如返回到输入页面或者转到下一个页面)并将这些信息保存在ActionForward对象中而接下来控制器就可以直接利用该ActionForward对象来进行页面的流转下面是orgapachestrutsactionRequestProcessor类的processForwardConfig()方法的摘录该方法调用发生在Action实例调用后
protected void processForwardConfig(HttpServletRequest request HttpServletResponse response ForwardConfig forward) throws IOException ServletException { … String forwardPath = forwardgetPath(); String uri = null; // paths not starting with / should be passed through without any processing // (ie theyre absolute) if (forwardPathstartsWith(/)) { uri = RequestUtilsforwardURL(request forward); // get module relative uri } else { uri = forwardPath; } if (forwardgetRedirect()) { // only prepend context path for relative uri if (uristartsWith(/)) { uri = requestgetContextPath() + uri; } responsesendRedirect(responseencodeRedirectURL(uri)); } else { doForward(uri request response); } }
注意 ForwardConfig是ActionForward的父类
该方法首先调用ForwardConfig的getPath()方法获得下一步流转的路径在某些条件下还需要进行一些拼装得到正确的URI最后根据该URI进行页面跳转可见在processForwardConfig()方法中只是对ActionForward进行了一些技术上的处理没有任何和业务相关的内容这样就将控制器(ActionServlet)和Action完全分开来两者互不影响达到了功能模块之间松散耦合的目的
模块间(系统间)松散耦合一直是OO设计所追求的但是具体如何去实现这样一种松散耦合却不是那么容易做到的Struts中的设计给了我们一些启示模块间相互关联影响因素的传递可以用对象的形式来包装起来其实个人觉得Struts中的做法还可以稍微有一点点改进就是在ActionForward中提供一个getURI()方法来给出最终的URI岂不是更好?
参考
Beyond MVC: A New Look at the Servlet Infrastructure
Allen Holub的Build user interfaces for objectoriented systems系列文章可以从这篇文章中学到很多面向对象设计方面的知识虽然作者并不认为MVC是一种面向对象的方法但是我们这些MVC的实践者仍然可以从中学到面向对象的知识
Struts 的介绍性文章深入Struts
Apache Struts Website
关于重复提交问题的讨论及其解决方案可以参考《Core JEE Patterns》一书(中文版《JEE核心模式》)
Deepak AlurJohn CrupiDan Malks: Core JEE Patterns-Best Practices and Design Strategies