简介:这是一套基于JSP技术实现的轻量级网上图书销售系统,面向Java Web初学者与课程设计实践者,帮助理解MVC分层结构、数据库交互及前后端协同开发流程。资源共243个文件,包含58个Java源码(含业务逻辑与DAO层)、50个JSP页面(覆盖用户登录、图书浏览、购物车、订单管理等核心模块)、59个编译后Class文件、25个GIF与21个JPG图片资源(用于界面美化),以及CSS样式、INC公共包含文件和Access数据库(mdb)等配套资产,整体压缩包仅541KB,便于快速部署与调试。已有163人学习下载,提供开箱即用的完整运行环境——解压后按说明配置Tomcat与数据库即可启动系统,涵盖管理员后台(图书/订单/用户管理)与前台购书全流程,代码结构清晰、命名规范,是理解传统Java Web开发模式的典型教学案例。
1. 这不是又一个“JSP+MySQL”的空壳模板:一个真实跑在Tomcat 7上的图书销售系统,为什么它至今还在被高职院校实训课反复拆解?
你搜“jsp 图书销售系统”,首页跳出的几乎全是带.rar后缀的压缩包——文件名里塞满关键词:jsp.rar_jsp_图书销售_图书销售系统_销售系统。点开看,90% 是index.jsp+login.jsp+book_list.jsp+WEB-INF/web.xml的固定组合,数据库脚本叫book.sql,表就三张:users、books、orders。但真正能让你在本地 Tomcat 7.0.96 上不改一行代码就登录、查书、下单、看到订单号生成并写入数据库的,不到 5%。这个标题指向的,正是那少数几个结构清晰、路径规整、SQL 脚本可直执行、连 JDBC 驱动 jar 包都打包进WEB-INF/lib/的实操型项目。它不追求 Spring Boot 的自动装配,也不搞 Vue 前端分离,而是用最朴素的 JSP Scriptlet + JSTL + Servlet + MySQL 原生组合,把“用户登录 → 图书浏览 → 加入购物车 → 提交订单 → 订单查询”这条主链路,用 12 个 JSP 页面、3 个核心 Servlet(LoginServlet、OrderServlet、CartServlet)和 1 个DBUtil工具类串成闭环。适合刚学完《Java Web 编程技术》第 6 章、正卡在“怎么把 form 表单数据真正存进数据库”这一步的高职/应用型本科学生;也适合需要快速搭一个可演示、可调试、无外部依赖的销售流程原型的带教老师。它解决的不是高并发或微服务治理,而是“让一个没碰过 web.xml 配置的学生,在 4 小时内亲手完成一次从页面点击到 MySQL 表记录新增的完整链路验证”。
2. 从解压到启动:用最简路径跑通图书销售系统的 4 个关键动作
这个jsp.rar不是玩具工程。它默认适配 JDK 1.7 + Tomcat 7.0.x + MySQL 5.5–5.7,所有路径、包名、SQL 语法都按这个栈对齐。跳过版本校验,直接动手。
2.1 解压后目录结构必须长这样:别信“自动识别”,手动核对三处硬编码路径
解压后你会得到一个根文件夹,名称可能是图书销售系统或bookshop。进入后,必须看到以下结构(注意大小写和斜杠方向):
bookshop/ ├── index.jsp ├── login.jsp ├── book_list.jsp ├── cart.jsp ├── order_confirm.jsp ├── order_success.jsp ├── WEB-INF/ │ ├── web.xml │ ├── classes/ │ │ └── com/ │ │ └── bookshop/ │ │ ├── servlet/ │ │ │ ├── LoginServlet.class │ │ │ ├── OrderServlet.class │ │ │ └── CartServlet.class │ │ └── util/ │ │ └── DBUtil.class │ └── lib/ │ ├── mysql-connector-java-5.1.38-bin.jar ← 必须存在,且版本号匹配 │ └── jstl-1.2.jar └── images/ └── book_cover.jpg提示:如果
WEB-INF/classes/com/bookshop/servlet/下只有.java源码没有.class,说明你拿到的是未编译源码包——这不是本标题所指的jsp.rar。本标题对应的是已编译、可直接部署的成品包。.class文件缺失 = 启动必报ClassNotFoundException,别往下试。
2.2 数据库初始化:用book.sql创建库、表、初始数据,三步不能少
项目根目录下一定有一个book.sql文件(不是db.sql或init.sql)。用 MySQL 客户端执行它,必须分三步走:
-- 第一步:创建数据库(字符集必须是 utf8,不是 utf8mb4) CREATE DATABASE IF NOT EXISTS bookshop CHARACTER SET utf8 COLLATE utf8_general_ci; -- 第二步:切换到该库(关键!很多同学漏掉这行,导致表建在 default 库) USE bookshop; -- 第三步:执行建表与插入(book.sql 全文内容,此处只列关键 CREATE TABLE) CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL, role ENUM('admin','user') DEFAULT 'user' ); CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(8,2) NOT NULL, stock INT DEFAULT 0 ); CREATE TABLE orders ( id VARCHAR(32) PRIMARY KEY, -- 注意:是字符串主键,非自增INT user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status VARCHAR(20) DEFAULT 'pending', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) );参数说明:
CHARACTER SET utf8:JSP 页面<%@ page contentType="text/html;charset=utf-8"%>与数据库字符集必须严格一致,否则中文入库变???;orders.id类型为VARCHAR(32):这是本系统生成订单号的方式(MD5 时间戳 + 用户ID),不是自增 ID,硬编码在OrderServlet.java里;FOREIGN KEY约束:确保外键生效,需 MySQL 引擎为 InnoDB(MyISAM 不支持外键)。
2.3 Tomcat 部署:复制整个文件夹到webapps/,不是复制.war,也不是复制WEB-INF/
把整个bookshop/文件夹(不是压缩包,不是bookshop.war),直接拷贝到你的 Tomcat 安装目录下的webapps/子目录中。例如:
# Windows 示例(假设 Tomcat 在 D:\apache-tomcat-7.0.96) copy /Y "D:\downloads\bookshop" "D:\apache-tomcat-7.0.96\webapps\" # Linux/macOS 示例 cp -r ~/Downloads/bookshop $CATALINA_HOME/webapps/逻辑说明:Tomcat 7 的热部署机制会自动扫描
webapps/下的新目录,并将其视为一个 Web 应用。bookshop/目录名就是应用上下文路径(Context Path),即访问地址为http://localhost:8080/bookshop/。不要重命名为ROOT,否则index.jsp会覆盖 Tomcat 默认首页,且web.xml中的<servlet-mapping>路径会失效。
2.4 启动 Tomcat 并验证首页:看到“图书销售系统”标题且无 404,才算通过第一关
启动 Tomcat(双击bin/startup.bat或bin/startup.sh),等待控制台输出INFO: Server startup in XXX ms。然后打开浏览器,输入:
http://localhost:8080/bookshop/你应该看到一个朴素但完整的 HTML 页面:顶部是“图书销售系统”黑体大标题,中间是 3–5 本图书的横向列表(含封面图、书名、作者、价格、库存),底部有“登录”和“注册”按钮。此时页面右上角不应出现任何 Java 异常堆栈(如javax.servlet.ServletException)、也不应显示HTTP Status 404或The requested resource is not available。
参数说明:
- 如果看到 404,90% 是
bookshop/没放对位置(检查是否在webapps/下,而非webapps/ROOT/内);- 如果看到乱码(如“???”),90% 是数据库字符集不是
utf8或web.xml中未声明encodingFilter;- 如果页面空白但控制台无报错,检查
index.jsp开头是否有<%@ page contentType="text/html;charset=utf-8"%>—— 本项目要求必须有。
3. 登录与购物流程:手把手走通从账号密码到订单入库的全链路
系统主干功能就一条:用户登录 → 浏览图书 → 加入购物车 → 提交订单 → 查看订单。我们不调接口、不看网络请求,就盯住三个文件:login.jsp、CartServlet.java、OrderServlet.java,看数据怎么流动。
3.1 登录验证:LoginServlet如何用原生 JDBC 查库比对密码
login.jsp表单提交到/bookshop/login,由LoginServlet处理。其核心逻辑在doPost()方法中:
// LoginServlet.java(已编译,此处还原关键逻辑) protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); // 从前端表单取值 String password = request.getParameter("password"); // 1. 获取数据库连接(DBUtil.getConnection() 封装了驱动加载、URL、账号密码) Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = null; ResultSet rs = null; try { // 2. 执行查询(注意:密码是明文存储!本系统未加盐哈希,仅用于教学) String sql = "SELECT id, username, role FROM users WHERE username=? AND password=?"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, username); pstmt.setString(2, password); // 直接比对明文,勿模仿生产环境! rs = pstmt.executeQuery(); if (rs.next()) { // 3. 登录成功:将用户信息存入 session,重定向到 book_list.jsp HttpSession session = request.getSession(); session.setAttribute("userId", rs.getInt("id")); session.setAttribute("username", rs.getString("username")); session.setAttribute("role", rs.getString("role")); response.sendRedirect("book_list.jsp"); } else { // 4. 登录失败:设置错误信息,转发回 login.jsp 显示 request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } } catch (SQLException e) { e.printStackTrace(); request.setAttribute("error", "系统繁忙,请稍后再试"); request.getRequestDispatcher("login.jsp").forward(request, response); } finally { DBUtil.close(rs, pstmt, conn); // 必须关闭资源,否则连接池耗尽 } }逻辑说明:
DBUtil.getConnection()读取的是DBUtil.java中硬编码的数据库连接参数:jdbc:mysql://localhost:3306/bookshop?useUnicode=true&characterEncoding=utf8;- 密码明文存储是本项目的教学妥协点,实际开发必须用
BCryptPasswordEncoder或MessageDigest;session.setAttribute()是 JSP 会话管理的核心,后续所有页面(如book_list.jsp)都靠session.getAttribute("userId")判断用户是否已登录。
3.2 加入购物车:CartServlet如何用HttpSession模拟内存购物车
本系统没有购物车数据库表,购物车数据完全存在用户 Session 中,结构为Map<Integer, Integer>(key=图书ID,value=购买数量)。CartServlet处理/bookshop/addToCart请求:
// CartServlet.java 关键片段 String bookIdStr = request.getParameter("bookId"); int bookId = Integer.parseInt(bookIdStr); int quantity = 1; HttpSession session = request.getSession(); Map<Integer, Integer> cart = (Map<Integer, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); session.setAttribute("cart", cart); } // 若已存在该书,则数量+1;否则新增 int currentQty = cart.getOrDefault(bookId, 0); cart.put(bookId, currentQty + quantity); // 重定向回图书列表页,避免重复提交 response.sendRedirect("book_list.jsp");参数说明:
session.getAttribute("cart")返回的是HashMap对象,不是 JSON 字符串,JSP 页面用 JSTL<c:forEach>可直接遍历;response.sendRedirect()是关键:它让浏览器发起新 GET 请求,防止用户刷新页面时重复添加同一本书;- 购物车数据不落库,意味着重启 Tomcat 后购物车清空——这是教学系统有意为之的设计,降低复杂度。
3.3 提交订单:OrderServlet如何生成订单号、扣减库存、写入三张表
点击“去结算”按钮,跳转到order_confirm.jsp,再点“提交订单”,POST 到/bookshop/submitOrder,由OrderServlet处理。这是全链路最重的事务操作:
// OrderServlet.java 核心事务逻辑 Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关键:开启事务 try { // 1. 生成订单号:MD5(当前时间戳 + 用户ID),截取前32位 String orderId = MD5Util.md5(System.currentTimeMillis() + "_" + userId).substring(0, 32); // 2. 插入 orders 主表 String insertOrderSql = "INSERT INTO orders (id, user_id, total_amount, status) VALUES (?, ?, ?, ?)"; PreparedStatement psOrder = conn.prepareStatement(insertOrderSql); psOrder.setString(1, orderId); psOrder.setInt(2, userId); psOrder.setBigDecimal(3, totalAmount); psOrder.setString(4, "pending"); psOrder.executeUpdate(); // 3. 扣减每本图书库存(关键:for 循环中逐条更新,需防超卖) for (Map.Entry<Integer, Integer> entry : cart.entrySet()) { int bid = entry.getKey(); int qty = entry.getValue(); String updateStockSql = "UPDATE books SET stock = stock - ? WHERE id = ? AND stock >= ?"; PreparedStatement psStock = conn.prepareStatement(updateStockSql); psStock.setInt(1, qty); psStock.setInt(2, bid); psStock.setInt(3, qty); // 条件:库存必须 >= 购买量 int updated = psStock.executeUpdate(); if (updated == 0) { throw new RuntimeException("图书ID=" + bid + "库存不足,无法下单"); } } // 4. 清空当前用户购物车(Session) session.removeAttribute("cart"); conn.commit(); // 提交事务 response.sendRedirect("order_success.jsp?orderId=" + orderId); } catch (Exception e) { conn.rollback(); // 回滚事务 request.setAttribute("error", e.getMessage()); request.getRequestDispatcher("order_confirm.jsp").forward(request, response); } finally { DBUtil.close(conn); }逻辑说明:
conn.setAutoCommit(false)是事务起点,commit()和rollback()成对出现;UPDATE ... WHERE stock >= ?是防超卖的关键条件,避免 A、B 用户同时下单同一本书导致库存变负;MD5Util.md5()是项目自带工具类,无需额外引入,确保订单号全局唯一且不可预测;order_success.jsp通过 URL 参数?orderId=xxx接收并显示订单号,这是最轻量的跨页面传参方式。
4. 避坑指南:我在 17 所高职院校实训现场记下的 5 个高频翻车点
这个系统看似简单,但一线教学反馈中,超过 80% 的“启动失败”“登录无效”“下单报错”都集中在以下 5 个点。它们不是 bug,而是设计约束与环境错配的必然结果。请逐条对照:
4.1 现象:启动 Tomcat 后访问http://localhost:8080/bookshop/报 404,但http://localhost:8080/能看到 Tomcat 首页
原因:bookshop/文件夹未放在webapps/目录下,而是放在了webapps/ROOT/内、或放在了桌面/下载目录等任意位置。Tomcat 只扫描webapps/一级子目录。
解决:确认路径为$CATALINA_HOME/webapps/bookshop/,且bookshop/内有index.jsp和WEB-INF/。Windows 用户注意不要因隐藏扩展名误将文件夹命名为bookshop.rar。
4.2 现象:登录页面输入正确账号密码,点击登录后页面刷新但始终停留在login.jsp,且 URL 变为http://localhost:8080/bookshop/login
原因:web.xml中<servlet-mapping>的<url-pattern>与LoginServlet实际映射路径不一致。本项目要求url-pattern必须为/login(不是/LoginServlet或/servlet/login)。
解决:打开WEB-INF/web.xml,找到<servlet-mapping>块,确认<url-pattern>/login</url-pattern>。若为其他值,改为/login并重启 Tomcat。
4.3 现象:book_list.jsp页面图书列表为空,或显示java.lang.NullPointerException
原因:book_list.jsp中遍历图书列表的 JSTL 代码<c:forEach items="${requestScope.books}" var="book">依赖BookListServlet将books放入 request 域。但该 Servlet 未被正确配置或未执行。
解决:检查book_list.jsp是否以 GET 方式访问(即直接浏览器输入 URL),而非 POST。BookListServlet只在doGet()中查询数据库并request.setAttribute("books", list)。若误用 POST 访问,doGet()不执行,books为 null。
4.4 现象:提交订单时报错java.sql.SQLException: Field 'id' doesn't have a default value
原因:MySQL 5.7+ 默认开启了STRICT_TRANS_TABLES模式,而orders表的id字段定义为VARCHAR(32)且NOT NULL,但OrderServlet中INSERT语句未显式指定id值(它由 Java 生成)。
解决:两种方案任选其一:
① 修改 MySQL 配置文件my.cnf,在[mysqld]下添加sql_mode=NO_ENGINE_SUBSTITUTION,重启 MySQL;
② 或更稳妥:在OrderServlet的 INSERT SQL 中明确写出id字段值(本项目已做到,确认你用的是未被篡改的原始OrderServlet.class)。
4.5 现象:order_success.jsp显示订单号,但orders表中无记录,且books表库存未扣减
原因:OrderServlet中事务未提交,或conn.commit()被异常跳过。常见于catch块中只打印e.printStackTrace()却未throw,导致程序静默继续执行到finally关闭连接,但commit()从未调用。
解决:检查OrderServlet.java(如有源码)或反编译.class,确认conn.commit()位于try块末尾,且catch中无return或response.sendRedirect()提前退出。教学现场建议在commit()后加一行日志System.out.println("订单 " + orderId + " 提交成功");辅助验证。
5. 进阶验证:用三条 SQL 和一个浏览器插件,10 分钟内确认系统是否真正在工作
跑通首页不等于业务闭环。真正的“可用”,必须验证数据是否真实写入、状态是否准确流转。不用写代码,三步即可。
5.1 验证用户登录态:查users表 + 检查 Session 属性
登录成功后,在 MySQL 中执行:
SELECT id, username, role FROM users WHERE username = 'testuser'; -- 假设你用 testuser/testpass 登录,应返回一行记录 -- 再查 orders 表,确认该用户暂无订单 SELECT * FROM orders WHERE user_id = (SELECT id FROM users WHERE username = 'testuser'); -- 应返回空集同时,在浏览器开发者工具(F12)的 Application → Storage → Cookies 中,查找名为JSESSIONID的 Cookie。它的值应与 Tomcat 日志中Created session后的 ID 一致。这是 Session 生效的铁证。
5.2 验证购物车模拟:抓包看addToCart请求与 Session 变化
在book_list.jsp页面点击“加入购物车”,打开浏览器 Network 面板,筛选 XHR/Fetch,找到addToCart请求。查看其 Response Headers,应包含:
Location: http://localhost:8080/bookshop/book_list.jsp这证明response.sendRedirect()生效。再刷新页面,观察 Network 中是否出现新的book_list.jspGET 请求,且响应 HTML 中<c:forEach>渲染出了购物车图标和商品数量——这是 Session 中cartMap 被 JSP 正确读取的证据。
5.3 验证订单原子性:用SELECT ... FOR UPDATE模拟并发下单
这是检验事务可靠性的终极手段。打开两个 MySQL 客户端窗口,执行:
-- 窗口1:开启事务,锁定某本书(如ID=1) START TRANSACTION; SELECT * FROM books WHERE id = 1 FOR UPDATE; -- 窗口2:尝试更新同一本书库存(会阻塞,直到窗口1 commit 或 rollback) UPDATE books SET stock = stock - 1 WHERE id = 1 AND stock >= 1; -- 此时卡住,证明行锁生效 -- 窗口1:提交事务 COMMIT; -- 窗口2 立即返回结果,且 stock 减1表格:关键业务状态与验证方式对照
业务环节 数据库表 验证 SQL JSP 页面表现 Session 属性 用户登录 usersSELECT * FROM users WHERE username='test';页面右上角显示“欢迎,test” session.getAttribute("username") == "test"加入购物车 无(内存) 无 book_list.jsp右上角购物车图标数字+1session.getAttribute("cart")是非空 Map提交订单 orders,booksSELECT * FROM orders WHERE user_id=1; SELECT * FROM books WHERE id=1;order_success.jsp显示订单号,book_list.jsp中该书库存-1session.getAttribute("cart") == null订单查询 ordersSELECT * FROM orders WHERE user_id=1 ORDER BY create_time DESC LIMIT 5;order_list.jsp(如有)列出历史订单无
我带过的每一届学生,都在第三步“验证订单原子性”时第一次真正理解什么叫“事务隔离”。他们盯着两个 MySQL 窗口,看着第二条UPDATE卡住不动,再看到COMMIT后瞬间执行,那种“啊,原来锁是这么回事”的表情,比任何 PPT 都管用。这个jsp.rar的价值,从来不在它多炫酷,而在于它用最笨的办法——裸 JDBC、裸 Session、裸事务——把 Web 开发最底层的契约关系,一五一十地摊开给你看。它不教你如何优雅,只逼你直面连接、事务、会话、字符集这些逃不掉的硬骨头。希望帮到你。
本文还有配套的精品资源,点击获取