尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JSP+SQL网上书店实战解析:JDBC连接、事务扣库存与分页优化

JSP+SQL网上书店实战解析:JDBC连接、事务扣库存与分页优化 简介这是一份面向JSPSQL技术栈的网上书店设计与实现资料包适合高校学生作为毕业设计参考也适合个人开发者研究传统Java Web项目结构或小公司快速搭建图书销售模块。资源共436个文件压缩后约3.2MB内容以JSP页面、Java类、编译后的class文件为主配以gif/jpg图片与css样式构建前端界面另有doc论文、XML配置及数据库MDF/LDF文件可支撑从环境搭建到功能调试的完整流程。包内Java源码与class文件并存便于对照学习涵盖用户登录、图书管理、订单处理等核心业务论文文档有助于撰写设计说明数据库文件可直接附加使用快速检验运行效果这份压缩包将源码、数据库、论文整合在一起免去四处搜集的麻烦。整体目录结构清晰代码与文档分离适合按模块逐层拆解。学习人数已达162人。1. JSPSQL 网上书店这个经典骨架解压 zip 后先看哪三样东西“JSPSQL网上书店设计(源代码论文).zip”这个压缩包里实际上装着三件事JSP/Servlet 构成的 Web 源码、配套的 SQL 建库脚本、以及一篇讲清楚表设计和流程的论文。三件东西各司其职源码负责页面跳转和订单入库SQL 把用户、图书、订单这些实体组织起来论文则把需求分析和表设计讲圆。网上书店是课程设计和毕业设计里最常被选中的题目因为业务不大但流程完整注册登录、浏览图书、加入购物车、结算下单每一步都能映射到数据库表。对做课程设计的人来说它是完整交付模板对想复习 Java Web 基础的工程师它又是一条能看到 JSP 编译路径、JDBC 事务边界和 SQL 优化取舍的短线项目。下面按这三个层次拆开讲。2. JSPSQL 的技术骨架MVC 三层划分与 JDBC 连接参数2.1 为什么这套骨架不引入 SSM 或 Spring Boot反而更容易看懂网上书店这类业务的体量通常不会让单表查询成为瓶颈真正的瓶颈在事务和并发。不用框架时JSP 只负责把数据渲染成 HTMLServlet 负责接收请求、调用服务DAO 层负责把 SQL 包装成 Java 方法这个分层就是 MVC 的最低配版本。它的好处是每一条请求路径都能在代码里被完整追踪用户点了“加入购物车”浏览器把 bookId 和数量交给 CartServletCartServlet 调 CartDao 里的 insert 方法JSP 再回显购物车页面。当需要把整套逻辑改造成 Spring Boot 版本时DAO 和 Service 部分几乎可以原样搬过去。我一般把这个骨架看成无框架状态下的最佳实践而不是过时技术。如果直接把 JDBC 代码写进 JSP 页面会碰到几个固定问题页面嵌套大量 try/catch异常会直接暴露给浏览器连接对象用完不关会把数据库连接耗尽业务校验和 SQL 混在一起权限没法细粒度控制。所以在源码结构里第一眼应该先找 util、dao、servlet、filter、jsp 这五个包或目录而不是去翻全局静态配置。有人把这类问题整理成 Java 面试八股其实拆开看就一句话边界没划清后面所有维护成本都会被放大。2.2 DBUtil 连接参数MySQL 与 SQL Server 驱动差异数据库选型是 zip 里最容易产生分歧的点。用 MySQL 时连接工具类通常会写成下面的形式。我建议固定在连接串里写五个参数useUnicode、characterEncoding、serverTimezone、useSSL、allowPublicKeyRetrieval。前两个决定中文不乱码第三个决定时间字段不偏移后两个负责消除本地开发的握手警告。package book.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(缺少 MySQL 驱动 jar); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码里Class.forName负责注册驱动MySQL 8 以上用com.mysql.cj.jdbc.Driver老版本是com.mysql.jdbc.Driver两者不能混用。连接串里的useSSLfalse用来避免本地开发时的证书握手警告serverTimezone必须按实际时区写写成 UTC 后会在写入 DATETIME 字段时产生 8 小时偏移。如果数据库换成 SQL Server驱动类要改成com.microsoft.sqlserver.jdbc.SQLServerDriverURL 变成jdbc:sqlserver://localhost:1433;DatabaseNamebookstore然后再从微软官网下载对应 SQL Server 版本的 JDBC jar 放进WEB-INF/lib。驱动 jar 和数据库版本不匹配时最常见的报错是ClassNotFoundException或者UnsupportedClassVersionError。2.3 Servlet 与 JSP 的职责边界登录路由与转发重定向的区别以“用户登录”为例推荐写法是 JSP 提交表单到 LoginServletServlet 只做参数解析真正的验证逻辑交给 UserService最后通过转发或重定向决定页面去向。这样做的直接好处是无论前端改成 AJAX 还是将来接移动端Servlet 都能复用同一个 UserService 方法。package book.servlet; import book.entity.User; import book.service.UserService; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { private final UserService userService new UserService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); User user userService.login(username, password); if (user ! null) { HttpSession session req.getSession(); session.setAttribute(currentUser, user); resp.sendRedirect(book/list); } else { req.setAttribute(loginError, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }WebServlet(/login)是 Servlet 3.0 的注解式路径映射兼容性较好的项目会在 web.xml 里用servlet-mapping做同样的配置但两条路不要同时写。sendRedirect(book/list)会发起第二次请求地址栏跟着变化刷新页面时不会重复提交表单forward则保持在同一次请求内request 范围的loginError参数还能继续在 JSP 里显示。下面的表是这套骨架里每个目录的职责边界照着分就不会把登录代码写成一坨。分层常见包名职责关键注意事项Controllerservlet接收请求、校验参数、决定页面去向不要在 Servlet 里拼 SQLServiceservice业务规则与事务边界多步写操作用事务包住DAOdaoSQL 执行与结果集映射每个方法用完就关闭资源Entityentity与表结构对应的 POJO字段名与下划线列名对齐Viewjsp 目录数据渲染与表单提交不出现脚本、不出现连接3. 网上书店的 SQL 落法五张表、事务扣库存与热销榜窗口函数3.1 五张核心表用户、分类、图书、订单、订单明细网上书店的数据库设计通常按“先主数据、再行为数据”的顺序展开。用户表和图书表是主数据订单与订单明细是行为数据。把分类独立成表而不是塞进图书字段是为了后续扩展分类统计时不必改表结构。下面是一份最小建表脚本以 MySQL 为例命名统一用下划线风格和 Java 属性做驼峰映射时不会产生歧义。CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4; USE bookstore; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, email VARCHAR(100), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL ); CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, title VARCHAR(200) NOT NULL, author VARCHAR(100), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, sales INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1, FOREIGN KEY (category_id) REFERENCES t_category(id) ); CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_no VARCHAR(32) NOT NULL UNIQUE, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES t_user(id) ); CREATE TABLE t_order_detail ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES t_order(id), FOREIGN KEY (book_id) REFERENCES t_book(id) );t_order_detail里的 price 是下单时的快照不能在订单生成之后再去关联t_book的最新价格否则订单历史会被后续改价污染。t_book.status用来控制上下架0 表示下架1 表示上架图书列表查询只检索 status1 的记录。库存和销量两个字段拆开维护stock 是当前可销售量sales 是累计卖出量两者更新时机不同后文会单独处理。分类如果不建表而是一长串字符串后边做分类销量报表时会很难写。表名角色主要操作场景t_user主数据注册、登录、个人信息展示页面t_category主数据分类维护、前台分类导航t_book主数据上下架、库存调整、图书列表t_order行为数据下单、用户订单查询t_order_detail行为数据订单明细、热销榜统计3.2 热销榜窗口函数与 explain 看什么图书首页常需要“销量排行”并且排除下架图书。大多数人先写 group by 再 join 主表到“取前 N”这一步就开始手忙脚乱。窗口函数row_number()可以一次性完成排序和编号SQL Server 和 MySQL 8.0 以上的版本都支持。这部分在论文里适合作为“数据分析可用性”的小亮点。SELECT book_id, total_quantity FROM ( SELECT od.book_id, SUM(od.quantity) AS total_quantity, ROW_NUMBER() OVER (ORDER BY SUM(od.quantity) DESC) AS rn FROM t_order_detail od JOIN t_book b ON b.id od.book_id AND b.status 1 GROUP BY od.book_id ) ranked WHERE rn 10;内部查询先按 book_id 聚合销量再用row_number()按照聚合结果降序编号外层过滤rn 10排序、编号、截断在同一个查询里全部完成。JSP 个人信息展示页面里如果要显示用户最近订单也可以用同样思路按user_id分区。执行EXPLAIN检查这条查询时主要看 type、key、rows 三个字段type 至少应达到 refkey 要显示实际命中的索引rows 要远小于全表行数。这个截图放进论文的优化章节比空谈“性能良好”有说服力得多。3.3 扣库存事务把“库存不足”判断写进 UPDATE 的 WHERE下单流程涉及三步写操作向t_order插入订单、向t_order_detail插入明细、扣减t_book.stock。任何一步失败都要回滚所以必须放在同一个事务里。常见错误做法是先 SELECT 检查库存再 UPDATE 扣减这个“先查后改”在并发下一定会超卖两个请求同时读到 stock1各自判断通过然后各扣 1库存变成 -1。正确做法是把库存判断直接写进 UPDATE 的条件。public boolean createOrder(Connection conn, int userId, ListCartItem items) { PreparedStatement psOrder null; PreparedStatement psDetail null; PreparedStatement psStock null; try { conn.setAutoCommit(false); String sqlOrder INSERT INTO t_order(user_id, order_no, total_amount) VALUES(?,?,?); psOrder conn.prepareStatement(sqlOrder, PreparedStatement.RETURN_GENERATED_KEYS); psOrder.setInt(1, userId); psOrder.setString(2, generateOrderNo()); psOrder.setBigDecimal(3, BigDecimal.ZERO); psOrder.executeUpdate(); int orderId 0; try (ResultSet rs psOrder.getGeneratedKeys()) { if (rs.next()) orderId rs.getInt(1); } BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { String sqlStock UPDATE t_book SET stock stock - ? WHERE id ? AND stock ?; psStock conn.prepareStatement(sqlStock); psStock.setInt(1, item.getQuantity()); psStock.setInt(2, item.getBookId()); psStock.setInt(3, item.getQuantity()); if (psStock.executeUpdate() 0) { throw new SQLException(库存不足: bookId item.getBookId()); } String sqlDetail INSERT INTO t_order_detail(order_id, book_id, quantity, price) VALUES(?,?,?,?); psDetail conn.prepareStatement(sqlDetail); psDetail.setInt(1, orderId); psDetail.setInt(2, item.getBookId()); psDetail.setInt(3, item.getQuantity()); psDetail.setBigDecimal(4, item.getPrice()); psDetail.executeUpdate(); total total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } psOrder conn.prepareStatement(UPDATE t_order SET total_amount ? WHERE id ?); psOrder.setBigDecimal(1, total); psOrder.setInt(2, orderId); psOrder.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new RuntimeException(下单失败, e); } finally { closeQuietly(psStock, psDetail, psOrder); } }这段代码的关键点有四个。conn.setAutoCommit(false)把连接切到手动提交UPDATE 语句里stock ?作为条件两个并发事务更新同一行时会被行锁串行化后到的一方影响行数为 0直接抛异常回滚订单号建议用时间戳加随机数生成避免依赖数据库序列finally 里只关闭 PreparedStatement连接归还交给上一层处理。SQL Server 场景下事务逻辑完全相同只是订单号可以用NEWID()如果并发压力不大也可以给 UPDATE 加WITH (UPDLOCK)行锁提示。4. 从源码到 ZIP编译部署、目录排布与排错对照4.1 Tomcat 部署后JSP 编译出的 class 文件在哪里把 zip 里的源码导入 IDEA 或 Eclipse 之后最常见的启动方式有两种在 IDEA 里配置 Tomcat Server用 Artifacts 打出 war exploded 包或者在 Maven 工程根目录执行下面这段命令把 war 扔给外置 Tomcat。mvn clean package -DskipTests cp target/bookstore.war $CATALINA_HOME/webapps/ sh $CATALINA_HOME/bin/startup.sh curl -I http://localhost:8080/bookstore/book/list浏览器首次访问 JSP 时Tomcat 会把它翻译成 Java 文件再编译成 class路径在$CATALINA_HOME/work/Catalina/localhost/bookstore/org/apache/jsp下。所谓“web 项目配置 tomcat 后查看 jsp 编译后的 java 类”就是去这个目录读 class 对应的.java源文件。看完会发现 JSP 里的% %代码块全部落进了_jspService方法里。如果改了 JSP 但浏览器仍是旧页面第一步不是清浏览器缓存而是删掉 work 目录里对应项目文件夹再重启。“当前不会命中断点”的常见原因也是 Tomcat 的 JSP 编译目录没有刷新把 work 目录清掉再启动断点通常就能打住。4.2 zip 交付目录怎么排src、sql、doc、lib 四部分部署跑通之后就要考虑把整个工程重新压成 zip 交出去。交付包里的目录结构应当让接手的人 1 分钟内找到入口下面是我常用的排布方式。bookstore-project/ ├── src/main/java # Java 源码servlet、service、dao、entity、util ├── src/main/webapp # JSP、css、js、WEB-INF/web.xml ├── sql/ │ ├── init.sql # 建库建表与基础数据 │ └── sample-data.sql # 演示用图书与测试账号 ├── doc/ │ ├── 开题报告.docx │ └── 设计论文.docx ├── lib/ # mysql-connector-java 等第三方 jar └── README.md # 环境要求与部署步骤很多 zip 的问题出在数据库初始化上论文里的表名和 init.sql 不一致或者 init.sql 里没有初始账号导致评审老师登录不进去。最简单有效的做法是在 sample-data.sql 里固定放一个用户名为 admin、密码为 123456 的测试账号并在 README.md 第一行写明“导入 sql/init.sql用 admin/123456 登录”。README 不要超过 30 行只写 JDK 版本、Tomcat 版本、数据库版本、初始化顺序和默认账号。lib 目录一定要带上第三方 jar否则换个没有本地 Maven 仓库的机器缺 mysql-connector-java.jar 会让整个工程起不来。4.3 四个高频坑乱码、驱动缺失、脚本重放与安装残留JSPSQL 网上书店源码包在评审机器上跑挂大概率落在下面这张表里。现象可能原因定位方式修复办法页面中文全部是问号Tomcat 请求响应编码不是 UTF-8检查 JSP 头部 pageEncoding 与连接串 characterEncoding加 CharacterEncodingFilter 统一 UTF-8ClassNotFoundException: com.mysql.jdbc.Driverlib 下驱动 jar 缺失或版本过旧jar -tf WEB-INF/lib/mysql-connector-java.jar换 8.x 驱动并随 zip 提供第二次导入 init.sql 报 duplicate key建表脚本没有幂等处理看报错停在哪张表建表前加DROP TABLE IF EXISTSSQL Server 安装程序支持文件残留老版本 SQL Server 卸载不干净控制面板显示安装失败清注册表残项或改用新版 SQL Server讨论 SQL Server 2008 R2 下载与安装的报错时多数情况是机器上残留了旧的安装程序支持文件。为了把网上书店稳定跑通我建议直接用 MySQL 8.0安装简单utf8mb4 支持也更好SQL Server 的语法差异可以留到论文兼容性分析里写不需要在实操环境里硬碰。5. 最后一步把“万能密码”挡在门外并给图书列表加分页5.1 “万能密码”绕过与 PreparedStatement 修复对照socket 注入万能密码绕过是论文答辩时最容易被追问的安全点。网上书店登录页最常见的错误写法是把用户名和密码直接拼进 SQLString sql SELECT * FROM t_user WHERE username username AND password password ;在用户名框里输入admin --就能把后面的密码条件注释掉实现无密码登录输入 or 11同样能绕过验证。修复方式是把字符串拼接改成 PreparedStatement 占位符密码字段不要存明文至少用 BCrypt 或 SHA-256 加盐。String sql SELECT id, username, password FROM t_user WHERE username ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { // 取出哈希后用 BCrypt.matches 比较不要直接等于明文 } } }setString传入参数后单引号、横线和--注释符都会变成普通字符不会再截断 SQL 语句。另一个容易被忽略的注入位是 ORDER BYPreparedStatement 无法给排序字段做占位必须用白名单映射例如把price_desc对应成ORDER BY price DESC不能让排序参数直接拼进 SQL。5.2 分页实现与 explain 检查图书数量一旦过百列表就要做物理分页。在 DAO 层通过页码和页大小计算 offset使用 LIMIT 完成分页int page Integer.parseInt(request.getParameter(page)); int size 10; int offset (page - 1) * size; String countSql SELECT COUNT(*) FROM t_book WHERE status 1; String listSql SELECT id, title, author, price FROM t_book WHERE status 1 ORDER BY id LIMIT ? OFFSET ?;对 listSql 执行 explain重点看 type 与 rows 字段。如果 type 为 ALL说明表上有全表扫描需要在status和id上建联合索引如果 rows 仍然接近全表行数再考虑去掉 ORDER BY 或改成覆盖索引。把 explain 结果截图放进论文的系统优化章节是一个成本极低但效果很好的加分项。提示zip 交付前最后再跑一次 SQL 注入检查在登录框输入 or 11 --确认响应不是管理员后台而是一句“用户名或密码错误”。本文还有配套的精品资源点击获取
返回列表