精细之处一:“利用Token解决重复提交”背后的前提
我们知道,可以利用同步令牌(Token)机制来解决Web应用中重复提交的问题,Struts也给出了一个参考实现。服务器端在处理到达的请求之前,会将请求中包含的令牌值与保存在当前用户会话中的令牌值进行比较,看是否匹配。在处理完该请求后,且在答复发送给客户端之前,将会产生一个新的令牌,该令牌除传给客户端以外,也会将用户会话中保存的旧的令牌进行替换。这样如果用户回退到刚才的提交页面并再次提交的话,客户端传过来的令牌就和服务器端的令牌不一致,从而有效地防止了重复提交的发生。对应于这段描述,你可能会在你的Action子类中有这么一段代码:
if (isTokenValid(request, true)) {
// your code here
return mapping.findForward("success");
} else {
saveToken(request);
return mapping.findForward("submitagain");
}
|
其中isTokenValid()和saveToken()都是org.apache.struts.action.Action类中的方法,而具体的Token处理逻辑都在org.apache.struts.util.TokenProcessor类中。Struts中是根据用户会话ID和当前系统时间来生成一个唯一(对于每个会话)令牌的,具体实现可以参考TokenProcessor类中的generateToken()方法。
不知道大家有没有注意到这样一个问题,因为Struts是将Token保存在Session的一个属性中,也就是说对于每个会话服务器端只保存而且只能保存一个最新Token值。对于这一点,我的同事就提出了疑问:那如果我在同一个会话中打开两个页面,那么后提交的那个页面肯定不能提交成功了。他还给出了一个实际的例子:比如现在需要把两个客户A和B的地址都改为某个值,那用户就可能同时打开两个页面,修改A,修改B,提交A,提交B,按照Struts中的处理逻辑,B的修改提交就肯定不能成功,但是这个提交操作对于用户来说并不存在操作不正确的地方。
在这里,可能有人要问:怎么可能在同一个会话中打开两个页面呢?重新打开一个IE浏览器不是重新开始了一个会话吗?不错,这种情况下是两个会话,不存在任何问题。但是,你还可以通过菜单“文件”-“新建”-“窗口”(或者快捷键Ctrl+N)来复制当前窗口,这个时候你会发现该页面与原有页面同处在一个会话当中。其实,能够发现这个问题得归功于我的那位同事对IE习惯性的操作方法。
这下我的那位同事不满意啦,他于是开始动手修改Struts中的实现方式,让每个页面(至少某类页面)在服务器端都保存有一个唯一的Token值。这样,前面所讲的客户A,B同时修改的限制就不存在了。但是不久,我的那位同事就开始意识到他正在走向一条危险的道路。首先,如果每个页面都在服务器端保存一个Token值,则服务器端保存的数据量将越来越大。而且,如果考虑这种同一个会话中打开多个页面的情况的话,就好像打开了潘多拉魔盒,将会给自己带来无穷无尽的麻烦。比如,首先打开页面P1,然后利用Ctrl+N得到页面P2,P1提交,P2提交,目前为止一切正常。但是如果此时,在P1,P2中点击“后退”按钮,然后再提交P1, P2呢,情况会是怎样?如果在P2中提交完后执行其它操作,而在P1中回退后提交,情况又是怎么样呢?如果有P1,P2,P3,那情况又是如何呢?太复杂啦!我想你也会和我们有同感,你需要考虑许多种可能的组合,而且有的时候结果并不是你想象中的那样简单。
此路不通,还得回来看看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(mapping.getInput()));
|
或
return (mapping.findForward("success"));
|
其实返回的就是一个ActionForward对象。在Action中我们根据程序执行的不同情况,决定接下来的页面走向(比如返回到输入页面或者转到下一个页面),并将这些信息保存在ActionForward对象中。而接下来控制器就可以直接利用该ActionForward对象来进行页面的流转。下面是org.apache.struts.action.RequestProcessor类的processForwardConfig()方法的摘录,该方法调用发生在Action实例调用后。
protected void processForwardConfig(HttpServletRequest
request,
HttpServletResponse
response,
ForwardConfig forward)
throws IOException, ServletException {
…
String forwardPath = forward.getPath();
String uri = null;
// paths not starting with /
should be passed through without any processing
// (ie. they're absolute)
if (forwardPath.startsWith("/")) {
uri = RequestUtils.forwardURL(request, forward);
// get module relative uri
} else {
uri = forwardPath;
}
if (forward.getRedirect()) {
// only prepend context path for relative uri
if (uri.startsWith("/")) {
uri = request.getContextPath() + uri;
}
response.sendRedirect(response.encodeRedirectURL(uri));
}
else {
doForward(uri, request, response);
}
}
|
注意: ForwardConfig是ActionForward的父类
该方法首先调用ForwardConfig的getPath()方法获得下一步流转的路径,在某些条件下还需要进行一些拼装得到正确的URI,最后根据该URI进行页面跳转。可见在processForwardConfig()方法中只是对ActionForward进行了一些“技术上”的处理,没有任何和业务相关的内容,这样就将控制器(ActionServlet)和Action完全分开来,两者互不影响,达到了功能模块之间松散耦合的目的。
模块间(系统间)松散耦合一直是OO设计所追求的,但是具体如何去实现这样一种松散耦合却不是那么容易做到的。Struts中的设计给了我们一些启示:模块间相互关联影响因素的传递可以用对象的形式来包装起来。其实,个人觉得Struts中的做法还可以稍微有一点点改进,就是在ActionForward中提供一个getURI()方法来给出最终的URI岂不是更好?
参考:
1、Beyond MVC: A New Look at the Servlet Infrastructure
2、Allen Holub的Build user interfaces for object-oriented systems系列文章,可以从这篇文章中学到很多面向对象设计方面的知识,虽然作者并不认为MVC是一种面向对象的方法,但是我们这些MVC的实践者仍然可以从中学到面向对象的知识。
3、Struts 1.1的介绍性文章:深入Struts 1.1
4、Apache Struts Website
5、关于重复提交问题的讨论及其解决方案,可以参考《Core J2EE Patterns》一书(中文版《J2EE核心模式》)。
Deepak Alur,John Crupi,Dan Malks: Core J2EE Patterns-Best Practices and Design Strategies
分享到:
相关推荐
表单验证部分则分别从手动校验和框架提供的数据校验机制两个角度来讲解如何实现用户输入的校验。 国际化实现部分指导如何为Web应用添加国际化支持,包含页面、Action和验证信息的国际化实现。拦截器浅析部分则介绍...
在深入探讨Struts1.x与Struts2.x的区别之前,我们先来了解一下这两个框架的基本背景。Apache Struts是用于构建企业级Web应用程序的开源框架,它提供了MVC(模型-视图-控制器)架构模式的实现,帮助开发者更好地组织...
下面将详细介绍这两个技术,并对比它们的使用方法。 **Struts上传** Struts是Apache软件基金会下的一个开源项目,它是一个基于MVC(Model-View-Controller)设计模式的Java Web框架。在Struts 1.x版本中,处理文件...
在Java世界中,Apache Commons FileUpload和COS(Comprehensive O'Reilly Servlets)是两个常用的文件上传库。Struts2默认使用Apache Commons FileUpload,需要在项目中添加对应的jar包:commons-fileupload-1.2.jar...
这两个库提供了Servlet和JSP的相关API,是web应用程序运行所必需的。 实现Struts2文件上传,你需要在Struts配置文件(通常为struts.xml)中配置相关的拦截器,例如`params` 和 `fileUpload` 拦截器。`params` 拦截...
Struts 是一个经典的Java Web开发框架,用于构建MVC(模型-视图-控制器)架构的应用程序。在Struts 1.x版本中,配置文件主要包括`web.xml`和`struts-config....正确配置这两个文件是确保Struts应用程序正常运行的关键。
Struts2 框架是Java Web开发中广泛使用的MVC框架之一,它提供了一种组织应用程序的方式,便于实现业务逻辑和视图的分离。在Struts2中,配置文件起着至关重要的作用,其中`struts.xml`用于管理Action映射和Result定义...
Struts2 Action 处理中文乱码问题是一个常见的挑战,尤其是在进行Web开发时,由于编码格式不统一,可能导致输入或显示的中文字符出现乱码。以下是对两种解决方法的详细解释: 方法一:通过配置Struts2的i18n编码 ...
主要的配置文件有两个:`web.xml`和`struts-config.xml`。 `web.xml`是Web应用的部署描述符,它包含了所有Web应用的基本配置信息,例如Servlet、过滤器、监听器等。在Struts应用中,`web.xml`主要负责配置Action...
Struts1.x是一个非常经典的Java Web开发框架,它在处理用户请求时,通过Action类来转发请求并返回相应的视图。然而,在实际开发过程中,难免会遇到各种异常情况,这时候就需要一套有效的异常处理机制来捕获和处理...
这里,`attrN`和`attrV`是两个相互关联的列表,通过索引关联,实现了同步遍历。 #### 结论 `<s:iterator>`标签是Struts2框架中处理集合数据的强大工具,它不仅简化了数据的遍历和展示,还提供了丰富的状态管理和...
主要包含两个配置文件:struts-config.xml和struts.properties。前者定义了Action类、ActionForm对象、数据源等核心组件,它告诉Struts如何处理用户的请求。例如,你将在这里定义每个Action的映射路径,指定...
首先,我们定义一个名为`Authority`的注解,该注解包含两个属性:`action`和`privilege`。其中,`action`表示需要进行权限控制的操作名称,而`privilege`则表示执行该操作所需的权限。 ```java import java.lang....
Struts2是一个流行的Java Web应用程序框架,它极大地简化了...通过这些配置,开发者能够精细控制Struts2的行为,包括Action的执行、结果的渲染、拦截器的使用以及错误处理等,从而实现符合项目需求的定制化解决方案。
它的配置主要涉及两个XML文件:`web.xml`和`struts-config.xml`。 **web.xml配置详解:** `web.xml`是Web应用的部署描述符,它定义了Web容器如何初始化和管理Web应用。在这个文件中,你需要配置Struts的Action...
Struts2是一个强大的Java web应用程序框架,用于构建MVC(模型-视图-控制器)架构的应用。它在Java EE世界中被广泛使用,因为它简化了Web应用开发,并提供了丰富的功能来处理请求、响应以及业务逻辑。本文将深入探讨...
Struts是一个非常流行的Java Web应用程序框架,它提供了一种组织和管理MVC(Model-View-Controller)架构的方式。在开发过程中,异常处理是至关重要的,因为它有助于确保程序的健壮性和用户友好的错误提示。Struts...
例如,以下配置创建了一个名为"myStack"的拦截器栈,包含"logging"和"validation"两个拦截器: ```xml <!-- 可以在此添加更多拦截器 --> <!-- 或者可以只引用单个拦截器 --> <!--...