ARTICLE DETAIL

资讯详情

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

JSP网上购书系统实战:Servlet+JDBC实现购物车与订单事务

JSP网上购书系统实战:Servlet+JDBC实现购物车与订单事务 简介一套基于JSP的网上购书系统毕业设计项目面向计算机相关专业毕业生及Java Web初学者提供完整源代码与配套论文。系统围绕数据关联规则设计涵盖用户管理、图书目录管理、图书信息录入、订单管理、图书浏览查找及购物结账等功能模块并基于TomcatJSP数据库实现包内同时包含WebLogic CMP/RDBMS相关类文件可帮助理解企业级Java持久化层开发。配套论文对个性化页面生成背景、系统结构及工作原理进行了阐述也分析了实现中的特殊性与难点适合作为毕业设计撰写参考。压缩包共1622个文件以Java源码、编译后的class文件、JSP页面、XML配置为主另含SQL数据库脚本、图片素材及论文文档整体约7.36MB目录结构完整便于按模块查阅。已有117人浏览学习可用于快速复现购物系统流程或在此基础上扩展图书推荐、订单统计等关联规则应用。1. 打开那台放了六年的 JSP 网上购书系统之前先想清楚这两件事毕业设计名单里“网上购书系统”出现的频率和“学生管理系统”差不多。如果你拿到的 zip 压缩包里有 webapp 目录、book.sql 和一篇两三万字的论文技术栈大概率是同一条JSP 输出页面Servlet 接收请求JDBC 连接 MySQL最后打成 war 放进 Tomcat 启动。这套组合看起来有些年头但它把“浏览器请求如何一步步变成 SQL再作为 HTML 返回”展示得比 Spring Boot 更直白。对要完成毕业设计的本科生、刚接手课程的实习生以及想在简历里写“熟悉 Servlet/JSP”的人这类“源代码论文”的项目都是性价比最高的练手题材。动手之前先确认三件事JDK 与 Tomcat 的版本是否兼容数据库脚本能否一次导入成功论文里的架构图是不是和你实际跑出来的页面一致。这三件事决定了你是花一天跑通还是花几天改环境。2. 选择 JSP 做网上购书系统的真实理由分层边界、Servlet 生命周期与运行环境2.1 为什么用 JSPServlet也要把 MVC 的边界划清楚写论文和答辩时最常说的一句话是“本系统采用 MVC 架构”。这意味着你要能指给评委看哪一层是 Model哪一层是 View哪一层是 Controller。在 JSP 网上购书系统里实际分工一般是 JSP 承担 ViewServlet 承担 ControllerDAO 和 Service 承担 Model。如果你见过把SELECT * FROM t_book直接写在 JSP 页面里的大作业就会明白边界模糊会让整个项目没法维护。购书系统的典型流程是用户点击“加入购物车”请求被CartServlet接收Servlet 调用CartService.addItem()Service 通过BookDao读写数据库结果存进 Session最后forward到cart.jsp渲染页面。这个流程里每一步都换一个组件答辩时被问“为什么这么分层”你才有话可讲而不只是说“老师要求用三层架构”。下面这张表可以放进论文“系统选型”一节用来交代为什么用 JSPServlet 而不是一步到位用 Spring Boot。写的时候重点不是论证谁更好而是说明基于 JSP 的毕业设计在最小可控的代码量里保留了请求到响应的完整路径。对比项JSP Servlet JDBCSSMSpring MVC MyBatisSpring Boot学习曲线平缓适合起步陡需要理解容器与代理平缓但底层被封装请求链路可见性高Servlet 与 JSP 明确分工中注解掩盖了细节低自动配置太多手写 SQL 机会多适合练习数据库多但被 MyBatis 包裹少通常走 JPA答辩展示点Servlet 生命周期、Session事务与 AOP、依赖注入自动装配、切面、监控适合题目的技术匹配度高中中2.2 搭建运行环境的最小清单JDK、Tomcat 和 Maven 配置下载源码后第一件事不是打开 IDEA而是确认三件套版本。JSP 网上购书系统最常见的部署环境是 JDK 8 或 JDK 11 搭配 Tomcat 8.5/9.0。Tomcat 10 之后把javax.servlet换成了jakarta.servlet如果你用的源码是基于老包名写的放进 Tomcat 10 几乎必报ClassNotFoundException: javax.servlet...。判断方式很简单打开源码里的web.xml或pom.xml看里面是javax.servlet-api还是jakarta.servlet-api。JDK 可以直接用压缩包版解压后配置JAVA_HOME比安装包少一些写注册表的行为换版本也方便。Maven 工程里最常用的依赖配置如下properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties dependencies !-- 编译期使用Tomcat 会提供该 API -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjsp-api/artifactId version2.0/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies注意两点。第一Servlet API 和 JSP API 的依赖必须用provided范围否则打 war 包时会把你本地 jar 里的javax.servlet类打进WEB-INF/lib和 Tomcat 自带的类冲突启动时经常出现java.lang.LinkageError。第二jstl依赖只管编译运行时的 taglib 由 Tomcat 提供或由你额外放入 lib。页面上使用c:forEach之前要先在 JSP 头部写% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %。2.2.1 用 Maven 打包和直接跑 Tomcat 的两种方式如果项目本身带pom.xml推荐命令行处理避开 IDE 缓存带来的“当前不会命中断点”之类的调试怪现象mvn clean package -DskipTests生成的目标文件是target/bookshop.war。把 war 文件复制到 Tomcat 的webapps目录启动 Tomcat 后它会自动解压成webapps/bookshop文件夹浏览器访问的是http://localhost:8080/bookshop/index.jsp而不是直接访问index.jsp文件本身。如果项目里没有 Maven只是手工组织的 Web 工程也可以直接在 IDEA 里配置 Tomcat Server Local将 deployment 指向源码中的 webapp 目录。两者没有优劣但对要写部署说明的论文来说Maven 的命令行步骤更容易被评委复现。注意如果源码是基于javax.servlet写的不要直接使用 Tomcat 10启动会报错。这不是代码问题是规范升级带来的兼容问题。Tomcat 9 和 Java 8 的组合对这类项目最稳。2.3 JSP 请求里三个内置对象的差别request、session、application购书系统里最容易被忽略的是这三个内置对象各自的存活时间。request一次请求结束就没了适合放查询参数和当前页要展示的数据session默认存活三十分钟适合放登录用户和购物车application全局只有一个适合放网站配置。把购物车放进session是最常见的做法但有一个常见错误购物车明明只有一个MapbookId, quantity有些同学却在每次“加入购物车”时查一次数据库换新对象再放回 session导致多窗口刷新后数据丢失。正确做法是启动时先取 session 里的购物车没有再创建之后一直复用同一个对象等提交订单时再把购物车内容整体清空。这个差距答辩追问时三句话就能问出来。3. 网上购书系统的数据库设计与订单事务建表、索引与防超卖3.1 六张表把用户、图书、购物车和订单串起来网上购书系统拿到手后别急着改页面先把book.sql里的表关系理一遍。我一般建议最少要有这六张表它们对应着“谁在买、买什么、放哪儿、订单是什么、订单里有什么、按什么分类”下面这张表可以直接抄进论文的数据库设计部分表名关键字段关联关系职责t_useruser_id, username, password, nickname被 t_cart_item、t_order 引用用户与登录t_bookbook_id, book_name, author, price, stock被购物车、订单项引用图书信息与库存t_categorycategory_id, category_namet_book.category_id 引用图书分类t_cart_itemcart_id, user_id, book_id, quantityuser_id→t_userbook_id→t_book临时购物车t_orderorder_id, order_no, user_id, total_amount, statususer_id→t_user订单头t_order_itemitem_id, order_id, book_id, price, quantityorder_id→t_order订单明细快照这里要说清两个设计取舍。第一t_order_item一定要把下单时的单价和书名冗余进来不能只存book_id因为图书价格以后可能调整订单明细要保留下单那一刻的“快照”。第二t_cart_item用数据库表而不是纯 Session 保存好处是用户换浏览器购物车还在答辩时也能讲出“数据持久化”这个词缺点是每次都要查库页面交互稍微慢一点我自己的做法是 Session 里缓存一份提交订单时以数据库里的记录为准。3.2 初始化 SQL 的常见写法与三个必调参数下面这段 SQL 是拿来即用的最小初始化脚本字符集统一用utf8mb4。很多 zip 里的book.sql只建表不写注释字符集还是老的latin1导入后中文全部乱码所以我习惯先把这段覆盖进去CREATE DATABASE IF NOT EXISTS bookshop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bookshop; CREATE TABLE t_book ( book_id INT PRIMARY KEY AUTO_INCREMENT, category_id INT, book_name VARCHAR(128) NOT NULL, author VARCHAR(64), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, cover_url VARCHAR(255), description TEXT, KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_order ( order_id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_order_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, book_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO t_book (category_id, book_name, author, price, stock) VALUES (1, Java 核心技术, Cay S. Horstmann, 99.00, 50), (1, 深入理解计算机系统, Randal E. Bryant, 89.00, 30);SQL 里三个参数值得在论文里展开AUTO_INCREMENT决定主键生成方式JSP 项目里不要用 UUID 做主键订单号需要人工生成但主键保持自增DECIMAL(10,2)存金额而不是 FLOATFLOAT 会出现0.10.2不等于0.3的问题DEFAULT CHARSETutf8mb4必须建表时带上建完再改字符集需要ALTER TABLE ... CONVERT TO CHARACTER SET而且可能因为旧数据报Data too long。t_order和t_order_item之间要有order_id外键但在演示项目里外键约束可以只描述在论文里避免删除图书时被外键挡住报错。提示运行插入语句前先用SHOW VARIABLES LIKE character_set_server确认 MySQL 服务端默认字符集避免建库语句执行成功但排序规则仍是旧的。3.3 生成订单时用同一个 Connection 保证事务网上购书系统最容易在“提交订单”这一步出 bug因为它涉及三次写操作插入订单头、插入订单明细、扣减库存。如果三次操作中途有一半失败数据库就会留下一个没有明细的空订单或者库存减了订单没生成。正确写法是把这三步放进同一个Connection并且setAutoCommit(false)全部成功才commit()任何一步失败都rollback()。下面是在 Service 层写订单的事务骨架public int createOrder(int userId, ListCartItem items) throws SQLException { Connection conn dataSource.getConnection(); try { conn.setAutoCommit(false); // 第一步插入订单头 String orderSql INSERT INTO t_order(order_no, user_id, total_amount, status) VALUES (?, ?, ?, 0); PreparedStatement ps conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, generateOrderNo()); ps.setInt(2, userId); BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { total total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } ps.setBigDecimal(3, total); if (ps.executeUpdate() ! 1) { throw new SQLException(插入订单头失败); } int orderId getGeneratedId(ps); // 拿到数据库自增主键 // 第二步插入订单明细同时扣减库存并检查库存 for (CartItem item : items) { PreparedStatement itemPs conn.prepareStatement( INSERT INTO t_order_item(order_id, book_id, price, quantity) VALUES (?, ?, ?, ?)); itemPs.setInt(1, orderId); itemPs.setInt(2, item.getBookId()); itemPs.setBigDecimal(3, item.getPrice()); itemPs.setInt(4, item.getQuantity()); itemPs.executeUpdate(); PreparedStatement stockPs conn.prepareStatement( UPDATE t_book SET stock stock - ? WHERE book_id ? AND stock ?); stockPs.setInt(1, item.getQuantity()); stockPs.setInt(2, item.getBookId()); stockPs.setInt(3, item.getQuantity()); int rows stockPs.executeUpdate(); if (rows 0) { throw new SQLException(库存不足book_id item.getBookId()); } } conn.commit(); return orderId; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }这段代码核心是UPDATE ... WHERE stock ?这一句它利用数据库行锁实现“条件更新”只有当前库存不小于购买数量时才会更新成功executeUpdate()返回 1否则返回 0 直接抛异常回滚。这比先SELECT stock再在 Java 里判断要安全因为它把判断和扣减合并成一个原子操作避免两个并发请求同时读到stock1然后各自买 1 本最后却都成功的问题也就是“超卖”。setAutoCommit(true)要放在finally里恢复否则连接归还到连接池后下一个请求拿到的连接仍是手动提交状态会导致后续所有操作都不自动提交排查起来非常隐蔽。4. 把 JSP 页面层做对个人信息展示、分页、离开提示与表单校验4.1 JSP 个人信息展示页面用 session 拿数据页面里不写业务搜索“jsp个人信息展示页面”的求助多半是不知道从哪拿当前用户。登录成功之后把用户对象放进session这是出现在几乎每个 JSP 项目里的经典做法。页面展示时只在 JSP 里读取不要再查数据库。写法有两种早期项目常用 JSP 脚本片段现代一点的写法推荐 EL 表达式加 JSTL% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % div classprofile-card p当前用户c:out value${sessionScope.username}//p p昵称c:out value${sessionScope.nickname}//p p最近登录时间c:out value${sessionScope.lastLoginTime}//p /divsessionScope.username表示从 session 作用域读取属性c:out用来输出防止用户名字里包含script时被浏览器当作 HTML 执行。为什么推荐 EL 而不推荐% session.getAttribute(username) %这种脚本片段因为 JSP 的隐式对象session虽然好用但脚本片段会让页面中混杂大量 Java 代码后期维护时 JSP 引擎的编译错误定位非常麻烦而且项目规范一点的团队会检查 JSP 里不出现%。如果遇到“页面显示 null”的情况先检查登录成功后是不是session.setAttribute(username, user.getUsername())的 key 和页面读取的 key 不一致其次检查是不是访问的页面和登录时所在的 webapp 上下文不同http://localhost:8080/bookshop/下的 session 不会跑到http://localhost:8080/下。4.2 分页查询的两个必调参数currentPage 和 pageSize网上购书系统的图书列表一般几十上百条一次全查出来会让首页变慢。JSP 里最常见的分页是基于LIMIT实现物理分页而不是先查全部再在 Java 里subList。请求参数里只需要两个值当前页码page每页条数pageSize页面上展示“第 1 / 8 页”这样的导航。Servlet 里接收参数时一定要做一次容错否则用户输入非数字参数会导致NumberFormatExceptionint currentPage 1; int pageSize 8; String pageParam request.getParameter(page); if (pageParam ! null pageParam.matches(\\d)) { currentPage Math.max(1, Integer.parseInt(pageParam)); } String sizeParam request.getParameter(pageSize); if (sizeParam ! null sizeParam.matches(\\d)) { pageSize Math.min(20, Integer.parseInt(sizeParam)); } int total bookDao.countBooks(); int totalPages (total pageSize - 1) / pageSize; ListBook books bookDao.findPage((currentPage - 1) * pageSize, pageSize); request.setAttribute(books, books); request.setAttribute(currentPage, currentPage); request.setAttribute(totalPages, totalPages); request.getRequestDispatcher(/book_list.jsp).forward(request, response);注意LIMIT ?, ?在 JDBC 里的两个占位符它们的含义是“跳过 offset 行取 pageSize 行”offset 是(currentPage - 1) * pageSize第一页的 offset 是 0这一行代码很容易写错。页面底部生成页码时要注意“当前页高亮”和“总页数不超过 range”这两个点例如最多显示 10 个页码按钮避免三百页时渲染出三百个按钮。分页查询里最容易出现的问题是统计总数用了SELECT * FROM t_book再list.size()这等于把全表数据又加载了一次正确写法是SELECT COUNT(*) FROM t_book。4.3 屏蔽 JSP 离开页面提示beforeunload 不要乱加“屏蔽jsp离开页面提示”是常见的页面体验问题典型场景是购物车页面加了window.onbeforeunload function() { return 确定要离开吗; }然后每次刷新、跳转、提交订单都弹窗最终被测试归为 bug。现代浏览器出于阻止恶意弹窗的考虑已经不再支持在这个事件里返回自定义字符串弹不弹窗只取决于你有没有调用preventDefault()或设置returnValue。真正合理的使用方式是先声明一个“是否允许离开”的标记只有表单被修改过才阻止离开let dirty false; document.getElementById(quantityInput).addEventListener(change, function () { dirty true; }); window.addEventListener(beforeunload, function (e) { if (!dirty) return; // 没有修改不弹窗 e.preventDefault(); e.returnValue ; }); document.getElementById(submitOrderBtn).addEventListener(click, function () { dirty false; // 提交订单时放行 });returnValue 是给老版本浏览器看的兼容写法新浏览器只认事件是否被取消所以这个空字符串赋值不能省。如果只是想让“没有改动的页面不弹窗”那最正确的做法就是不写任何beforeunload监听默认就干净。网上购书系统里这个弹窗只在编辑收货地址、修改购物车数量这种有“未保存改动”语义的页面里才有存在价值单纯展示型页面加了只会增加跳出率。在 Safari 里这个事件还有特殊表现赋值returnValue不一定弹窗真机上要实际点一遍离开操作来验证。4.4 表单校验写在 JS 和写在 Servlet 各一次网上购书系统的注册、下单表单都需要校验常见的错误是只在前端用 JS 校验后端 Servlet 里直接getParameter拿去用。前端 JS 负责用户体验后端才是安全边界恶意用户可以绕过页面直接构造 HTTP 请求所以两层都要写。前端这层按“最少代码挡住最常见错误”来写function validateOrderForm(form) { const receiver form.receiver.value.trim(); const phone form.phone.value.trim(); const email form.email.value.trim(); if (!receiver) { alert(收货人姓名不能为空); return false; } if (!/^1[3-9]\d{9}$/.test(phone)) { alert(请输入正确的手机号); return false; } if (email !/^[\w.-][\w-]\.[\w.]$/.test(email)) { alert(请输入正确的邮箱); return false; } return true; }正则匹配手机号只做格式检查不能保证号码真实存在后端也不能完全依赖这个正则。Servlet 侧校验的最小要求是对quantity、price、userId这些数字字段做字符串到整数的安全转换捕获NumberFormatException并且对文本字段做长度限制比如receiver最长 64 个字符防止往VARCHAR(64)字段塞入超长内容触发数据库报错。金额这类字段尤其要在后端重新计算不能信任前端传来的totalAmount否则修改请求里的金额就能以任意价格下单。这个点讲清楚答辩时能从“安全问题”角度跟评委说你不只是做了页面。5. 部署与整改JSP 编译后的 class、会话安全、zip 包的交付细节5.1 JSP 编译后的 class 存在哪以及“当前不会命中断点”的解法Tomcat 启动后会在work/Catalina/localhost目录下为每个应用生成编译产物JSP 第一次被访问时会由 Jasper 引擎编译成.java和.class文件。网上购书系统改完 JSP 页面不生效最常见原因是浏览器缓存旧页面其次是 Tomcat work 目录里残留了旧的 class。手动清掉 work 下的对应应用目录再重启 Tomcat 即可。这个目录的另一个用途是排查 JSP 语法错误有时控制台不报错但页面 500去 work 目录看生成的.java源文件就能定位是哪一行 Java 语法出问题。“当前不会命中断点”在 JSP 项目里的原因一般不是代码问题而是调试器没有识别到这个类的源文件。在 IDEA 里对 JSP 断点实际上断在的是编译后的 Servlet 类如果你的源码里没有这个类IDE 会提示源码与版本不符需要重置缓存或重启调试会话。对纯 JSP 页面来说最可靠的调试方式是用System.out.println或日志输出 request 参数和 session 属性先在页面顶部显示临时调试信息定位问题后删掉比纠结断点更省时间。5.2 网上购书系统上线前要检查的四个会话与安全参数JSP 项目的安全薄弱点集中在会话管理和数据库访问上。我在答辩前通常会给代码过一遍这几个位置改完再把关键参数写进论文配置项位置或代码写法推荐值说明会话超时web.xml的session-config30 分钟值太小购物车频繁丢失太大有会话劫持风险Cookie HttpOnlyweb.xml加cookie-confighttp-onlytrue/http-only/cookie-configtrue防止 JS 读取 sessionIdJDBC 账号数据库连接信息不用 root单独创建bookshop账号并只授予该库权限密码存储用户注册/登录模块BCrypt不要用 MD5彩虹表一查就破web.xml里会话超时的写法是session-configsession-timeout30/session-timeout/session-config这里的单位是分钟。如果项目里用的是 Tomcat 8.5 以上的容器Cookie 的安全属性也可以通过 context.xml 里的CookieProcessor配置两种方式都能实现 HttpOnly选一种保持一致即可。JDBC 连接串里避免出现明文密码落进代码仓库的方法是把db.properties排除在提交文件外并在 README 里写清楚创建数据库账号的 SQL这样论文的“部署说明”也顺带有了内容。MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver老代码里com.mysql.jdbc.Driver也能用但会在日志里输出 deprecation 提示连接串末尾要加useSSLfalseserverTimezoneAsia/Shanghai否则 JDK 时区不一致会在读写时间字段时出问题。5.3 论文包里“源代码 论文”的整理方式以及 zip 损坏时的正确处理拿到或交付这类项目时目录结构直接决定读代码的人会不会发火。我见过最乱的情况是六七个文件平铺在 zip 根目录连哪个是 SQL、哪个是部署说明都分不清。如果这份代码最终要交给导师或作为开源参考推荐至少保持这样的层级bookshop-online/ ├── 01-src/ │ ├── bookshop/ # Maven 工程根目录 │ └── sql/bookshop.sql ├── 02-docs/ │ ├── 论文.docx │ └── 答辩演示.pptx └── 03-deploy/ └── README.md # 环境、账号、测试数据说明如果你下载到的 zip 提示已损坏或需要密码不要急着去找“zip 压缩包密码移除工具”这类批处理软件先换一个规范一点的解压软件重试确认是网络传输导致文件截断还是确实被加密。被加密的压缩包没有可靠口令基本无解正确做法是联系打包人重新生成或者核对哈希值确认文件完整试图用来路不明的一次性工具只会换回一个新的安全风险。论文部分需要注意“源代码”与“论文”的一致性很多 zip 里论文画的设计架构图是三层结构代码里却是所有 SQL 全写在 JSP 页面里这种不一致在答辩时会被直接指出。建议把论文里“系统实现”章节的每个小节标题和源码里的包名、方法名对应起来比如“图书列表的模糊查询”对应BookDao.searchByName()这样评委翻代码时能对上号。交付前把这三项过一遍bookshop.sql导入后中文是否正常、Tomcatwork目录清理后能否直接跑通、论文里的运行环境小节与 README 是否完全一致。这三项没问题剩下的只是答辩时把购物车和订单事务这两条链路讲顺。本文还有配套的精品资源点击获取
返回列表