JSP+SQL网上书店项目实战:从环境搭建到购物车事务实现
简介一套面向高校计算机专业毕设与课设的JSPSQL网上书店完整项目结合了JSP动态网页、SQL数据库及Java后端开发适合需要完成同类课题或系统实践Web开发的学生参考。压缩包共436个文件体量约3.25MB内容以jsp页面、java源文件、class编译文件为核心辅以gif与jpg展示系统界面和操作流程doc为论文文档css/htm用于前端样式同时附带mdf/ldf数据库文件便于直接部署与二次开发。已有120人学习下载反映出其在课程设计与毕业设计中的实用价值。项目覆盖图书管理、用户登录、订单处理等典型业务模块可帮助读者理解MVC分层、DAO数据访问及数据库表关联等关键知识点论文部分则从背景、系统设计到测试结果给出完整叙述为动手实现和撰写文档提供一份扎实的参考。通过分析源码与调试运行能够串联起前端展示、业务逻辑与数据持久化的完整开发链路。1. 一千个毕设题目里为什么这个 JSPSQL 网上书店最值得你动手跑一遍每年到了四月和十月总会有人在技术群里丢出一句“JSPSQL网上书店设计源代码论文文档资料”紧接着就是一堆追问这玩意儿现在还有人用吗答辩能过吗环境怎么配我给你的回答很直接JSP 的确是老技术但它恰好是理解 Java Web 请求-响应模型最便宜的入场券。你花两小时把一个基于 JSPServletJDBC 的书店跑起来比你看十篇 Spring Boot 教程更能理解 session、cookie、过滤器、连接池这些概念到底在解决什么问题。这个项目包的典型内容是一套完整的网上书店源码外加一份可以改改封面就提交的论文文档。适合三类人一是 Java Web 课设/毕设选了电商方向的学生二是想快速搭一个“能演示”的 CRUD 原型交给上级看的新人三是打算把老项目翻新成 SSM 或 Spring Boot 版本练手的开发者。这篇文章不帮你做选择题而是把“下载下来之后到底怎么跑通、怎么改、怎么避开答辩现场翻车”这件事讲透包括源码结构、数据库初始化、会话购物车实现、订单事务处理和部署打包这几个核心环节。2. 把 JSPSQL 网上书店拆开看它到底是什么架构以及为什么能活到今天很多人一看到 JSP 就觉得是化石但它其实和你每天在用的前后端分离思想是同一套逻辑只是分工方式不同。网上书店这类课设项目本质上是一个教科书级的 Java Web 三层架构浏览器发起请求Tomcat 把请求交给 JSP 或 Servlet 处理Servlet 里通过 JDBC 操作 MySQL 数据库再把结果塞回 JSP 页面渲染成 HTML 返回给用户。这套链路短、清晰、每一步都能打断点调试比一上来就上 Spring BootMyBatis-Plus 的黑匣子好理解得多。2.1 源码目录结构拿到压缩包后先认清这五个文件夹最常见的源码布局是 Eclipse 或 IDEA 的 Web 项目结构解压后你会看到 src、WebContent或 webapp、database、doc、lib 这几个目录。src 下面按照 com.xxx.bean、com.xxx.dao、com.xxx.servlet、com.xxx.filter 分包bean 放实体类对应数据库表字段dao 放 JDBC 操作封装servlet 处理请求分发filter 统一设置编码和登录校验。WebContent 下是 JSP 页面、CSS/JS 静态资源以及 WEB-INF/web.xml注意 web.xml 是 Servlet 3.0 之前的项目标配新一点的源码可能改用注解。database 目录放的是 bookstore.sql 这种初始化脚本doc 目录放着任务书、开题报告、论文正文和答辩 PPT 模板。拿到手第一步不是双击运行而是逐个打开看一遍确认数据库脚本里有建库语句和样例数据别等到启动时才发现脚本只导出了表结构没有数据页面上一片空白。2.2 为什么老源码偏好 SQL 而不是 ORMJDBC 的三种连库姿势对比这个项目里的 dao 层基本是手写 JDBC因为它要展示你“懂底层”。常见做法是写一个 DBUtil 工具类里面用静态代码块加载驱动提供一个获取 Connection 的方法。三种连库方式里最经典的是DriverManager.getConnection(url, user, password)每个请求都新建连接课设阶段够用但并发一高就会卡死进阶一点是使用 Apache DBCP 连接池通过配置文件维护最小连接数和最大连接数再往上走是 c3p0配置略繁琐但性能更好。网上书店这种访问量级的课设DriverManager 加一个单例就能演示。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/bookstore?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps ! null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }这段代码有四个参数你必须根据自己的环境改否则翻车率百分百。URL 里的bookstore是数据库名要和 SQL 脚本建的库一致characterEncodingutf8如果漏了中文书名查出来全变成问号答辩时当场社死serverTimezoneAsia/Shanghai是 MySQL 8.x 的强制要求不加就报The server time zone value的错useSSLfalse是避免本地连接时 SSL 握手警告刷屏。驱动类名也要注意MySQL 5.x 用com.mysql.jdbc.Driver8.x 之后改成com.mysql.cj.jdbc.Driver写错直接 ClassNotFound。2.3 页面渲染与请求流转一本书从点击到加入购物车经历了什么书店首页有一个经典的查询链路。用户打开 index.jsp浏览器发 GET 请求到bookServlet?actionlistServlet 里调用BookDao.findAll()执行SELECT * FROM book拿到 List 之后用request.setAttribute(bookList, list)存进请求域再forward到 book_list.jsp。JSP 页面里用c:forEach标签遍历集合渲染成表格。这套流程在你学 Spring MVC 时会原封不动再见一次只是RequestMapping注解替代了 web.xml 里的servlet-mappingModelAndView 替代了 request 域。需要注意一个隐患如果源码里用的是% %脚本片段在页面里拼 HTML且没有任何 JSTL/EL 表达式说明这套源码的写作年代偏早你在答辩时最好能说清楚“JSP 里不该写业务逻辑”这个演进过程。很多老师专抓这个点你能主动指出并说自己用 EL 表达式替代了 scriptlet这是加分项。3. 跑通最小可演示版本环境、建库、启动的三步落地法我这里说的“跑通”不是把整个项目完整跑出所有功能而是先拿到一个能打开首页、能看到商品列表的最小闭环。很多人的失败不是因为代码有问题而是因为环境变量、JDK 版本、Tomcat 版本和项目出生年代不匹配。常见做法是JDK 8 Tomcat 8.5 MySQL 5.7 或 8.0这个组合兼容性最稳既不会碰到 Spring 5 需要 JDK 8 的底线问题也不会触发 JSP 2.3 的新特性兼容性报错。3.1 环境变量和 Tomcat 配置三个最容易出错的地方先把 JAVA_HOME 配好指向你的 JDK 安装目录然后在 Path 里加%JAVA_HOME%\bin。Tomcat 不用安装解压到纯英文路径即可比如 D:\apache-tomcat-8.5.100千万别放中文目录或带空格的目录否则 Catalina 启动脚本十有八九报错。到 conf/server.xml 里看默认端口 8080 是否被占用被占了就改成 8081改Connector port8080这一行。最后确认CATALINA_HOME系统变量指向 Tomcat 根目录有些源码里的 build 脚本依赖这个变量。# 验证 JDK 和 Tomcat 环境 java -version javac -version echo %JAVA_HOME% echo %CATALINA_HOME%如果你在 cmd 里执行java -version能显示 1.8.x但javac报“不是内部或外部命令”说明只配了 JRE 没配 JDK或者 Path 里 JRE 路径排在 JDK 前面。这种情况去控制面板把 Java 相关的环境变量理一遍重点看 Path 里是否有多余的 Oracle 公共 JRE 条目。3.2 数据库初始化直接执行 SQL 脚本最常见的报错解法打开 SQL 脚本注意看第一行是不是CREATE DATABASE IF NOT EXISTS bookstore DEFAULT CHARACTER SET utf8;。如果有直接用 Navicat 或 MySQL 命令行执行整个脚本就行如果没有建库语句你要手动先建库再source导入。用命令行执行时先cd到 sql 文件所在目录再mysql -u root -p bookstore bookstore.sql这样不会因为路径里有中文或空格导致解析失败。CREATE DATABASE IF NOT EXISTS bookstore DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; USE bookstore; CREATE TABLE book ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) DEFAULT NULL, author VARCHAR(50) DEFAULT NULL, publisher VARCHAR(100) DEFAULT NULL, category VARCHAR(50) DEFAULT NULL, stock INT DEFAULT 100, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8; INSERT INTO book (name, price, author, publisher, category) VALUES (Java核心技术, 99.00, Cay S. Horstmann, 机械工业出版社, 编程), (深入理解计算机系统, 139.00, Randal E. Bryant, 机械工业出版社, 计算机), (MySQL必知必会, 49.50, Ben Forta, 人民邮电出版社, 数据库);注意ENGINEInnoDB这个参数网上书店的订单表和外键约束必须靠它才能持久化和维护一致性MyISAM 不支持事务会导致下单成功后库存扣了但订单记录丢失的情况。脚本执行完用SELECT * FROM book;验证三条样例数据都在并且中文没有乱码。如果乱码多半是建库时字符集不是 utf8或者 MySQL 客户端的 default-character-set 没设对。3.3 IDEA 里导入项目的三种姿势与运行配置Eclipse 时代的老项目导入 IDEA 有三种路径选哪种取决于压缩包里有没有.project和.classpath文件。有.project的直接用 “Open” 选目录IDEA 能识别成 Eclipse 项目并转换没有的话用 “New → Project from Existing Sources”选择 Web 目录作为根目录让 IDEA 自动检测最稳妥的是建一个空的 Java Enterprise 项目手动把源码目录标记为 Sources Root把 WebContent 标记为 Web Resources。运行前必须确认 Project Structure 里的 Artifacts 配置然后注意 Building 时的 Output Directory 是否指向了 tomcat 的部署目录否则 Tomcat 起来后访问 404。然后加一个本地 Tomcat Server 运行配置Deployment 标签页把 war exploded 包部署进去Application context 设为/bookstore这样访问路径就是http://localhost:8080/bookstore/index.jsp。如果你启动后只见 Tomcat 小猫页没有你的项目八成是 Application context 没设置或者 Deploy 没有勾选。4. 购物车与订单落库从 Session 到事务的完整链路附可复现代码这一章是整个项目的核心也是论文里“系统设计”部分最应该写透的内容。网上书店的购买流程是教科书级的“会话状态事务一致性”案例用户把书加入购物车数据暂存 Session点击结算时一次性写入数据库并扣减库存这两个操作必须同时成功或同时失败。很多课设源码的购物车只用 Session 实现这本身没错但它无法跨浏览器会话保留而且结算逻辑稍微一乱就会出现库存扣成负数这种尴尬状况。我们先说怎么实现一个够用的 Session 购物车再说怎么给结算逻辑加上事务保障。4.1 购物车的 CartItem 与 Cart 实体为什么用 Map 而不是 ListCartItem 绑定 bookId 和数量Cart 作为载体它内部用一个MapInteger, CartItem来保存数据。用 Map 比用 List 的好处是 add 操作天然去重同一本书重复加入时只更新数量和总价不需要遍历查找。这是网上书店项目里一个很小的设计决策但答辩时老师经常会问“为什么这里用 Map”你要能答出“查询复杂度从 O(n) 降到 O(1)”和“HashMap 适合按键取值购物车正是这种场景”。public class Cart { private MapInteger, CartItem items new HashMap(); private double totalPrice; public void addBook(Book book, int quantity) { int bookId book.getId(); if (items.containsKey(bookId)) { CartItem item items.get(bookId); item.setQuantity(item.getQuantity() quantity); } else { items.put(bookId, new CartItem(book, quantity)); } recalculateTotal(); } public void removeBook(int bookId) { items.remove(bookId); recalculateTotal(); } private void recalculateTotal() { this.totalPrice items.values().stream() .mapToDouble(item - item.getBook().getPrice() * item.getQuantity()) .sum(); } }recalculateTotal 这个方法在每次 add 和 remove 后都会执行这样页面读取 totalPrice 时不需要临时计算。这里有一个容易忽略的点CartItem里存的是 Book 对象而非 bookId原因是页面渲染购物车列表时需要书名和单价如果只存 Id每次渲染都要重新查库增加无谓的数据库压力。这套设计在 Session 里存放的是 Cart 对象Tomcat 默认的 Session 是内存存储重启服务后购物车丢失这是正常现象不是 bug你答辩时主动说出“Session 默认非持久化重启后购物车清空”反而说明你理解了这个机制。4.2 结算下单的 Service 方法如何用 Connection 手动控制事务下单包含两个写操作往 order 表插入一条订单、往 order_item 表插入 N 条明细、把 book 表的 stock 减去对应数量。如果不用事务第二步失败时会出现“支付了但库存没扣”或“库存扣了但没订单”的数据不一致。JDBC 的事务 API 极其简单核心就是conn.setAutoCommit(false)和conn.commit()关键是要让三条数据库操作共用同一个 Connection。public boolean createOrder(Order order, ListOrderItem items) { Connection conn null; PreparedStatement psOrder null; PreparedStatement psItem null; PreparedStatement psStock null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交事务从这里开始 psOrder conn.prepareStatement( INSERT INTO orders(user_id, total_price, create_time) VALUES(?,?,NOW()), Statement.RETURN_GENERATED_KEYS); psOrder.setInt(1, order.getUserId()); psOrder.setDouble(2, order.getTotalPrice()); psOrder.executeUpdate(); ResultSet keys psOrder.getGeneratedKeys(); int orderId -1; if (keys.next()) { orderId keys.getInt(1); } psItem conn.prepareStatement( INSERT INTO order_item(order_id, book_id, quantity, price) VALUES(?,?,?,?)); psStock conn.prepareStatement( UPDATE book SET stock stock - ? WHERE id ? AND stock ?); for (OrderItem item : items) { psItem.setInt(1, orderId); psItem.setInt(2, item.getBookId()); psItem.setInt(3, item.getQuantity()); psItem.setDouble(4, item.getPrice()); psItem.executeUpdate(); psStock.setInt(1, item.getQuantity()); psStock.setInt(2, item.getBookId()); psStock.setInt(3, item.getQuantity()); int rows psStock.executeUpdate(); if (rows 0) { throw new SQLException(库存不足回滚); } } conn.commit(); // 全部成功提交事务 return true; } catch (SQLException e) { e.printStackTrace(); try { if (conn ! null) { conn.rollback(); // 任何一步失败回滚到事务开始前 } } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { try { if (conn ! null) { conn.setAutoCommit(true); // 恢复自动提交避免连接池复用出错 } } catch (SQLException e) { e.printStackTrace(); } DBUtil.close(conn, psOrder, null); DBUtil.close(null, psItem, null); DBUtil.close(null, psStock, null); } }这段代码里最值得你答辩时讲的三处细节一是Statement.RETURN_GENERATED_KEYS它让你拿到自增主键 orderId否则你无法建立订单和明细的父子关系二是UPDATE ... WHERE stock ?这个条件它在 SQL 层面做了库存校验比先 SELECT 再比较的写法更安全因为两个并发请求同时进来时 SELECT 会读到相同的库存值而 UPDATE 的条件判断是原子操作三是 finally 块里把 autoCommit 恢复成 true如果你用了连接池而没恢复这个连接下次被取用时可能还处于事务未提交状态。这套写法就是 Spring 的Transactional在 JDBC 层的手工实现你把这个讲明白了老师就不觉得你只是会抄代码。4.3 SQL 注入与中文字符集网上书店最常见的两个安全/显示问题课设源码里的登录功能十有八九是SELECT * FROM user WHERE usernamename AND passwordpwd这是全项目最容易挨批的地方。你只需要在用户名输入框里输入admin or 11密码随手打几个字符就能绕过登录。这不是危言耸听而是 JSP 课设项目里几乎必然存在的安全隐患。解决方式很简单全部换成 PreparedStatement 占位符参数绑定数据库驱动会自动转义特殊字符。你可以在论文的“系统测试”章节里专门写一条 SQL 注入测试用例记录修复前后的不同表现这比写十条普通功能测试更像认真做了安全设计。// 安全的写法 PreparedStatement ps conn.prepareStatement( SELECT * FROM user WHERE username ? AND password ?); ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery();至于中文字符集问题你要同时检查三个链路再说结论数据库表字符集是 utf8、JDBC URL 带 characterEncodingutf8、每个 JSP 页面第一行有 contentType 指定 UTF-8。有时候数据库和数据源都配对了但页面中文还是乱码那就是 Tomcat 的URIEncoding没设置去 server.xml 里的 Connector 加URIEncodingUTF-8。还有一批老源码用的是 ISO-8859-1 编码写的最省事的办法是用 IDEA 的全局编码转换批量转成 UTF-8再在上面三处对齐。5. 常见问题与避坑清单从环境报错到导出项目 war 包的五个血泪教训这些坑是你一定会碰到的或者说我在帮别人调试这个课题时每天都在碰。每条我都按“现象→原因→解决”的方式写你直接对着排查就行。5.1 java.lang.ClassNotFoundException: com.mysql.jdbc.Driver现象启动 Tomcat 访问页面Tomcat 日志里报这个驱动类找不到。原因mysql-connector-java 的 jar 没有复制到 WEB-INF/lib 目录下或者你只是把 jar 加到了 IDEA 的 classpath 而没有跟随项目部署。解决去 Maven 仓库下载对应版本的驱动MySQL 8.x 用 8.0.33 以上把 jar 放到 WebContent/WEB-INF/lib 目录然后重新构建 Artifact。这里不要只依赖 IDEA 的外部库机制部署到 Tomcat 时不一定会打包进去直接放 WEB-INF/lib 是最稳的。5.2 HTTP 404 但 Tomcat 首页能打开现象能看见 Tomcat 官网首页和示例应用但访问http://localhost:8080/bookstore报 404。原因项目没有成功部署到 Tomcat 的 webapps 目录或者 Application context 配置不对。解决检查 IDEA 的运行配置 Deployment 标签页里有没有添加项目右边 Application context 是否填了/bookstore确保和你要访问的路径一致。如果是手动部署把项目整个文件夹直接丢进 webapps 目录再启动。还有一种愚蠢但高频的原因是 Tomcat 的 webapps 目录下有同名老项目的残留文件删除干净再重启。5.3 添加图书时中文乱码但数据库里直接插入中文是正常的现象数据库手工 INSERT 中文没问题但在 JSP 表单里提交中文保存后就变成???。原因前端 JSP 页面是 GBK 编码后端 Servlet 用 UTF-8 解码两者不匹配。解决把所有 JSP 文件的 contentType 统一为text/html; charsetUTF-8页面头部加meta charsetUTF-8同时在 Servlet 里对请求设置request.setCharacterEncoding(UTF-8)并且放到调用任何 getParameter 之前。还有一条隐藏链路是 Tomcat 8 及以上版本默认 GET 请求 UTF-8 解码而老版本 Tomcat 7 需要你在 server.xml 的 connector 上配置 URIEncoding。5.4 tomcat 启动时端口被占用现象启动 Tomcat 时报Port 8080 required by Tomcat v8.5 Server at localhost is already in use。原因上一次运行的 Tomcat 实例没杀掉或者其他 Web 服务器占了 8080。解决用命令查端口占用情况然后处理。Windows 下打开 cmd 执行netstat -ano | findstr 8080看到占用进程 PID 后去任务管理器结束对应进程或者一劳永逸改 Tomcat 的端口。我不建议把端口改成 80因为 Windows 上权限问题会带来新的麻烦改到 8081 或 8090 这类端口即可。# Windows 下杀掉占用 8080 的进程 netstat -ano | findstr 8080 taskkill /F /PID 这里的PID5.5 导出可交差的完整材料war 包和数据库脚本的收尾操作答辩和课设提交需要的三个东西能运行的 war 包、初始化 SQL 脚本、论文里的运行说明。war 包在 IDEA 里通过 Build → Build Artifacts → 选择 war 模式构建构建产物在 out 目录下老派的做法是在 Eclipse 里右键项目导出 WAR 文件。数据库脚本要重新从 MySQL 里导出一份干净的不要用 Navicat 的默认导出模式因为它的转储可能包含多余的系统表或者带上你的本地测试数据。用命令行的mysqldump -u root -p bookstore bookstore_final.sql最干净这样别人拿到后在 MySQL 命令行里执行就能重建库。论文运行说明里写清楚 JDK 版本、Tomcat 版本、MySQL 版本这三个关键参数很多人论文写“JDK 1.6”结果室友用 JDK 17 跑老项目直接报错这就是版本兼容性没写明白。6. 让它从“课设”变成“作品”验证方法、功能增强与二次开发方向如果你只是把项目跑通交付了事这篇笔记的使命已经完成了大半但如果你想在答辩时拿个优秀或者把这个项目写进简历你需要再做三件事。第一件是给系统加上单元测试和功能验证清单第二件是把 JDBC 手写 DAO 改造成 MyBatis 或者 Spring JDBC Template 版本第三件是给项目写一份几十秒的演示脚本。关于功能增强开发者最容易上手的是这三个方向。方向 A把 Session 购物车改成基于 Cookie 或数据库持久化的购物车。Session 的缺陷是关掉浏览器就丢数据改法是引入一张 cart_item 表每次 add 操作直接写库用户下次登录时查库恢复购物车。这个改动涉及表设计、DAO 层和购物车操作逻辑工作量大约两个晚上。方向 B加入 Redis 做缓存把首页的热门图书列表缓存到 Redis用 JSP 页面请求时先查缓存再查数据库这个点最能体现“我懂高并发优化”的概念。方向 C把用户密码从明文改成 MD5 加盐存储并在注册时做二次校验这个改动只有几十行代码但能让你在“系统安全设计”章节多写两页论文。验证方法的层面我强烈建议你在演示前做一个「十分钟验收清单」。用三个浏览器窗口分别测试窗口一登录、窗口二不登录直接访问购物车页面应该被重定向到登录页、窗口三从不同入口进入同一本书的详情页。最后验证事务回滚逻辑手动把数据库连接断开再提交订单看系统会不会给出友好提示而不是抛出一堆异常堆栈。我自己的习惯是永远保留一份“纯净可运行版”的备份里面只含源码、SQL 脚本和 README不夹带任何本地环境的配置绝对路径比如 DBUtil 里的密码写成 root/123456 并在 README 里注明需要修改。因为每次交版本给老师或同事时对方最痛苦的时刻就是看到一个路径写死在自己电脑上的配置文件。做成一个能被状态变更记录追踪的项目也比单文件乱丢要可靠得多。希望这套链路能帮你从解压 ZIP 开始一步不卡地走到答辩讲台上祝你顺利。本文还有配套的精品资源点击获取