ブラウザ読込中にボタンクリックで意図しない二重実行

ヘッダー広告
スポンサードリンク

今回の記事はブラウザで読み込み中に再度実行ボタンをクリックすることで発生する意図しない二重実行の問題について紹介する記事になります。
(HTTPリクエスト後、レスポンスが返却前に再度リクエストを行う)

Webサイトを閲覧していると、ボタンをクリックしたはずなのに次の画面が表示されるのにすごく時間がかかるということがよくあると思います。
Webサーバ側の処理に時間がかかっていたり、ネットワークが遅かったりするせいで、処理に時間がかかって待ちきれず再度ボタンをクリックしてしまうということは、Webアプリケーション開発者でない場合にはよくやってしまうことだと思います。 今回は処理中に再度ボタンをクリックすることによって発生する二重実行について紹介する記事となります。

サンプルの画面や処理は、以下の記事のソースコードとほぼ同じものを使っており、以下の記事の問題と被る部分もありますので、参考にしていただければと思います。


ブラウザバックによる二重実行の注意

HTTPリクエスト実行中に再度実行で意図しない二重実行

二重実行再現

わかりやすくするために状況を再現します。

振込入力画面で、振込先の名前・振込金額を入力してPOST送信ボタンをクリックします。

1.振込入力画面

この時、サーバ側の処理にスリープ処理を追加しているため、処理に時間がかかり、入力画面から進まないという状況が生まれます。
そこで再度POST送信ボタンをクリックします。

2.振込入力画面(再実行)

その後しばらくすると、振込完了画面へ遷移して、transactionテーブルに入力した内容が反映されます。
3.振込完了画面

4.振込完了後DB状態

このDBの結果は佐藤太郎さんに10,000円を2回振り込んだというものが反映されています。
しかしこのブラウザの動きはWebサイトの閲覧者側からみると、振込を1回だけしか行われていないというように見えます。
そのためWebアプリケーションの管理者側と利用者側で認識の相違が発生してしまい、問題となってしまう動作となります。

これが私がお伝えしたかったHTTPリクエスト実行中に、再度実行ボタンをクリックすることで発生する意図しない二重実行の挙動です。

ブラウザは処理を実行していても、次のボタンがクリックされたらそのリクエストも処理しようとしてくれます。
そのため、処理中に再度実行されてしまうと二回目も同じく処理が実行されて、サーバ側で対処していない場合には、同じトランザクションが二重実行となってしまうのです。

二重実行の対策

二重実行の対策方法はいくつかありますが、一番効果的なのはリクエスト実行時に、DBチェックを入れて二重実行を回避するというものになります。
これはブラウザバックによる二重実行の注意
の記事と同じ対策方法ですので、今回はブラウザ側でそもそも二回ボタンがクリックされないように対策する方法をご紹介したいと思います。

ブラウザ側で二回クリックされないようにするにはjavascriptを使用します。
まず、post送信ボタンをtype=”submit”で実現していましたが、type=”button”とonclickにて代用します。

●当初
<input type=”submit” value=”POST送信” />
●変更後
<input type=”button” id=”btt” onclick=”FirstCheck()” value=”POST送信” />

そして、ボタンをクリックした後に呼びだすFirstCheckファンクションに、二度押しチェックを入れて一回目のクリックで あった場合にはsubmitを行うような指定をします。

<script>
  var isFristTime = true;

  function FirstCheck() {
    if(isFristTime) {
      isFristTime = false;
      document.forms[“PostForm”].submit();
    } else {
      return;
    }
  }
</script>

これは画面を開いた時に、isFristTime 変数にTrueをセットし、クリックされたらfalseに変更され、以降のクリックは無視されるというロジックになります。
これによって、予期しない二度押しが制御され、二重実行を回避することが出来ます。

ちなみにこれは補足ですが、IEやChormeは次の画面でブラウザバックした時には入力したテキストやjavascriptの変数状態は最初の状態に戻るのですが、Firefoxは次の画面に遷移する直前の状態が保存されているため、入力したテキストやjavascriptの変数状態はそのままで、再度クリック出来ないという状態のままになりますので、ご注意ください。

参考までにhtmlを貼り付けておきます。


<HTML>

<HEAD>

<TITLE>振込</TITLE>

<style type="text/css">
    .block1 {
	display: inline-block;
	width: 5em
    }
</style>

</HEAD>

<BODY>

<script>
  var isFristTime = true;

  function FirstCheck() {
    if(isFristTime) {
      isFristTime = false;
      document.forms["PostForm"].submit();
    } else {
      return;
    }
  }
</script>

<h1>振込</h1>

<form Name="PostForm" Action="TransferServlet" method="post">
    <span class="block1">名前:</span><input type="text" name ="userName" /><br>
    <span class="block1">振込金額:</span><input type="text" name ="Amount" /><br>
    <input type="button" id="btt" onclick="FirstCheck()" value="POST送信" />
</form>

</BODY>

</HTML>

今回の記事は以上となりますが、最後に今回のソースコードも載せておきます。

今回のコード(参考)

jsp

振込入力画面:Transfer.jsp
<%@ page language="java" contentType="text/html;charset=Windows-31J" %>
<%@ page pageEncoding="Windows-31J" %>

<HTML>

<HEAD>

<TITLE>振込</TITLE>

<style type="text/css">
    .block1 {
	display: inline-block;
	width: 5em
    }
</style>

</HEAD>

<BODY>

<script>
  var isFristTime = true;

  function FirstCheck() {
    if(isFristTime) {
      isFristTime = false;
      document.forms["PostForm"].submit();
    } else {
      return;
    }
  }
</script>

<%

    String ErrMsg = (String)request.getAttribute("ErrMsg");

    if (ErrMsg != null) {
%>
<strong><font color=#ff0000><%= ErrMsg %></font></strong>
<%
    }
%>

<h1>振込</h1>

<form Name="PostForm" Action="TransferServlet" method="post">
    <span class="block1">名前:</span><input type="text" name ="userName" /><br>
    <span class="block1">振込金額:</span><input type="text" name ="Amount" /><br>
    <input type="button" id="btt" onclick="FirstCheck()" value="POST送信" />
</form>

</BODY>

</HTML>

振込完了画面:Transfer_Finish.jsp
<%@ page language="java" contentType="text/html;charset=Windows-31J" %>
<%@ page pageEncoding="Windows-31J" %>

<HTML>

<HEAD>

<TITLE>振込完了</TITLE>

<style type="text/css">
    .block1 {
	display: inline-block;
	width: 5em
    }
</style>

<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">

</HEAD>

<BODY>

<%
  String userName = request.getParameter("userName");
  String Amount = request.getParameter("Amount");
%>


<h1>振込完了</h1>

以下の内容で振込しました。<br>
<span class="block1">名前:</span><%= userName %><br>
<span class="block1">振込金額:</span><%= Amount %><br>

</BODY>

</HTML>

javaサーブレット

メインのコード:TransferServlet.java
package webapp;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import dao.TransactionDao;

public class TransferServlet extends HttpServlet {
  @Override

    protected void doPost( HttpServletRequest req, HttpServletResponse res ) throws ServletException, IOException{

	  	int SqlResult;
	  	int TransNo;

	  	final int WaitTime = 5000;

	  	try{
	  		Thread.sleep(WaitTime);
	  	}catch(InterruptedException e){
	  	}

        res.setContentType("text/html; charset=Windows-31J");

        Calendar date = Calendar.getInstance();

        SimpleDateFormat sdf = new SimpleDateFormat("MMddHHmmss");
        TransNo = Integer.parseInt(sdf.format(date.getTime()));

	  	TransactionDao Dao1 = new TransactionDao();

	  	Dao1.setTransNo(TransNo);
	  	Dao1.setName(req.getParameter("userName"));
	  	Dao1.setAmount(Long.valueOf(req.getParameter("Amount")));

	  	SqlResult = Dao1.insert001();
	  	String disp;

	  	if (SqlResult == 0) {
	  		req.setAttribute("ErrMsg","データの重複が発生しました。");
	  		disp = "/Transfer.jsp";
	  	}else{
	  		disp = "/Transfer_Finish.jsp";
	  	}

	    RequestDispatcher dispatch = req.getRequestDispatcher(disp);
	    //dispatch.forward(req, res);
	    dispatch.include(req, res);

    }
}

DB操作:TransactionDao.java
package dao;

import java.sql.SQLException;

public class TransactionDao  {

	private int TransNo = 0;
	private String Name = "";
	private long Amount = 0;

	private static final String insert001 = ""
			+ "insert into "
			+ "  transaction "
			+ "values"
			+ "  ("
			+ "    TRANS_NO,"
			+ "    'TRANS_NAME',"
			+ "    TRANS_AMOUNT"
			+ "  );";
    public int insert001() {

	    String sql = insert001;
	    sql = sql.replaceAll("TRANS_NO",Integer.toString(TransNo));
	    sql = sql.replaceAll("TRANS_NAME",Name);
	    sql = sql.replaceAll("TRANS_AMOUNT",Long.toString(Amount));

	    try {
	    	return DBManager.simpleUpdate(sql);
	    }catch (SQLException e) {
	    	return 0;
	    }
    }

    public int getTransNo() {
    	return TransNo;
    }
    public void setTransNo(int TransNo) {
    	this.TransNo = TransNo;
    }

    public String getName() {
    	return Name;
    }
    public void setName(String Name) {
    	this.Name = Name;
    }

    public Long getAmount() {
    	return Amount;
    }
    public void setAmount(Long Amount) {
    	this.Amount = Amount;
    }

}

本日はここまで!
フッター広告

スポンサードリンク



シェアする

  • このエントリーをはてなブックマークに追加

フォローする