Heute werden wir ein Tic-Tac-Toe-Spiel mit Servlets und JSP schreiben.

Dieses Projekt wird sich ein wenig von den vorherigen unterscheiden. Es enthält nicht nur Aufgaben, sondern auch Erklärungen zu deren Ausführung. Das heißt, es wird ein Projekt aus der Reihe „HOW TO ...“ sein.

Anweisung:

  1. Fork aus dem Repository: https://github.com/CodeGymCC/project-servlet.git
  2. Laden Sie Ihre Version des Projekts auf Ihren Computer herunter.
  3. Richten Sie den Anwendungsstart in IDEA ein:
    • Alt + Umschalt + F9 -> Konfigurationen bearbeiten… -> Alt + Einfügen -> Tom (in die Suchleiste) -> Lokal.
    • Danach müssen Sie auf „KONFIGURIEREN“ klicken und angeben, wo das Archiv mit Tomcat heruntergeladen und entpackt wurde.
    • Auf der Registerkarte „Bereitstellung“: Alt + Einfügen -> Artefakt… -> tic-tac-toe:war explodiert -> OK.
    • Im Feld „Anwendungskontext“: Lassen Sie nur „/“ (Schrägstrich) stehen.
    • Drücken Sie „ANWENDEN“.
    • Schließen Sie das Einstellungsfenster.
    • Machen Sie den ersten Testlauf der angepassten Konfiguration. Wenn alles richtig gemacht wurde, öffnet sich Ihr Standardbrowser, in dem Folgendes angezeigt wird:
  4. Öffnen Sie die Datei „pom.xml“ . Im Block „Abhängigkeiten“ gibt es 2 Abhängigkeiten .
    • javax.servlet-apiist für die Spezifikation von Servlets verantwortlich. Der „bereitgestellte“ Bereich wird während der Entwicklung, aber nicht zur Laufzeit benötigt (Tomcat verfügt bereits über diese Abhängigkeit im lib-Ordner).
    • jstl– kann als Template-Engine betrachtet werden.
  5. Im Ordner „webapp“ befinden sich 3 Dateien :
    • index.jsp- das ist unsere Vorlage (ähnlich der HTML-Seite). Es enthält Markup und Skripte. Es ist die Datei mit dem Namen „index“ , die als Startseite angegeben wird, wenn keine Konfigurationen vorhanden sind, die wir in Schritt 3 gesehen haben.
    • /static/main.css- Datei für Stile. Wie im vorherigen Projekt liegt auch hier alles bei Ihnen, malen Sie, wie Sie möchten.
    • /static/jquery-3.6.0.min.js- Frontend-Abhängigkeit, die unser Server statisch verteilt.
  6. Das Paket „com.tictactoe“ enthält den gesamten Java-Code. Im Moment gibt es 2 Klassen:
    • Sign- Aufzählung, die für das „Kreuz/Null/Leere“ verantwortlich ist .
    • Fieldist unser Fachgebiet. Diese Klasse verfügt über eine „Feld“ -Map . Das Prinzip der Datenspeicherung ist wie folgt: Die Zellen des Tic-Tac-Toe-Feldes werden von Null an nummeriert. In der ersten Zeile 0, 1 und 2. In der zweiten: 3, 4 und 5. Und so weiter. Es gibt auch 3 Methoden. „getEmptyFieldIndex“ sucht nach der ersten leeren Zelle (ja, unser Gegner wird nicht sehr schlau sein). „checkWin“ prüft, ob das Spiel beendet ist. Bei einer Reihe mit drei Kreuzen wird ein Kreuz zurückgegeben; bei einer Reihe mit drei Nullen wird eine Null zurückgegeben. Ansonsten ist es leer. „getFieldData“ – gibt die Werte der „field“ -Map als Liste zurück, sortiert in aufsteigender Indexreihenfolge.
  7. Die Erläuterungen zur Vorlage sind abgeschlossen, nun können Sie mit der Aufgabe beginnen. Beginnen wir mit dem Zeichnen einer 3x3-Tabelle. Fügen Sie dazu den folgenden Code zu „index.jsp“ hinzu :
    <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>
    Anschließend entfernen wir die Zahlen in der Tabelle und ersetzen sie durch ein Kreuz, eine Null oder ein leeres Feld. Fügen Sie außerdem innerhalb des „head“-Tags die Style-Datei ein. Fügen Sie dazu eine Zeile hinzu:<link href="static/main.css" rel="stylesheet">

    Der Inhalt der Style-Datei bleibt Ihnen überlassen. Ich habe dieses verwendet:
    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;
    }
    Nach dem Ausführen sieht mein Ergebnis so aus:
  8. Fügen wir nun die folgende Funktionalität hinzu: Wenn auf eine Zelle geklickt wird, wird eine Anfrage an den Server gesendet, in der wir den Index der angeklickten Zelle als Parameter übergeben. Diese Aufgabe kann in zwei Teile unterteilt werden: eine Anfrage von vorne senden und eine Anfrage auf dem Server annehmen. Fangen wir zur Abwechslung einmal vorne an.

    Fügen wir jedem „d“ -Tag einen „onclick“ -Parameter hinzu . Im Wert geben wir den Wechsel der aktuellen Seite zur angegebenen URL an. Das Servlet, das für die Logik verantwortlich ist, hat die URL „/logic“ . Und es wird ein Parameter namens „click“ benötigt . Wir übergeben also den Index der Zelle, auf die der Benutzer geklickt hat.
    <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>
    Sie können über das Entwicklerfenster im Browser überprüfen, ob alles richtig gemacht wurde. In Chrome wird es beispielsweise mit der F12 -Taste geöffnet . Wenn Sie auf eine Zelle mit Index 4 klicken, sieht das Bild wie folgt aus: Wir erhalten eine Fehlermeldung, weil wir noch kein Servlet erstellt haben, das den Server an die Adresse „logic“ senden kann .
  9. Erstellen Sie im Paket „com.tictactoe“ eine Klasse „LogicServlet“ , die von der Klasse „javax.servlet.http.HttpServlet“ abgeleitet werden soll . Überschreiben Sie in der Klasse die Methode „doGet“ .

    Und fügen wir eine Methode hinzu, die den Index der Zelle erhält, auf die geklickt wurde. Sie müssen außerdem eine Zuordnung hinzufügen (die Adresse, an der dieses Servlet die Anfrage abfängt). Ich schlage vor, dies über eine Anmerkung zu tun (wenn Sie jedoch Schwierigkeiten mögen, können Sie auch web.xml verwenden). Allgemeiner Servlet-Code:
    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;
        }
    
    }
    Wenn wir nun auf eine beliebige Zelle klicken, erhalten wir den Index dieser Zelle auf dem Server (Sie können dies sicherstellen, indem Sie den Server im Debug-Modus ausführen). Und es erfolgt eine Weiterleitung auf die gleiche Seite, von der aus der Klick erfolgt ist.
  10. Jetzt können wir klicken, aber es ist noch kein Spiel. Damit das Spiel über Logik verfügt, müssen Sie den Status des Spiels (wo die Kreuze sind, wo die Nullen sind) zwischen den Anfragen speichern. Der einfachste Weg, dies zu tun, besteht darin, diese Daten in der Sitzung zu speichern. Bei diesem Ansatz wird die Sitzung auf dem Server gespeichert und der Client erhält eine Sitzungs-ID in einem Cookie namens „JSESSIONID“ . Die Sitzung muss jedoch nicht jedes Mal erstellt werden, sondern nur zu Beginn des Spiels. Starten wir dazu ein weiteres Servlet, das wir „InitServlet“ nennen werden . Wir werden die darin enthaltene „doGet“ -Methode überschreiben, indem wir eine neue Sitzung erstellen, ein Spielfeld erstellen, dieses Spielfeld und eine Liste vom Typ „Sign“ in die Sitzungsattribute einfügen und „ weiterleiten“ an die index.jsp senden Buchseite. Code:
    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);
        }
    }
    Und nicht zu vergessen, ändern wir die Startseite, die sich im Browser nach dem Starten des Servers öffnet, in „/start“ : Jetzt nach dem Neustart des Servers und dem Klicken auf eine beliebige Zelle des Feldes im Browser-Entwicklermenü im Abschnitt „Request Headers“. , es wird ein Cookie mit der Sitzungs-ID vorhanden sein:
  11. Wenn wir ein Repository haben, in dem wir den Status zwischen Anfragen vom Client (Browser) speichern können, können wir mit dem Schreiben der Spiellogik beginnen. Die Logik, die wir haben, befindet sich in „LogicServlet“ . Wir müssen mit der „doGet“ -Methode arbeiten . Fügen wir der Methode dieses Verhalten hinzu:
    • Wir erhalten das Objekt „field“ vom Typ „Field“ aus der Sitzung (wir werden es zur Methode „extractField“ übertragen ).
    • Setzen Sie ein Kreuz an die Stelle, auf die der Benutzer geklickt hat (bisher ohne Überprüfung).
    @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;
    }
    Das Verhalten hat sich noch nicht geändert, aber wenn Sie den Server im Debug-Modus starten und einen Haltepunkt in der Zeile setzen, an die die Umleitung gesendet wird, können Sie das „Innere“ des „Daten“ -Objekts sehen . Dort erscheint tatsächlich „CROSS“ unter dem angeklickten Index.
  12. Jetzt ist es an der Zeit, das Kreuz im Frontend anzuzeigen. Dazu arbeiten wir mit der Datei „index.jsp“ und der Technologie „JSTL“ .
    • Fügen Sie im Abschnitt <head> Folgendes hinzu:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Ändern Sie in der Tabelle in jedem <td>-Block den Index in ein Konstrukt, mit dem Sie Werte berechnen können. Zum Beispiel für den Index Null: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Wenn Sie nun auf eine Zelle klicken, erscheint dort ein Kreuz:
  13. Wir haben unseren Zug gemacht, jetzt ist die „Null“ an der Reihe. Und fügen wir hier noch ein paar Kontrollen hinzu, damit die Schilder nicht in bereits belegten Zellen platziert werden.
    • Sie müssen überprüfen, ob die angeklickte Zelle leer ist. Andernfalls unternehmen wir nichts und leiten den Benutzer auf dieselbe Seite weiter, ohne die Sitzungsparameter zu ändern.
    • Da die Anzahl der Zellen auf dem Feld ungerade ist, ist es möglich, dass ein Kreuz gesetzt wurde, für eine Null aber kein Platz ist. Nachdem wir ein Kreuz gesetzt haben, versuchen wir daher, den Index einer unbesetzten Zelle abzurufen (die Methode getEmptyFieldIndex der Klasse Field). Wenn der Index nicht negativ ist, tragen Sie dort eine Null ein. Code:
      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. In dieser Phase können Sie Kreuze setzen, die KI antwortet mit Nullen. Es gibt jedoch keine Kontrolle, wann das Spiel beendet werden soll. Dies kann in drei Fällen der Fall sein:
    • nach der nächsten Bewegung des Kreuzes bildete sich eine Reihe aus drei Kreuzen;
    • nach dem nächsten Rückzug mit einer Null bildete sich eine Reihe aus drei Nullen;
    • Nach dem nächsten Kreuzzug endeten die leeren Zellen.
    Fügen wir eine Methode hinzu, die prüft, ob drei Kreuze/Nullen hintereinander vorhanden sind:
    /**
     * 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;
    }
    Die Besonderheit dieser Methode besteht darin, dass wir, wenn der Gewinner gefunden wird, der Sitzung einen weiteren Parameter hinzufügen, mit dem wir in den folgenden Absätzen die Anzeige in „index.jsp“ ändern .
  15. Fügen wir der Methode „doGet“ zweimal einen Aufruf der Methode „ checkWin “ hinzu . Das erste Mal nach dem Setzen des Kreuzes, das zweite Mal nach dem Setzen der Null.
    // 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. Bezüglich des Verhaltens hat sich fast nichts geändert (außer dass, wenn eines der Zeichen gewinnt, keine Nullen mehr platziert werden. Lassen Sie uns den Parameter „winner“ in „index.jsp“ verwenden und den Gewinner anzeigen. Wir verwenden Anweisungen nach der Tabelle: c:setc: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>
    Wenn Flanken gewinnen, wird die Meldung „KREUZE GEWINNEN!“ angezeigt. , wenn die Nullen „NOUGHTS WIN!“ lauten. . Als Ergebnis können wir eine von zwei Inschriften erhalten:
  17. Wenn es einen Gewinner gibt, müssen Sie in der Lage sein, sich zu rächen. Dazu benötigen Sie einen Button, der eine Anfrage an den Server sendet. Und der Server macht die aktuelle Sitzung ungültig und leitet die Anfrage zurück an „/start“ .
    • Schreiben Sie in „index.jsp“ im Abschnitt „head“ das Skript „jquery“ . Mithilfe dieser Bibliothek senden wir eine Anfrage an den Server.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • Fügen Sie in „index.jsp“ im Abschnitt „script“ eine Funktion hinzu, die eine POST-Anfrage an den Server senden kann. Wir machen die Funktion synchron und wenn eine Antwort vom Server kommt, wird die aktuelle Seite neu geladen.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Fügen Sie innerhalb der „c:if“ -Blöcke eine Schaltfläche hinzu, die beim Klicken die Funktion aufruft, die wir gerade geschrieben haben:
      <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>
    • Erstellen wir ein neues Servlet, das die URL „/restart“ bereitstellt .
      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");
          }
      }
      Nach dem Sieg erscheint die Schaltfläche „Neu starten“ . Nachdem Sie darauf geklickt haben, wird das Feld vollständig geleert und das Spiel beginnt von vorne.
  18. Es bleibt die letzte Situation zu betrachten. Was wäre, wenn der Benutzer ein Kreuz gesetzt hätte, es keinen Sieg gegeben hätte und es keinen Platz für eine Null gäbe? Dann ist dies eine Auslosung, und wir werden sie jetzt bearbeiten:
    • Fügen Sie in der „LogicServlet“ -Sitzung einen weiteren Parameter „draw“ hinzu, aktualisieren Sie das Feld „data“ und senden Sie eine Weiterleitung an „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;
      }
    • In „index.jsp“ verarbeiten wir diesen Parameter:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Als Ergebnis einer Auslosung erhalten wir die entsprechende Nachricht und ein Angebot zum Neuanfang:

Damit ist das Schreiben des Spiels abgeschlossen.

Code der Klassen und Dateien, mit denen sie gearbeitet haben

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);
    }
}

LogicServlet

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;
    }
}

Starten Sie das Servlet neu

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");
    }
}

index.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;
   }