今天我们将使用 servlet 和 JSP 编写一个井字游戏。

该项目将与之前的项目略有不同。它不仅包含任务,还包含如何执行任务的说明。也就是说,它将是“HOW TO ...”系列中的一个项目。

操作说明:

  1. 从存储库中分叉: https: //github.com/vasylmalik/project-servlet.git
  2. 将您的项目版本下载到您的计算机。
  3. 在 IDEA 中设置应用程序启动:
    • Alt + Shift + F9 -> 编辑配置... -> Alt + 插入 -> tom(进入搜索栏)-> Local。
    • 之后,您需要单击“配置”并指明下载和解压缩带有 Tomcat 的存档文件的位置。
    • 在“部署”选项卡中:Alt + insert -> Artifact ... -> tic-tac-toe:war exploded -> OK。
    • 在“应用程序上下文”字段中:仅保留“/”(斜线)。
    • 按“应用”。
    • 关闭设置窗口。
    • 对自定义配置进行首次试运行。如果一切都正确完成,您的默认浏览器将打开,其中将是:
  4. 打开“pom.xml”文件。“依赖项”块中有 2 个依赖项。
    • javax.servlet-api负责servlets的规范。在开发期间需要“提供”的范围,但在运行时不需要(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- 枚举,负责“交叉/零/无效”
    • Field是我们的领域。这个类有一个“字段”映射。数据存储的原则如下:井字游戏字段的单元格从零开始编号。在第一行中是 0、1 和 2。在第二行中:3、4 和 5。依此类推。也有3种方法。“getEmptyFieldIndex”寻找第一个空单元格(是的,我们的对手不会很聪明)。“checkWin”检查游戏是否结束。如果有一排三个十字,它返回一个十字;如果一排三个零,它返回一个零。否则,它是空的。“getFieldData” ——返回“字段”映射的值作为按索引升序排序的列表。
  7. 关于模板的解释已经完成,现在您可以开始任务了。让我们从绘制一个 3×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. 现在让我们添加以下功能:单击单元格时,将向服务器发送请求,我们将在其中将被单击的单元格的索引作为参数传递。这个任务可以分为两个部分:从前端发送一个请求,在服务器上接受一个请求。让我们从前面开始改变。

    让我们为每个“d” 标签添加一个“onclick”参数。在值中,我们指示当前页面更改为指定的 URL。负责逻辑的 servlet 将具有 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”的 servlet 。
  9. 在包“com.tictactoe”中创建一个类“LogicServlet”,它应该从类“javax.servlet.http.HttpServlet”派生。在类中,覆盖“doGet”方法。

    让我们添加一个方法来获取被单击的单元格的索引。您还需要添加一个映射(此 servlet 将拦截请求的地址)。我建议通过注释来做到这一点(但如果你喜欢困难,你也可以使用 web.xml)。通用servlet代码:
    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 。但是session不需要每次都创建,只需要在游戏开始的时候创建即可。让我们为此启动另一个 servlet,我们将其称为“InitServlet”。我们将覆盖其中的“doGet”方法,在该方法中我们将创建一个新会话,创建一个运动场,将这个运动场和一个 Sign 类型的列表放在会话属性中,并将“ forward”发送到 index.jsp页。代码:
    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 类型的“字段”对象(我们将其取出到“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> 块内的表中,将索引更改为允许您计算值的结构。例如,对于索引零: <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 的答案是零。但是没有检查何时停止游戏。这可以分为三种情况:
    • 十字的下一步移动之后,形成了三个十字的线;
    • 在下一个带零的返回移动之后,形成了一条由三个零组成的线;
    • 在十字的下一步移动之后,空单元格结束。
    让我们添加一个方法来检查一行中是否有三个叉号/零号:
    /**
     * 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 ”方法的调用。第一次设置十字后,第二次 - 设置零后。
    // 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. 在行为方面,几乎没有任何变化(除了如果其中一个符号获胜,则不再放置零。让我们在“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>
    如果交叉获胜,则显示“交叉获胜!”的 消息 ,如果零是“NOUGHTS WIN!” . 结果,我们可以获得两个铭文之一:
  17. 如果有赢家,你需要能够报复。为此,您需要一个向服务器发送请求的按钮。服务器将使当前会话无效并将请求重定向回“/start”
    • 在“head”部分的“index.jsp”中,编写脚本“jquery”。使用这个库,我们将向服务器发送请求。
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • “index.jsp”“script”部分,添加一个可以向服务器发送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>
    • 让我们创建一个新的 servlet 来为“/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>
      作为平局的结果,我们将收到相应的消息和重新开始的提议:

这样就完成了游戏的编写。

他们使用的类和文件的代码

初始化Servlet

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

重启Servlet

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>

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