今日はサーブレットと JSP を使用して三目並べゲームを作成します。

このプロジェクトはこれまでのものとは少し異なります。タスクだけでなく、その実行方法の説明も含まれます。つまり「HOW TO…」シリーズの企画となります。

命令:

  1. リポジトリからフォーク: https://github.com/CodeGymCC/project-servlet.git
  2. プロジェクトのバージョンをコンピュータにダウンロードします。
  3. IDEA でアプリケーションの起動を設定します。
    • Alt + Shift + F9 -> 構成の編集… -> Alt + 挿入 -> トム (検索バーに) -> ローカル。
    • その後、「CONFIGURE」をクリックして、Tomcat を含むアーカイブをダウンロードして解凍した場所を指定する必要があります。
    • 「展開」タブ: Alt + 挿入 -> アーティファクト… -> 三目並べ:戦争爆発 -> OK。
    • 「アプリケーションコンテキスト」フィールドでは、「/」(スラッシュ)のみを残します。
    • 「適用」を押します。
    • 設定ウィンドウを閉じます。
    • カスタマイズした構成の最初のテスト実行を行います。すべてが正しく行われると、デフォルトのブラウザが開き、次のようになります。
  4. 「pom.xml」ファイルを開きます。「依存関係」ブロックには2 つの依存関係があります。
    • javax.servlet-apiサーブレットの仕様を担当します。スコープ「provided」は開発中に必要ですが、実行時には必要ありません (Tomcat は既に lib フォルダーにこの依存関係を持っています)。
    • jstl– テンプレート エンジンとみなすことができます。
  5. 「webapp」フォルダーには3 つのファイルがあります。
    • index.jsp- これは私たちのテンプレートです (HTML ページに似ています)。これにはマークアップとスクリプトが含まれます。ステップ 3 で確認したように、設定がない場合に最初のページとして指定されるのは、「index」というファイルです。
    • /static/main.css- スタイル用のファイル。前のプロジェクトと同様に、ここでの作業はすべてあなた次第で、好きなようにペイントできます。
    • /static/jquery-3.6.0.min.js- サーバーが静的として配布するフロントエンドの依存関係。
  6. 「com.tictactoe」パッケージには、すべての Java コードが含まれます。現在、次の 2 つのクラスがあります。
    • Sign- enum、 「cross / zero / void」を担当します。
    • Field私たちの分野です。このクラスには「フィールド」マップがあります。データ保存の原理は次のとおりです。三目並べフィールドのセルには 0 から番号が付けられます。最初の行は 0、1、2、2 行目は 3、4、5 などとなります。方法も3つあります。「getEmptyFieldIndex」は最初の空のセルを探します (はい、対戦相手はあまり賢くないでしょう)。「checkWin」はゲームが終了したかどうかをチェックします。3 つのバツの行がある場合はバツを返し、3 つのゼロの行がある場合は 0 を返します。それ以外の場合は空です。「getFieldData」 - 「フィールド」マップの値をインデックスの昇順でソートされたリストとして返します。
  7. テンプレートの説明が終わりましたので、タスクを開始してください。まずは 3 x 3 のテーブルを描画します。これを行うには、次のコードを「index.jsp」に追加します。
    <table>
    	<tr>
    		<td>0</td>
    		<td>1</td>
    		<td>2</td>
    	</tr>
    	<tr>
    		<td>3</td>
    		<td>4</td>
    		<td>5</td>
    	</tr>
    	<tr>
    		<td>6</td>
    		<td>7</td>
    		<td>8</td>
    	</tr>
    </table>
    次に、テーブル内の数値を削除し、バツ、ゼロ、または空のフィールドに置き換えます。また、「head」タグ内にスタイルファイルを記述します。これを行うには、次の行を追加します。<link href="static/main.css" rel="stylesheet">

    スタイル ファイルの内容はあなた次第です。私はこれを使用しました:
    td {
        border: 3px solid black;
        padding: 10px;
        border-collapse: separate;
        margin: 10px;
        width: 100px;
        height: 100px;
        font-size: 50px;
        text-align: center;
        empty-cells: show;
    }
    実行後の結果は次のようになります。
  8. 次に、次の機能を追加しましょう。セルがクリックされると、リクエストがサーバーに送信され、クリックされたセルのインデックスがパラメータとして渡されます。このタスクは 2 つの部分に分けることができます。フロントからのリクエストの送信と、サーバーでのリクエストの受け入れです。気分転換にフロントから始めましょう。

    各「d」 タグに「onclick」パラメータを追加しましょう。値では、現在のページが指定された URL に変更されることを示します。ロジックを担当するサーブレットの URL は「/logic」になります。そして、 「click」というパラメータを受け取ります。したがって、ユーザーがクリックしたセルのインデックスを渡します。
    <table>
        <tr>
            <td onclick="window.location='/logic?click=0'">0</td>
            <td onclick="window.location='/logic?click=1'">1</td>
            <td onclick="window.location='/logic?click=2'">2</td>
        </tr>
        <tr>
            <td onclick="window.location='/logic?click=3'">3</td>
            <td onclick="window.location='/logic?click=4'">4</td>
            <td onclick="window.location='/logic?click=5'">5</td>
        </tr>
        <tr>
            <td onclick="window.location='/logic?click=6'">6</td>
            <td onclick="window.location='/logic?click=7'">7</td>
            <td onclick="window.location='/logic?click=8'">8</td>
        </tr>
    </table>
    ブラウザの開発者パネルから、すべてが正しく行われたことを確認できます。たとえば、Chrome では、F12ボタンで開きます。インデックス 4 のセルをクリックした結果、図は次のようになります。 サーバーをアドレス"logic" に送信できるサーブレットをまだ作成していないため、エラーが発生します。
  9. パッケージ「com.tictactoe」内に、クラス「javax.servlet.http.HttpServlet」から派生するクラス「 LogicServlet」を作成します。クラス内で、「doGet」メソッドをオーバーライドします。

    そして、クリックされたセルのインデックスを取得するメソッドを追加しましょう。また、マッピング (このサーブレットがリクエストをインターセプトするアドレス) を追加する必要もあります。注釈を使用してこれを行うことをお勧めします (ただし、難しい場合は、web.xml を使用することもできます)。一般的なサーブレット コード:
    package com.tictactoe;
    
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    
    @WebServlet(name = "LogicServlet", value = "/logic")
    public class LogicServlet extends HttpServlet {
        @Override
    	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            int index = getSelectedIndex(req);
            resp.sendRedirect("/index.jsp");
        }
    
    
        private int getSelectedIndex(HttpServletRequest request) {
            String click = request.getParameter("click");
            boolean isNumeric = click.chars().allMatch(Character::isDigit);
            return isNumeric ? Integer.parseInt(click) : 0;
        }
    
    }
    ここで、任意のセルをクリックすると、サーバー上のこのセルのインデックスが取得されます (サーバーをデバッグで実行することで確認できます)。そして、クリックされたページと同じページにリダイレクトされます。
  10. これでクリックできるようになりましたが、まだゲームではありません。ゲームにロジックを持たせるには、リクエスト間でゲームの状態 (バツの位置、ゼロの位置) を保存する必要があります。これを行う最も簡単な方法は、このデータをセッションに保存することです。このアプローチでは、セッションはサーバーに保存され、クライアントは「JSESSIONID」という名前の Cookie でセッション ID を受け取ります。ただし、セッションは毎回作成する必要はなく、ゲームの開始時にのみ作成する必要があります。このために別のサーブレットを開始しましょう。これを「InitServlet」と呼びます。その中で「doGet」メソッドをオーバーライドします。そこで、新しいセッションを作成し、競技場を作成し、この競技場とタイプ Sign のリストをセッション属性に入れて、index.jsp に「forward」を送信しますページ。コード:
    package com.tictactoe;
    
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import java.io.IOException;
    import java.util.List;
    import java.util.Map;
    
    @WebServlet(name = "InitServlet", value = "/start")
    public class InitServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            // Create a new session
            HttpSession currentSession = req.getSession(true);
    
            // Create a playing field
            Field field = new Field();
            Map<Integer, Sign> fieldData = field.getField();
    
            // Get a list of field values
            List<Sign> data = field.getFieldData();
    
            // Adding field parameters to the session (needed to store state between requests)
            currentSession.setAttribute("field", field);
            // and field values ​​sorted by index (required for drawing crosses and zeroes)
            currentSession.setAttribute("data", data);
    
            // Redirect request to index.jsp page via server
            getServletContext().getRequestDispatcher("/index.jsp").forward(req, resp);
        }
    }
    そして忘れないように、サーバーの起動後にブラウザーで開くスタート ページを「/start」に変更しましょう。 サーバーを再起動し、ブラウザーの開発者メニューの「リクエスト ヘッダー」 セクションのフィールドのセルをクリックした後、、セッション ID を持つ Cookie が存在します。
  11. クライアント (ブラウザー) からのリクエスト間の状態を保存できるリポジトリがあれば、ゲーム ロジックの作成を開始できます。私たちのロジックは「LogicServlet」にあります。「doGet」メソッドを使用する必要があります。この動作をメソッドに追加しましょう。
    • セッションから Field タイプの「field」オブジェクトを取得します (それを「extractField」メソッドに取り出します)。
    • ユーザーがクリックした場所にバツ印を付けます (ここまではチェックなし)。
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Get the current session
        HttpSession currentSession = req.getSession();
    
        // Get the playfield object from the session
        Field field = extractField(currentSession);
    
        // get the index of the cell that was clicked
        int index = getSelectedIndex(req);
    
        // put a cross in the cell that the user clicked on
        field.getField().put(index, Sign.CROSS);
    
        // Read the list of icons
        List<Sign> data = field.getFieldData();
    
        // Update field object and icon list in session
        currentSession.setAttribute("data", data);
        currentSession.setAttribute("field", field);
    
        resp.sendRedirect("/index.jsp");
    }
    
    
    
    private Field extractField(HttpSession currentSession) {
        Object fieldAttribute = currentSession.getAttribute("field");
        if (Field.class != fieldAttribute.getClass()) {
            currentSession.invalidate();
            throw new RuntimeException("Session is broken, try one more time");
        }
        return (Field) fieldAttribute;
    }
    動作はまだ変わっていませんが、デバッグでサーバーを起動し、リダイレクトが送信される行にブレークポイントを設定すると、「データ」オブジェクトの「内部」を確認できます。すると、確かに、クリックしたインデックスの下に 「CROSS」と表示されます。
  12. 次に、フロントエンドに十字架を表示します。これを行うには、 「index.jsp」ファイルと「JSTL」テクノロジーを使用します。
    • <head> セクションに以下を追加します。<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • 各 <td> ブロック内のテーブルで、インデックスを値を計算できる構造に変更します。たとえば、インデックス 0 の場合: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> セルをクリックすると、そこに十字が表示されます。
  13. 私たちは動きました、次は「ゼロ」の番です。ここで、すでに占有されているセルに標識が配置されないように、いくつかのチェックを追加しましょう。
    • クリックされたセルが空であることを確認する必要があります。それ以外の場合は、何もせず、セッション パラメーターを変更せずにユーザーを同じページに送信します。
    • フィールド上のセルの数が奇数であるため、十字が置かれた可能性はありますが、ゼロが入る余地はありません。したがって、バツ印を付けた後、空いているセルのインデックスを取得しようとします (Field クラスの getEmptyFieldIndex メソッド)。インデックスが負でない場合は、そこにゼロを置きます。 コード:
      package com.tictactoe;
      
      import javax.servlet.RequestDispatcher;
      import javax.servlet.ServletException;
      import javax.servlet.annotation.WebServlet;
      import javax.servlet.http.HttpServlet;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import javax.servlet.http.HttpSession;
      import java.io.IOException;
      import java.util.List;
      
      @WebServlet(name = "LogicServlet", value = "/logic")
      public class LogicServlet extends HttpServlet {
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              // Get the current session
              HttpSession currentSession = req.getSession();
      
              // Get the playfield object from the session
              Field field = extractField(currentSession);
      
              // get the index of the cell that was clicked
              int index = getSelectedIndex(req);
              Sign currentSign = field.getField().get(index);
      
              // Check if the clicked cell is empty.
              // Otherwise, we do nothing and send the user to the same page without changes
              // parameters in the session
              if (Sign.EMPTY != currentSign) {
                  RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/index.jsp");
                  dispatcher.forward(req, resp);
                  return;
              }
      
              // put a cross in the cell that the user clicked on
              field.getField().put(index, Sign.CROSS);
      
              // Get an empty field cell
              int emptyFieldIndex = field.getEmptyFieldIndex();
      
              if (emptyFieldIndex >= 0) {
                  field.getField().put(emptyFieldIndex, Sign.NOUGHT);
              }
      
              // Read the list of icons
              List<Sign> data = field.getFieldData();
      
              // Update field object and icon list in session
              currentSession.setAttribute("data", data);
              currentSession.setAttribute("field", field);
      
              resp.sendRedirect("/index.jsp");
          }
      
          private int getSelectedIndex(HttpServletRequest request) {
              String click = request.getParameter("click");
              boolean isNumeric = click.chars().allMatch(Character::isDigit);
              return isNumeric ? Integer.parseInt(click) : 0;
          }
      
          private Field extractField(HttpSession currentSession) {
              Object fieldAttribute = currentSession.getAttribute("field");
              if (Field.class != fieldAttribute.getClass()) {
                  currentSession.invalidate();
                  throw new RuntimeException("Session is broken, try one more time");
              }
              return (Field) fieldAttribute;
          }
      }
  14. この段階では×を付けることができ、AI はゼロで答えます。ただし、ゲームをいつ停止するかを確認することはできません。これには次の 3 つのケースが考えられます。
    • クロスの次の動きの後、3 つのクロスのラインが形成されました。
    • 次のゼロを使ったリターンムーブの後、3 つのゼロの列が形成されました。
    • 次の十字の移動の後、空のセルは終了します。
    連続して 3 つのバツ/ゼロがあるかどうかを確認するメソッドを追加しましょう。
    /**
     * The method checks if there are three X/O's in a row.
     * returns true/false
     */
    private boolean checkWin(HttpServletResponse response, HttpSession currentSession, Field field) throws IOException {
        Sign winner = field.checkWin();
        if (Sign.CROSS == winner || Sign.NOUGHT == winner) {
            // Add a flag to indicate that someone has won
            currentSession.setAttribute("winner", winner);
    
            // Read the list of icons
            List<Sign> data = field.getFieldData();
    
            // Update this list in session
            currentSession.setAttribute("data", data);
    
            // helmet redirect
            response.sendRedirect("/index.jsp");
            return true;
        }
        return false;
    }
    このメソッドの特徴は、勝者が見つかった場合、セッションに別のパラメータを追加し、それを使用して次の段落で 「index.jsp」の表示を変更することです。
  15. 「doGet」メソッドに「checkWin 」メソッドの呼び出しを 2 回追加しましょう。1回目はクロスを設定した後、2回目はゼロを設定した後です。
    // Check if the cross won after adding the user's last click
    if (checkWin(resp, currentSession, field)) {
        return;
    }
    if (emptyFieldIndex >= 0) {
        field.getField().put(emptyFieldIndex, Sign.NOUGHT);
        // Check if the zero won after adding the last zero
        if (checkWin(resp, currentSession, field)) {
            return;
        }
    }
  16. 動作に関しては、ほとんど何も変わっていません (符号の 1 つが勝った場合にゼロが配置されなくなる点を除いて)。「index.jsp」の「winner」パラメータを使用して、勝者を表示しましょう。テーブル c:setの後にディレクティブを使用します。c:if
    <hr>
    <c:set var="CROSSES" value="<%=Sign.CROSS%>"/>
    <c:set var="NOUGHTS" value="<%=Sign.NOUGHT%>"/>
    
    <c:if test="${winner == CROSSES}">
        <h1>CROSSES WIN!</h1>
    </c:if>
    <c:if test="${winner == NOUGHTS}">
        <h1>NOUGHTS WIN!</h1>
    </c:if>
    クロスが勝った場合は「CROSSES WIN!」 のメッセージが表示されます。、ゼロが「NOUGHTS WIN!」の場合 。その結果、次の 2 つの碑文のいずれかを取得できます。
  17. 勝者がいる場合は、リベンジできる必要があります。これを行うには、サーバーにリクエストを送信するボタンが必要です。そして、サーバーは現在のセッションを無効にし、リクエストを"/start"にリダイレクトします。
    • 「index.jsp」の「head」セクションに、スクリプト「jquery」を記述します。このライブラリを使用して、サーバーにリクエストを送信します。
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • 「script」セクションの「index.jsp」に、サーバーにPOSTリクエストを送信できる関数を追加します。この関数を同期させ、サーバーから応答が返されると、現在のページをリロードします。
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • 「c:if」ブロック内に、クリックすると先ほど作成した関数を呼び出すボタンを追加します。
      <c:if test="${winner == CROSSES}">
          <h1>CROSSES WIN!</h1>
          <button onclick="restart()">Start again</button>
      </c:if>
      <c:if test="${winner == NOUGHTS}">
          <h1>NOUGHTS WIN!</h1>
          <button onclick="restart()">Start again</button>
      </c:if>
    • 「/restart」 URLを提供する新しいサーブレットを作成しましょう。
      package com.tictactoe;
      
      import javax.servlet.annotation.WebServlet;
      import javax.servlet.http.HttpServlet;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      
      @WebServlet(name = "RestartServlet", value = "/restart")
      public class RestartServlet extends HttpServlet {
          @Override
          protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
              req.getSession().invalidate();
              resp.sendRedirect("/start");
          }
      }
      勝利すると「やり直す」ボタンが表示されます。クリックするとフィールドが完全にクリアされ、ゲームが最初からやり直しになります。
  18. 最後の状況を考慮する必要があります。ユーザーがバツ印を付けた場合、勝利はなく、ゼロを入れる場所もない場合はどうなるでしょうか? この場合、これは引き分けなので、これから処理します。
    • 「LogicServlet」セッションで、別のパラメータ「draw」を追加し、 「data」フィールドを更新して、 「index.jsp」にリダイレクトを送信します。
      // If such a cell exists
      if (emptyFieldIndex >= 0) {}
      // If there is no empty cell and no one wins, then it's a draw
      else {
          // Add a flag to the session that signals that a draw has occurred
          currentSession.setAttribute("draw", true);
      
          // Read the list of icons
          List<Sign> data = field.getFieldData();
      
          // Update this list in session
          currentSession.setAttribute("data", data);
      
          // helmet redirect
          response.sendRedirect("/index.jsp");
          return;
      }
    • 「index.jsp」では、このパラメータを処理します。
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      抽選の結果、対応するメッセージと最初からやり直すオファーを受け取ります。

これでゲームの作成は完了です。

クラスとそれらが操作するファイルのコード

InitServlet

package com.tictactoe;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;
import java.util.Map;

@WebServlet(name = "InitServlet", value = "/start")
public class InitServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Create a new session
        HttpSession currentSession = req.getSession(true);

        // Create a playing field
        Field field = new Field();
        Map<Integer, Sign> fieldData = field.getField();

        // Get a list of field values
        List<Sign> data = field.getFieldData();

        // Adding field parameters to the session (needed to store state between requests)
        currentSession.setAttribute("field", field);
        // and field values ​​sorted by index (required for drawing crosses and zeroes)
        currentSession.setAttribute("data", data);

        // Redirect request to index.jsp page via server
        getServletContext().getRequestDispatcher("/index.jsp").forward(req, resp);
    }
}

ロジックサーブレット

package com.tictactoe;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;

@WebServlet(name = "LogicServlet", value = "/logic")
public class LogicServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Get the current session
        HttpSession currentSession = req.getSession();

        // Get the playfield object from the session
        Field field = extractField(currentSession);

        // get the index of the cell that was clicked
        int index = getSelectedIndex(req);
        Sign currentSign = field.getField().get(index);

        // Check if the clicked cell is empty.
        // Otherwise, we do nothing and send the user to the same page without changes
        // parameters in the session
        if (Sign.EMPTY != currentSign) {
            RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/index.jsp");
            dispatcher.forward(req, resp);
            return;
        }

        // put a cross in the cell that the user clicked on
        field.getField().put(index, Sign.CROSS);

        // Check if the cross has won after adding the user's last click
        if (checkWin(resp, currentSession, field)) {
            return;
        }

        // Get an empty field cell
        int emptyFieldIndex = field.getEmptyFieldIndex();

        if (emptyFieldIndex >= 0) {
            field.getField().put(emptyFieldIndex, Sign.NOUGHT);
            // Check if the zero won after adding the last zero
            if (checkWin(resp, currentSession, field)) {
                return;
            }
        }
        // If there is no empty cell and no one wins, then it's a draw
        else {
            // Add a flag to the session that signals that a draw has occurred
            currentSession.setAttribute("draw", true);

            // Read the list of icons
            List<Sign> data = field.getFieldData();

            // Update this list in session
            currentSession.setAttribute("data", data);

            // helmet redirect
            resp.sendRedirect("/index.jsp");
            return;
        }

        // Read the list of icons
        List<Sign> data = field.getFieldData();

        // Update field object and icon list in session
        currentSession.setAttribute("data", data);
        currentSession.setAttribute("field", field);

        resp.sendRedirect("/index.jsp");
    }

    /**
     * The method checks if there are three X/O's in a row.
     * returns true/false
     */
    private boolean checkWin(HttpServletResponse response, HttpSession currentSession, Field field) throws IOException {
        Sign winner = field.checkWin();
        if (Sign.CROSS == winner || Sign.NOUGHT == winner) {
            // Add a flag to indicate that someone has won
            currentSession.setAttribute("winner", winner);

            // Read the list of icons
            List<Sign> data = field.getFieldData();

            // Update this list in session
            currentSession.setAttribute("data", data);

            // helmet redirect
            response.sendRedirect("/index.jsp");
            return true;
        }
        return false;
    }

    private int getSelectedIndex(HttpServletRequest request) {
        String click = request.getParameter("click");
        boolean isNumeric = click.chars().allMatch(Character::isDigit);
        return isNumeric ? Integer.parseInt(click) : 0;
    }

    private Field extractField(HttpSession currentSession) {
        Object fieldAttribute = currentSession.getAttribute("field");
        if (Field.class != fieldAttribute.getClass()) {
            currentSession.invalidate();
            throw new RuntimeException("Session is broken, try one more time");
        }
        return (Field) fieldAttribute;
    }
}

再起動サーブレット

package com.tictactoe;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "RestartServlet", value = "/restart")
public class RestartServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        req.getSession().invalidate();
        resp.sendRedirect("/start");
    }
}

インデックス.jsp _

<%@ page import="com.tictactoe.Sign" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<!DOCTYPE html>
<html>
<head>
    <link href="static/main.css" rel="stylesheet">
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    <title>Tic-Tac-Toe</title>
</head>
<body>
<h1>Tic-Tac-Toe</h1>

<table>
    <tr>
        <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td>
        <td onclick="window.location='/logic?click=1'">${data.get(1).getSign()}</td>
        <td onclick="window.location='/logic?click=2'">${data.get(2).getSign()}</td>
    </tr>
    <tr>
        <td onclick="window.location='/logic?click=3'">${data.get(3).getSign()}</td>
        <td onclick="window.location='/logic?click=4'">${data.get(4).getSign()}</td>
        <td onclick="window.location='/logic?click=5'">${data.get(5).getSign()}</td>
    </tr>
    <tr>
        <td onclick="window.location='/logic?click=6'">${data.get(6).getSign()}</td>
        <td onclick="window.location='/logic?click=7'">${data.get(7).getSign()}</td>
        <td onclick="window.location='/logic?click=8'">${data.get(8).getSign()}</td>
    </tr>
</table>

<hr>
<c:set var="CROSSES" value="<%=Sign.CROSS%>"/>
<c:set var="NOUGHTS" value="<%=Sign.NOUGHT%>"/>

<c:if test="${winner == CROSSES}">
    <h1>CROSSES WIN!</h1>
    <button onclick="restart()">Start again</button>
</c:if>
<c:if test="${winner == NOUGHTS}">
    <h1>NOUGHTS WIN!</h1>
    <button onclick="restart()">Start again</button>
</c:if>
<c:if test="${draw}">
    <h1>IT'S A DRAW</h1>
    <button onclick="restart()">Start again</button>
</c:if>

<script>
    function restart() {
        $.ajax({
            url: '/restart',
            type: 'POST',
            contentType: 'application/json;charset=UTF-8',
            async: false,
            success: function () {
                location.reload();
            }
        });
    }
</script>

</body>
</html>

main.css _

td {
    border: 3px solid black;
    padding: 10px;
    border-collapse: separate;
    margin: 10px;
    width: 100px;
    height: 100px;
    font-size: 50px;
    text-align: center;
    empty-cells: show;
   }