前言
防止表单重复提交在web开发中是一个经常遇到的问题,一般来避免重复提交有两种方式:客户端JavaScript代码实现和服务端代码实现。这里主要介绍服务端的实现方式。在服务端实现表单重复提交的基本原理是:通过创建一个Session对象,并产生一个令牌值,将这个令牌值作为隐藏域随表单一起发送给客户端,同时在Session中保存令牌值。在用户提交表单的时候判断提交参数的令牌值与Session中的是否相等,如果相等则清除,不再使用这个令牌值,,然后执行后续的处理;如果两者不相等,表示已经提交过表单,服务端产生一个新的令牌值并保存到Session中。当用户下次访问的的时候,将新产生的领牌值发送到客户端。
Struts2的实现方式
在Struts2中通过使用拦截器来实现的,机制与前言中采用令牌的方式是一样的。可以通过两种方式实现避免重复表单(实际上就是两个不同的拦截器):token拦截器和tokenSession拦截器。由于在struts-default.xml的默认拦截器栈中并没有将这两个拦截器作为默认实现,所以需要在action中手动添加这两个拦截器。这两种方式的区别在于:使用token拦截器重复提交表单的时候,浏览器会跳转到一个错误页面,而使用更tokenSession拦截器重复提交表单的话是不会跳转的,仍然在成功之后页面。需要注意的是,使用者两个拦截器重复提交表单的时候,都只会向服务器提交一次请求,所以这种方式可以有效降低服务器的负担。
具体的例子
在使用以上拦截器进行测试的时候,需要如下步骤:
步骤一:编写login.jsp、success.jsp和error.jsp三个页面
login.jsp
...
<s:form action="tokenWait" namespace="/" method="post">
<s:textfield label="用户名" name="user.username"></s:textfield>
<s:password label="密码" name="user.password"></s:password>
<!-- 这个标签不能少 -->
<%-- <s:token></s:token> --%>
<s:submit value="登录"></s:submit>
</s:form>
...
success.jsp
...
<s:property value="user.username"/>,<%=new Date() %>
...
error.jsp
<html>
<body>
<!-- 登录失败,请重新登录
<a href="login.jsp">返回</a> -->
您已经提交过表单了!
</body>
</html>
步骤二:编写action
package action;
import java.util.ArrayList;
import java.util.List;
import bean.User;
import com.opensymphony.xwork2.ActionSupport;
public class TokenAction extends ActionSupport {
private static final long serialVersionUID = 1L;
private User user;
@Override
public String execute() throws Exception {
Thread.sleep(3000);
List<User> users = new ArrayList<User>();
users.add(user);
for (User user : users) {
System.out.println(user);
}
return SUCCESS;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
步骤三:配置struts.xml
<!-- 避免表单重复提交 -->
<action name="token" class="action.TokenAction">
<!-- 配置Token拦截器 -->
<interceptor-ref name="defaultStack"></interceptor-ref>
<interceptor-ref name="token"></interceptor-ref>
<!-- 如果重复提交,则跳转到error.jsp -->
<result name="invalid.token">/error.jsp</result>
<result>/success.jsp</result>
</action>
<action name="tokenSession" class="action.TokenAction">
<!-- 配置TokenSession拦截器 -->
<interceptor-ref name="defaultStack"></interceptor-ref>
<interceptor-ref name="tokenSession"></interceptor-ref>
<!-- 如果重复提交,则跳转到error.jsp -->
<result name="invalid.token">/error.jsp</result>
<result>/success.jsp</result>
</action>
这里需要提出的是,action中name属性为invalid.token
的result是不可少的。
步骤四:发布测试
经过测试,发现使用token拦截器在重复提交表单的时候会转到error.jsp,而使用tokenSession拦截器在重复提交表单的时候不会转到error.jsp。
显示等待页面
有时候在action需要处理较长时间的时候,一般是5到10分钟,在这种情况下向用户显示一个等待页面可能会比较友好一些。在Struts2中通过使用execAndWait拦截器就可以非常轻松实现这点。
execAndWait的工作机制:
execAndWait拦截器能够让一个A执行时间超过5分钟的Action在后台运行,并向用户显示一个等待页面。之所以是5分钟是因为这样防止HTTP请求超时。当一个请求到来的时候,execAndWait拦截器会创建一个线程来执行Session,然后返回一个等待页面,这样用户就知道请求在处理中。等待页面包含了自动刷新功能,在超时之前,浏览器会向初始请求的action再次发起请求,以便知道后台action是否已经执行完毕。如果action仍然没有执行完毕,则继续显示等待页面,如果action已经执行完毕,则等待页面将发生跳转,向用户处理结束之后的页面。
execAndWait拦截器有以下几个参数:
- threadPriority:执行线程的优先级
- delay:指定在显示等待页面前初始的延迟加载时间,单位是毫秒
- delaySleepInternal:指定检查后台线程是否执行完毕的时间间隔,必须和delay参数一起使用,单位是毫秒,默认是100毫秒。表示每100毫秒进行一次检查
使用execAndWait拦截器显示等待页面,首先需要编写一个等待页面:
...
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">
<title>等待页面</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="refresh" content="3;url=tokenWait.action">
</head>
<body>
您的请求正在处理,请稍等。
<span id="time" style="font-size:30px;color:red;font-face:隶书"></span>秒后页面将自动跳转
<script type="text/javascript">
var start = 3;
var step = -1;
function timer () {
document.getElementById("time").innerHTML = start;
if(start > 0){
start = start + step;
}
setTimeout("timer()",1000);
}
window.onload = timer;
</script>
</body>
</html>
在head 标签中需要添加自动刷新meta标签,不然是不会出发自动检查的。在这个等待页面中,表示3秒后就会跳转到成功页面。
之后是添加execAndWait拦截器的配置:
<!-- 显示自动等待页面 -->
<action name="tokenWait" class="action.TokenAction">
<result name="wait">/wait.jsp</result>
<result>/success.jsp</result>
<interceptor-ref name="defaultStack">
<!-- 把default方法排序在外,表示不拦截!default.action -->
<param name="excludeMethods">default</param>
</interceptor-ref>
<interceptor-ref name="execAndWait">
<!-- 把default方法排序在外,表示不拦截!default.action -->
<param name="excludeMethods">default</param>
<!-- 等待延迟时间 -->
<param name="delay">1000</param>
</interceptor-ref>
</action>
注意到TokenAction类中,使用Tread.sleep(3000),表示通过让线程休眠的方式延长action的处理时间,还有一点要注意的是struts.xml中execAndWait拦截器的delay参数的值需要小于Thread.sleep(time)的时间。这样就能保证在action处理结束之前完成显示等待页面,不然很可能会直接success.jsp页面了。