今天我们将使用 servlet 和 JSP 编写一个井字游戏。
该项目将与之前的项目略有不同。它不仅包含任务,还包含如何执行任务的说明。也就是说,它将是“HOW TO ...”系列中的一个项目。
操作说明:
- 从存储库中分叉: https: //github.com/vasylmalik/project-servlet.git
- 将您的项目版本下载到您的计算机。
- 在 IDEA 中设置应用程序启动:
- Alt + Shift + F9 -> 编辑配置... -> Alt + 插入 -> tom(进入搜索栏)-> Local。
- 之后,您需要单击“配置”并指明下载和解压缩带有 Tomcat 的存档文件的位置。
- 在“部署”选项卡中:Alt + insert -> Artifact ... -> tic-tac-toe:war exploded -> OK。
- 在“应用程序上下文”字段中:仅保留“/”(斜线)。
- 按“应用”。
- 关闭设置窗口。
- 对自定义配置进行首次试运行。如果一切都正确完成,您的默认浏览器将打开,其中将是:
- 打开“pom.xml”文件。“依赖项”块中有 2 个依赖项。
javax.servlet-api
负责servlets的规范。在开发期间需要“提供”的范围,但在运行时不需要(Tomcat 已经在 lib 文件夹中具有此依赖项)。jstl
– 可以被认为是一个模板引擎。- “webapp”文件夹中有 3 个文件:
index.jsp
- 这是我们的模板(类似于 HTML 页面)。它将包含标记和脚本。如果没有我们在步骤 3 中看到的配置,它就是作为初始页面给出的名为“index”的文件。/static/main.css
- 样式文件。和之前的项目一样,这里的一切都由你决定,想怎么画就怎么画。/static/jquery-3.6.0.min.js
- 我们的服务器将作为静态分发的前端依赖项。- “com.tictactoe”包将包含所有 Java 代码。现在有2个类:
Sign
- 枚举,负责“交叉/零/无效”。Field
是我们的领域。这个类有一个“字段”映射。数据存储的原则如下:井字游戏字段的单元格从零开始编号。在第一行中是 0、1 和 2。在第二行中:3、4 和 5。依此类推。也有3种方法。“getEmptyFieldIndex”寻找第一个空单元格(是的,我们的对手不会很聪明)。“checkWin”检查游戏是否结束。如果有一排三个十字,它返回一个十字;如果一排三个零,它返回一个零。否则,它是空的。“getFieldData” ——返回“字段”映射的值作为按索引升序排序的列表。- 关于模板的解释已经完成,现在您可以开始任务了。让我们从绘制一个 3×3 的表格开始。为此,将以下代码添加到“index.jsp”:
然后,我们将删除表中的数字,并用叉号、零或空字段替换它们。此外,在“head”标签内,包含样式文件。为此,添加一行:<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>
<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; }
- 现在让我们添加以下功能:单击单元格时,将向服务器发送请求,我们将在其中将被单击的单元格的索引作为参数传递。这个任务可以分为两个部分:从前端发送一个请求,在服务器上接受一个请求。让我们从前面开始改变。
让我们为每个“d” 标签添加一个“onclick”参数。在值中,我们指示当前页面更改为指定的 URL。负责逻辑的 servlet 将具有 URL “/logic”。它需要一个名为“click”的参数。所以我们将传递用户点击的单元格的索引。
您可以通过浏览器中的开发人员面板检查一切是否正确完成。例如,在 Chrome 中,它使用F12按钮打开。单击索引为 4 的单元格后,图片将如下所示: 我们得到一个错误,因为我们还没有创建一个可以将服务器发送到地址“logic”的 servlet 。<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>
- 在包“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; } }
- 现在我们可以点击了,但这还不是游戏。为了使游戏具有逻辑性,您需要在请求之间保存游戏状态(叉在哪里,零在哪里)。最简单的方法是将此数据存储在会话中。使用这种方法,会话将存储在服务器上,客户端将在名为“JSESSIONID”的 cookie 中收到一个会话 ID 。但是session不需要每次都创建,只需要在游戏开始的时候创建即可。让我们为此启动另一个 servlet,我们将其称为“InitServlet”。我们将覆盖其中的“doGet”方法,在该方法中我们将创建一个新会话,创建一个运动场,将这个运动场和一个 Sign 类型的列表放在会话属性中,并将“ forward”发送到 index.jsp页。代码:
不要忘记,让我们将启动服务器后在浏览器中打开的起始页更改为“/start”: 现在,在重新启动服务器并单击“请求标头” 部分的浏览器开发人员菜单中的字段的任何单元格后,将会有一个带有会话 ID 的 cookie: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”中。我们需要使用“doGet”方法。让我们将此行为添加到方法中:
- 我们将从会话中获取 Field 类型的“字段”对象(我们将其取出到“extractField”方法)。
- 在用户点击的地方打叉(到目前为止没有任何检查)。
行为尚未改变,但如果您在调试中启动服务器并在发送重定向的行上设置断点,您可以看到“数据”对象的“内部结构”。实际上,“CROSS”出现在被点击的索引下方。@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; }
- 现在是时候在前端显示十字了。为此,我们将使用“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>
现在,当您单击一个单元格时,那里会出现一个十字:
- 在 <head> 部分添加:
- 我们已经采取行动,现在轮到“零”了。让我们在这里添加一些检查,以便标志不会放置在已占用的单元格中。
- 您需要检查单击的单元格是否为空。否则,我们什么也不做,将用户发送到同一页面而不更改会话参数。
- 由于该字段上的单元格数量是奇数,因此可能放置了一个十字,但没有零的空间。因此,我们在打叉后,尝试获取一个未被占用的单元格的索引(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; } }
- 在这个阶段,你可以打叉,AI 的答案是零。但是没有检查何时停止游戏。这可以分为三种情况:
- 十字的下一步移动之后,形成了三个十字的线;
- 在下一个带零的返回移动之后,形成了一条由三个零组成的线;
- 在十字的下一步移动之后,空单元格结束。
此方法的特殊之处在于,如果找到获胜者,我们会向会话添加另一个参数,我们将使用该参数更改以下段落中 “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; }
- 让我们向“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; } }
- 在行为方面,几乎没有任何变化(除了如果其中一个符号获胜,则不再放置零。让我们在“index.jsp”中使用“winner”参数并显示获胜者。我们在表格
c:set
后面使用指令:c:if
如果交叉获胜,则显示“交叉获胜!”的 消息 ,如果零是“NOUGHTS WIN!” . 结果,我们可以获得两个铭文之一:<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>
- 如果有赢家,你需要能够报复。为此,您需要一个向服务器发送请求的按钮。服务器将使当前会话无效并将请求重定向回“/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"); } }
- 在“head”部分的“index.jsp”中,编写脚本“jquery”。使用这个库,我们将向服务器发送请求。
- 最后一种情况还有待考虑。如果用户打叉,没有胜利,也没有零的位置怎么办?那么这是一个平局,我们现在将处理它:
- 在“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>
- 在“LogicServlet”中向会话添加另一个“draw”参数,更新“data”字段并将重定向发送到“index.jsp”:
这样就完成了游戏的编写。
他们使用的类和文件的代码
初始化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;
}
GO TO FULL VERSION