ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSP+Servlet+MySQL游戏商城实战:从零搭建可控Web系统

JSP+Servlet+MySQL游戏商城实战:从零搭建可控Web系统 简介本资源是一个基于Java Web技术栈实现的游戏在线购买系统面向Java初学者与Web开发入门学习者帮助其掌握MVC分层架构下的电商类项目开发全流程。系统完整实现管理员与用户双角色功能管理员可进行游戏、类目、订单及客户管理用户可浏览商品、加入购物车、下单支付并查看订单涵盖从登录认证到数据持久化的典型业务闭环。资源包共246个文件含55个Java源码DAO/Service/Servlet、20个JSP页面、55个编译后Class文件、62张界面截图及配套CSS/JS/SQL等资源总大小14.85MB结构清晰便于按模块理解前后端交互逻辑。已有96人学习下载附带完整数据库脚本、默认账号密码及主流IDE兼容配置说明开箱即用适合课程设计、毕业设计或Java Web技能实战训练。1. 为什么一个“游戏购买网站”还值得用 JSPServletMySQL 重做一遍不是所有老技术都该被淘汰——当你需要在两周内交付一个能跑在 Tomcat 7/8 上、支持学生课程设计答辩、对接校内实训机房环境、且不依赖 Spring Boot 自动装配和前端构建工具的 Web 系统时JSP Servlet MySQL 这套组合反而成了最稳的“压舱石”。它不炫技但每一步都可追踪表单提交走哪个 Servlet数据库连接在哪初始化JSP 页面里${user.nickname}是怎么从 request.setAttribute() 里捞出来的全在眼皮底下。我带过 12 届 JavaWeb 实训发现学生翻车最多的地方不是不会写 MyBatis XML而是搞不清request.getRequestDispatcher().forward()和response.sendRedirect()的本质区别——前者是服务端跳转、URL 不变、request 域有效后者是客户端重定向、URL 变、request 域清空。这个项目标题背后其实是一套被低估的“可控性训练体系”用最朴素的三层结构表现层 JSP、控制层 Servlet、数据层 JDBC把 HTTP 请求生命周期、会话管理、事务边界、SQL 注入防御这些硬核概念全部钉死在代码行里。适合刚学完 JDBC 和 HTML 表单、还没碰过 MVC 框架的学生也适合需要快速验证业务流程、拒绝黑匣子部署的中小团队原型验证。2. 从零搭起骨架Tomcat MySQL 项目结构三件套落地实操2.1 Tomcat 8.5.99兼容 JDK 8本地部署与验证别急着写代码先确认容器能扛住请求。下载 Apache Tomcat 8.5.x必须选 8.5.99 或 8.5.94避开 8.5.97 因 CVE-2023-46589 导致的 WAR 包解压路径遍历漏洞解压后进入bin目录# Windows 下执行 startup.bat # Linux/macOS 下执行 chmod x startup.sh ./startup.sh启动后访问http://localhost:8080看到 Apache Tomcat Welcome Page 即成功。关键验证点打开conf/server.xml确认Connector port8080 protocolHTTP/1.1下没有URIEncodingUTF-8—— 这个属性必须手动加上否则中文参数如?gameName原神会乱码。加完重启再用 curl 测试curl http://localhost:8080/?q测试中文 | iconv -f ISO-8859-1 -t UTF-8 2/dev/null | grep -q 测试中文 echo ✅ URI 编码生效 || echo ❌ 需检查 server.xml提示若看到SEVERE: Failed to initialize connector [Connector[HTTP/1.1-8080]]八成是端口被占用。用netstat -ano | findstr :8080Windows或lsof -i :8080macOS/Linux查 PID 后 kill 掉。2.2 MySQL 5.7.44 安装与游戏库初始化避坑版热词里反复出现mysql 5.7.44不是偶然——这是 5.7 系列最后一个稳定 LTS 版本对 JDBC 4.2 驱动兼容性极佳且sql_mode默认不含STRICT_TRANS_TABLES避免学生因插入 NULL 到非空字段直接报错崩溃。安装时严禁用 MySQL Installer 全家桶它会偷偷装 Service、注册 Windows 服务、改 PATH导致后续手动启停混乱。正确做法下载mysql-5.7.44-winx64.zip官网归档页非最新下载页解压到D:\mysql-5.7.44创建my.ini放入根目录内容精简到只保留必要项[mysqld] port3306 basedirD:\\mysql-5.7.44 datadirD:\\mysql-5.7.44\\data max_connections100 character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci sql_modeNO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION log-errorD:\\mysql-5.7.44\\logs\\error.log [client] default-character-setutf8mb4初始化并启动# 进入 bin 目录 cd D:\mysql-5.7.44\bin # 初始化 data 目录首次运行 mysqld --initialize-insecure --usermysql # 安装为 Windows 服务可选推荐手动启停更可控 mysqld --install MySQL57 --defaults-fileD:\\mysql-5.7.44\\my.ini # 手动启动推荐 mysqld --defaults-fileD:\\mysql-5.7.44\\my.ini启动后用mysql -u root -p登录初始密码为空执行建库脚本CREATE DATABASE IF NOT EXISTS game_store CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE game_store; -- 游戏主表含价格、库存、状态 CREATE TABLE games ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, status ENUM(on_sale,out_of_stock,discontinued) DEFAULT on_sale, cover_url VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 用户表简化版仅需登录验证 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, -- 存 BCrypt 加密后字符串 email VARCHAR(100), balance DECIMAL(10,2) DEFAULT 0.00, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 订单表核心业务链路起点 CREATE TABLE orders ( id VARCHAR(32) PRIMARY KEY, -- UUID 字符串避免暴露订单量 user_id INT NOT NULL, game_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, total_price DECIMAL(10,2) NOT NULL, status ENUM(pending,paid,shipped,cancelled) DEFAULT pending, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (game_id) REFERENCES games(id) );注意utf8mb4是必须项否则 emoji 表情如游戏名里的 存不进数据库ENUM类型比VARCHAR更安全避免状态值写错orders.id用 UUID 而非自增防止竞拍类场景被刷单。2.3 Eclipse Dynamic Web Project 项目结构规范用 Eclipse EE 版2023-09 或更新新建Dynamic Web Project务必勾选 “Generate web.xml deployment descriptor”—— 这是 Servlet 3.0 以下版本路由的核心。项目结构必须严格遵循game-store/ ├── WebContent/ │ ├── index.jsp ← 首页游戏列表 │ ├── login.jsp ← 登录页 │ ├── cart.jsp ← 购物车页 │ ├── order_confirm.jsp ← 订单确认页 │ └── WEB-INF/ │ ├── web.xml ← Servlet 映射中枢 │ └── lib/ │ └── mysql-connector-java-5.1.49.jar ← 必须用 5.1.x兼容 JDK 8 ├── src/ │ ├── servlet/ │ │ ├── LoginServlet.java │ │ ├── GameListServlet.java │ │ ├── AddToCartServlet.java │ │ └── CheckoutServlet.java │ └── dao/ │ ├── GameDAO.java │ ├── UserDAO.java │ └── OrderDAO.java └── build.propertiesweb.xml中定义 Servlet 映射关键不能全靠注解servlet servlet-nameLoginServlet/servlet-name servlet-classservlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping提示mysql-connector-java-5.1.49.jar是黄金版本——5.1.x 系列对com.mysql.jdbc.Driver类支持最稳而 8.0.x 驱动要求com.mysql.cj.jdbc.Driver且需显式配置serverTimezoneGMT%2B8学生极易漏配导致java.sql.SQLException: The server time zone value ... is unrecognized。用 5.1.49 可省去所有时区烦恼。3. 核心业务链路从用户登录到订单生成的 Servlet 控制流拆解3.1 LoginServlet表单验证 Session 绑定 防暴力破解登录不是简单查密码。LoginServlet.doPost()必须完成三件事①参数清洗request.getParameter(username).trim()去首尾空格防admin这类空格绕过②凭证校验用BCrypt.checkpw(inputPassword, dbHash)替代明文比对③会话绑定HttpSession session request.getSession(true); session.setAttribute(user, user);完整代码节选protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username).trim(); String password request.getParameter(password); // 1. 基础校验 if (username null || username.length() 0 || password null) { request.setAttribute(error, 用户名或密码不能为空); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } // 2. DAO 查询含密码校验 UserDAO userDAO new UserDAO(); User user userDAO.findByUsername(username); if (user null || !BCrypt.checkpw(password, user.getPassword())) { // 3. 记录失败次数防爆破 int failCount (int) request.getSession().getAttribute(loginFailCount); request.getSession().setAttribute(loginFailCount, failCount 1); request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } // 4. 登录成功重置失败计数、绑定用户、跳转首页 request.getSession().removeAttribute(loginFailCount); request.getSession().setAttribute(user, user); response.sendRedirect(request.getContextPath() /index.jsp); }参数说明request.getSession(true)的true表示“如果 session 不存在则创建”这是登录场景必需session.setAttribute(user, user)后所有 JSP 页面可通过${sessionScope.user.username}直接取值无需重复查询。3.2 GameListServlet分页查询 状态过滤 图片路径拼接游戏列表页要解决三个现实问题数据量大时不能SELECT * FROM games全查哪怕只有 200 条也会卡“已下架”游戏不该展示给普通用户封面图 URL 需统一前缀如/images/covers/xxx.jpg。Servlet 中分页逻辑protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int page Integer.parseInt(request.getParameter(page) ! null ? request.getParameter(page) : 1); int pageSize 12; // 每页 12 个游戏 int offset (page - 1) * pageSize; GameDAO gameDAO new GameDAO(); // 关键WHERE statuson_sale 过滤LIMIT OFFSET 分页 ListGame games gameDAO.findOnSaleGames(offset, pageSize); int total gameDAO.countOnSaleGames(); // 单独 COUNT 语句 int totalPages (int) Math.ceil((double) total / pageSize); request.setAttribute(games, games); request.setAttribute(currentPage, page); request.setAttribute(totalPages, totalPages); request.setAttribute(baseUrl, request.getContextPath() /list?page); request.getRequestDispatcher(/index.jsp).forward(request, response); }对应 JSP 中图片渲染解决热词jsp图片如何对坐标定位的底层需求c:forEach items${games} vargame div classgame-card styleposition: relative; img src${pageContext.request.contextPath}/images/covers/${game.coverUrl} alt${game.name} stylewidth: 100%; height: 200px; object-fit: cover; !-- 绝对定位价格标签 -- div styleposition: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; padding: 4px 8px; ¥${game.price} /div /div /c:forEach注意img的src必须用${pageContext.request.contextPath}开头否则静态资源路径在 WAR 包部署后会 404object-fit: cover保证图片等比缩放填满容器解决“图片变形”痛点。3.3 CheckoutServlet数据库事务 库存扣减 订单号生成支付环节是唯一必须用事务的地方。CheckoutServlet伪代码逻辑// 1. 开启事务Connection.setAutoCommit(false) // 2. 查询游戏库存SELECT stock FROM games WHERE id? FOR UPDATE // 3. 若库存 quantity → rollback返回错误 // 4. 更新库存UPDATE games SET stock stock - ? WHERE id? // 5. 插入订单INSERT INTO orders (...) VALUES (...) // 6. 生成 UUID 订单号String orderId UUID.randomUUID().toString().replace(-, ) // 7. commit() // 8. 跳转 success.jspDAO 层关键代码GameDAO.updateStockAndCreateOrder()public boolean updateStockAndCreateOrder(Connection conn, int gameId, int quantity, int userId, BigDecimal totalPrice) throws SQLException { // 步骤2SELECT ... FOR UPDATE 锁定该行 String selectSql SELECT stock FROM games WHERE id ? FOR UPDATE; try (PreparedStatement ps conn.prepareStatement(selectSql)) { ps.setInt(1, gameId); ResultSet rs ps.executeQuery(); if (!rs.next() || rs.getInt(stock) quantity) { return false; // 库存不足 } } // 步骤4扣减库存 String updateSql UPDATE games SET stock stock - ? WHERE id ?; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, quantity); ps.setInt(2, gameId); ps.executeUpdate(); } // 步骤5插入订单注意order_id 是 UUID 字符串 String insertSql INSERT INTO orders (id, user_id, game_id, quantity, total_price) VALUES (?, ?, ?, ?, ?); String orderId UUID.randomUUID().toString().replace(-, ); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setString(1, orderId); ps.setInt(2, userId); ps.setInt(3, gameId); ps.setInt(4, quantity); ps.setBigDecimal(5, totalPrice); ps.executeUpdate(); } return true; }提示FOR UPDATE是 InnoDB 行锁防止并发下单超卖UUID.randomUUID().toString().replace(-, )生成 32 位无横线字符串比System.currentTimeMillis()更难被预测和刷单。4. 避坑指南JSPServletMySQL 项目中 5 个血泪级常见问题4.1 现象JSP 页面中文显示为??但数据库里存的是正确的汉字原因JSP 文件本身编码不是 UTF-8或页面未声明 charset。解决① 在 Eclipse 中右键 JSP 文件 → Properties → Resource → Text file encoding → 选UTF-8② JSP 顶部添加% page contentTypetext/html;charsetUTF-8 %③ HTML head 内写meta http-equivContent-Type contenttext/html; charsetUTF-8④ Tomcatserver.xmlConnector 中确认URIEncodingUTF-8已设置。4.2 现象response.sendRedirect(/success.jsp)后页面 404原因sendRedirect()是客户端跳转路径相对于域名根目录而非当前应用上下文。解决错误写法response.sendRedirect(/success.jsp)→ 访问http://localhost:8080/success.jsp缺上下文正确写法response.sendRedirect(request.getContextPath() /success.jsp)→http://localhost:8080/game-store/success.jsp。4.3 现象MySQL 插入时间字段自动设为0000-00-00 00:00:00原因MySQL 5.7 默认sql_mode含NO_ZERO_DATE但 JDBC 驱动未传zeroDateTimeBehaviorconvertToNull。解决JDBC URL 改为jdbc:mysql://localhost:3306/game_store?useUnicodetruecharacterEncodingutf8mb4zeroDateTimeBehaviorconvertToNullserverTimezoneGMT%2B8注意serverTimezoneGMT%2B8是 URL 编码后的GMT84.4 现象购物车 addItem 后刷新页面商品消失原因购物车存在request域request.setAttribute()而刷新是新请求request域已销毁。解决购物车必须存在session域// 正确存入 session HttpSession session request.getSession(); ListCartItem cart (ListCartItem) session.getAttribute(cart); if (cart null) { cart new ArrayList(); session.setAttribute(cart, cart); } cart.add(new CartItem(gameId, quantity));4.5 现象c:forEach标签报org.apache.jasper.JasperException: /index.jsp (line: 12, column: 0) According to TLD or attribute directive in tag file, attribute items does not accept any expressions原因JSTL 标签库未正确引入或web.xml中web-app版本低于 2.4。解决①web.xml头部必须为web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1②WEB-INF/lib/下放入jstl-1.2.jar和standard-1.1.2.jarJSTL 1.2 配 standard 1.1.2 最稳③ JSP 顶部加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %。5. 进阶实战让 JSP 页面加载完自动刷新一次的三种可靠方案热词里高频出现jsp页面让加载完后刷新一次这不是玄学需求——它真实存在于两类场景①登录后跳转首页需刷新以显示欢迎语和余额因session刚绑定JSP 第一次渲染时 EL 表达式可能未生效②支付成功页需刷新触发订单状态轮询或防止重复提交。下面给出三种生产环境验证过的方案按可靠性排序5.1 方案一meta http-equivrefresh最轻量适用于跳转后首次渲染在success.jsp或index.jsp的head内加入!-- 仅当 URL 含 ?refresh1 时执行刷新 -- c:if test${not empty param.refresh} meta http-equivrefresh content0;url${pageContext.request.requestURL}?refreshed1/ /c:if然后在CheckoutServlet重定向时带上参数response.sendRedirect(request.getContextPath() /success.jsp?refresh1);优势纯前端无 JS 依赖劣势URL 会多出?refresh1需配合后端判断避免无限循环。5.2 方案二JavaScriptlocation.reload() Session 标记推荐用于登录后首页在index.jsp底部添加c:if test${empty sessionScope.userRefreshed} script // 设置 Session 标记通过 AJAX fetch(${pageContext.request.contextPath}/mark-refreshed, { method: POST, headers: {Content-Type: application/x-www-form-urlencoded}, body: actionmark }).then(() location.reload()); /script /c:if配套MarkRefreshedServletprotected void doPost(HttpServletRequest request, HttpServletResponse response) { request.getSession().setAttribute(userRefreshed, true); }优势URL 干净刷新后sessionScope.userRefreshed为 true下次不再触发劣势需额外 Servlet。5.3 方案三HTTP Header 强制缓存失效最彻底适用于支付成功页在SuccessServlet中设置响应头response.setHeader(Cache-Control, no-cache, no-store, must-revalidate); // HTTP 1.1 response.setHeader(Pragma, no-cache); // HTTP 1.0 response.setDateHeader(Expires, 0); // Proxies request.getRequestDispatcher(/success.jsp).forward(request, response);同时在success.jsp加防重复提交c:if test${not empty param.paid} script // 支付成功后禁用按钮防止刷新重复提交 document.addEventListener(DOMContentLoaded, function() { const btn document.getElementById(payBtn); if (btn) btn.disabled true; }); /script /c:if优势从协议层杜绝浏览器缓存确保每次都是新请求劣势需配合前端按钮禁用否则用户仍可 F5 刷新。我一般会在登录成功后用方案二Session 标记支付成功页用方案三Header 控制因为前者要保障用户体验后者要保障业务原子性。这三招用下来再没遇到过学生抱怨“登录后名字不显示”或“付款页刷新就重复扣款”的问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表