Vandaag gaan we een Tic-Tac-Toe-spel schrijven met servlets en JSP.

Dit project zal een beetje anders zijn dan de vorige. Het bevat niet alleen taken, maar ook uitleg over hoe ze moeten worden uitgevoerd. Dat wil zeggen, het wordt een project uit de serie "HOW TO ...".

Instructie:

  1. Fork uit de repository: https://github.com/CodeGymCC/project-servlet.git
  2. Download uw versie van het project naar uw computer.
  3. Applicatie starten instellen in IDEA:
    • Alt + Shift + F9 -> Configuraties bewerken... -> Alt + invoegen -> tom (in de zoekbalk) -> Lokaal.
    • Daarna moet u op "CONFIGUREREN" klikken en aangeven waar het archief met Tomcat is gedownload en uitgepakt.
    • Op het tabblad "Inzet": Alt + invoegen -> Artefact… -> Boter-kaas-en-eieren: oorlog ontploft -> OK.
    • In het veld "Toepassingscontext": laat alleen "/" (slash) staan.
    • Druk op "TOEPASSEN".
    • Sluit het instellingenvenster.
    • Maak de eerste testrun van de aangepaste configuratie. Als alles correct is gedaan, wordt uw standaardbrowser geopend, waarin het zal zijn:
  4. Open het bestand "pom.xml" . Er zijn 2 afhankelijkheden in het blok "afhankelijkheden" .
    • javax.servlet-apiis verantwoordelijk voor de specificatie van servlets. Scope "mits" is nodig tijdens de ontwikkeling, maar niet tijdens runtime (Tomcat heeft deze afhankelijkheid al in de map lib).
    • jstl- kan worden beschouwd als een sjabloon-engine.
  5. Er zijn 3 bestanden in de map "webapp" :
    • index.jsp- dit is ons sjabloon (vergelijkbaar met de HTML-pagina). Het zal markeringen en scripts bevatten. Het is het bestand met de naam "index" dat wordt gegeven als de eerste pagina, als er geen configuraties zijn, die we in stap 3 hebben gezien.
    • /static/main.css- bestand voor stijlen. Net als in het vorige project, is alles hier aan jou, schilder zoals je wilt.
    • /static/jquery-3.6.0.min.js- frontend-afhankelijkheid die onze server als statisch zal distribueren.
  6. Het pakket "com.tictactoe" zal alle Java-code bevatten. Op dit moment zijn er 2 klassen:
    • Sign- enum, dat verantwoordelijk is voor de "cross / zero / void" .
    • Fieldis ons vakgebied. Deze klasse heeft een "veld" -kaart . Het principe van gegevensopslag is als volgt: de cellen van het boter-kaas-en-eierenveld zijn genummerd vanaf nul. In de eerste regel 0, 1 en 2. In de tweede: 3, 4 en 5. En zo verder. Er zijn ook 3 methodes. "getEmptyFieldIndex" zoekt naar de eerste lege cel (ja, onze tegenstander zal niet erg slim zijn). "checkWin" controleert of het spel voorbij is. Als er een rij van drie kruisjes is, wordt een kruisje geretourneerd; als er een rij van drie nullen is, wordt een nul geretourneerd. Anders is het leeg. "getFieldData" - retourneert de waarden van de "veld" -kaart als een lijst gesorteerd in oplopende indexvolgorde.
  7. De uitleg over de sjabloon is voltooid, nu kunt u met de taak beginnen. Laten we beginnen met het tekenen van een tabel van 3 bij 3. Voeg hiervoor de volgende code toe aan "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>
    We zullen dan de getallen in de tabel verwijderen en vervangen door een kruisje, nul of een leeg veld. Neem ook binnen de "head" -tag het stijlbestand op. Voeg hiervoor een regel toe:<link href="static/main.css" rel="stylesheet">

    De inhoud van het stijlbestand is aan jou. Ik heb deze gebruikt:
    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;
    }
    Na het uitvoeren ziet mijn resultaat er als volgt uit:
  8. Laten we nu de volgende functionaliteit toevoegen: wanneer er op een cel wordt geklikt, wordt er een verzoek naar de server gestuurd, waarin we de index van de cel waarop is geklikt als parameter doorgeven. Deze taak kan in twee delen worden verdeeld: een verzoek van voren verzenden, een verzoek op de server accepteren. Laten we voor de verandering eens vooraan beginnen.

    Laten we een "onclick" -parameter aan elke "d" -tag toevoegen . In de waarde geven we de verandering van de huidige pagina naar de opgegeven URL aan. De servlet die verantwoordelijk is voor de logica heeft de URL “/logic” . En er is een parameter nodig met de naam "klik" . We geven dus de index door van de cel waarop de gebruiker heeft geklikt.
    <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>
    U kunt controleren of alles correct is gedaan via het ontwikkelaarspaneel in de browser. In Chrome wordt het bijvoorbeeld geopend met de F12- knop . Door op een cel met index 4 te klikken, ziet het beeld er als volgt uit: We krijgen een foutmelding omdat we nog geen servlet hebben gemaakt die de server naar het adres "logic" kan sturen .
  9. Maak in het pakket "com.tictactoe" een klasse "LogicServlet" aan die moet worden afgeleid van de klasse "javax.servlet.http.HttpServlet" . Overschrijf in de klas de methode "doGet" .

    En laten we een methode toevoegen die de index krijgt van de cel waarop is geklikt. U moet ook een toewijzing toevoegen (het adres waarop deze servlet het verzoek zal onderscheppen). Ik stel voor dit te doen door middel van een annotatie (maar als je van moeilijkheden houdt, kun je ook web.xml gebruiken). Algemene servletcode:
    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;
        }
    
    }
    Als we nu op een willekeurige cel klikken, krijgen we de index van deze cel op de server (u kunt dit controleren door de server in foutopsporing uit te voeren). En er zal een omleiding zijn naar dezelfde pagina van waaruit de klik is gemaakt.
  10. Nu kunnen we klikken, maar het is nog geen spel. Om ervoor te zorgen dat het spel logica heeft, moet je de status van het spel opslaan (waar de kruisjes zijn, waar de nullen zijn) tussen verzoeken. De eenvoudigste manier om dit te doen is door deze gegevens op te slaan in de sessie. Met deze aanpak wordt de sessie op de server opgeslagen en ontvangt de client een sessie-ID in een cookie met de naam "JSESSIONID" . Maar de sessie hoeft niet elke keer te worden gemaakt, maar alleen aan het begin van het spel. Laten we hiervoor een andere servlet starten, die we "InitServlet" zullen noemen . We zullen de "doGet" -methode daarin overschrijven , waarin we een nieuwe sessie zullen maken, een speelveld zullen creëren, dit speelveld en een lijst van het type zullen plaatsen Meld de sessieattributen aan en stuur " forward" naar de index.jsp bladzijde. 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);
        }
    }
    En niet te vergeten, laten we de startpagina die in de browser wordt geopend na het starten van de server wijzigen in "/start" : Nu na het herstarten van de server en klikken op een willekeurige cel van het veld in het browserontwikkelaarsmenu in het gedeelte "Verzoekkopteksten" , zal er een cookie zijn met de sessie-ID:
  11. Als we een repository hebben waarin we de status tussen verzoeken van de client (browser) kunnen opslaan, kunnen we beginnen met het schrijven van spellogica. De logica die we hebben is in "LogicServlet" . We moeten werken met de methode "doGet" . Laten we dit gedrag aan de methode toevoegen:
    • we zullen het "field" -object van het veldtype uit de sessie halen (we zullen het meenemen naar de "extractField" -methode ).
    • plaats een kruisje waar de gebruiker heeft geklikt (tot nu toe zonder vinkjes).
    @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;
    }
    Het gedrag is nog niet veranderd, maar als je de server in debug start en een breekpunt instelt op de regel waar de omleiding naartoe wordt gestuurd, kun je de "ingewanden" van het "data"-object zien . Daar verschijnt inderdaad "CROSS" onder de index waarop is geklikt.
  12. Nu is het tijd om het kruis op de frontend weer te geven. Om dit te doen, zullen we werken met het bestand "index.jsp" en de technologie "JSTL" .
    • Voeg in de sectie <head> toe:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • Verander in de tabel binnen elk <td>-blok de index in een constructie waarmee u waarden kunt berekenen. Bijvoorbeeld voor index nul: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> Als u nu op een cel klikt, verschijnt daar een kruis:
  13. We hebben onze zet gedaan, nu is het de beurt aan de "nul". En laten we hier een paar controles toevoegen, zodat de borden niet in reeds bezette cellen worden geplaatst.
    • U moet controleren of de cel waarop is geklikt leeg is. Anders doen we niets en sturen we de gebruiker naar dezelfde pagina zonder de sessieparameters te wijzigen.
    • Aangezien het aantal cellen op het veld oneven is, is het mogelijk dat er een kruisje is geplaatst, maar er is geen ruimte voor een nul. Daarom proberen we, nadat we een kruisje hebben geplaatst, de index van een onbezette cel te krijgen (de methode getEmptyFieldIndex van de klasse Field). Als de index niet negatief is, zet daar dan een nul. 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 dit stadium kunt u kruisjes plaatsen, AI antwoordt met nullen. Maar er is geen controle wanneer het spel moet worden gestopt. Dit kan in drie gevallen:
    • na de volgende zet van het kruis vormde zich een rij van drie kruisen;
    • na de volgende terugzet met een nul ontstond er een rij van drie nullen;
    • na de volgende zet van het kruis eindigden lege cellen.
    Laten we een methode toevoegen die controleert of er drie kruisjes / nullen op een rij staan:
    /**
     * 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;
    }
    De eigenaardigheid van deze methode is dat als de winnaar wordt gevonden, we een andere parameter aan de sessie toevoegen, waarmee we de weergave in "index.jsp" in de volgende paragrafen zullen wijzigen.
  15. Laten we een aanroep van de "checkWin "-methode twee keer toevoegen aan de "doGet"-methode . De eerste keer na het instellen van het kruis, de tweede - na het instellen van de nul.
    // 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. Qua gedrag is er bijna niets veranderd (behalve dat als een van de tekens wint, er geen nullen meer worden geplaatst. Laten we de parameter "winnaar" gebruiken in "index.jsp" en de winnaar weergeven. We gebruiken richtlijnen achter de tabel: 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>
    Als kruisen winnen, wordt het bericht "CROSSES WIN!" , als de nullen "NOUGHTS WIN!" . Als gevolg hiervan kunnen we een van de volgende twee inscripties krijgen:
  17. Als er een winnaar is, moet je wraak kunnen nemen. Hiervoor heeft u een knop nodig die een verzoek naar de server stuurt. En de server zal de huidige sessie ongeldig maken en het verzoek terugsturen naar "/start" .
    • Schrijf in "index.jsp" in het gedeelte "head" het script "jquery" . Met behulp van deze bibliotheek sturen we een verzoek naar de server.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • Voeg in "index.jsp" in het gedeelte "script" een functie toe die een POST-verzoek naar de server kan sturen. We zullen de functie synchroon maken en wanneer er een reactie van de server komt, wordt de huidige pagina opnieuw geladen.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • Voeg binnen de "c:if" -blokken een knop toe die, wanneer erop wordt geklikt, de functie aanroept die we zojuist hebben geschreven:
      <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>
    • Laten we een nieuwe servlet maken die de URL "/restart" zal bedienen .
      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");
          }
      }
      Na de overwinning verschijnt de knop "Opnieuw beginnen" . Nadat je erop hebt geklikt, wordt het veld volledig leeggemaakt en begint het spel opnieuw.
  18. Het blijft om de laatste situatie te overwegen. Wat als de gebruiker een kruis heeft geplaatst, er geen overwinning was en er geen plaats is voor een nul? Dan is dit een loting en gaan we het nu verwerken:
    • Voeg in de sessie "LogicServlet" nog een parameter "draw" toe, werk het veld "data" bij en stuur een omleiding naar "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" zullen we deze parameter verwerken:
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      Als resultaat van een trekking ontvangen we het bijbehorende bericht en een aanbod om opnieuw te beginnen:

Hiermee is het schrijven van het spel voltooid.

Code van klassen en bestanden waarmee ze werkten

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

HerstartServlet

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>

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