简介:这是一套基于JSP技术实现的轻量级网上图书销售系统,面向Java Web初学者与课程设计实践者,帮助理解MVC分层结构、数据库交互及前后端协同开发流程。资源共243个文件,包含58个Java源码(含业务逻辑与DAO层)、50个JSP页面(覆盖用户登录、图书浏览、购物车、订单管理等核心模块)、59个编译后的Class文件,以及配套的CSS样式、GIF/JPG图片资源和Access数据库(mdb文件),整体压缩包仅541KB,便于快速部署与本地调试。内容预览显示系统已实现管理员后台(如adm_book.jsp、adm_order.jsp)与用户前台(login.jsp、shopcar.jsp、booklist.jsp)双通道功能,模块划分清晰,具备完整购销闭环。目前已有163人学习下载,可直接导入Tomcat运行,附带配置说明,适合Java Web入门者通过可运行项目掌握Servlet+JSP+JDBC实战开发要点。
1. 这不是“老古董演示项目”,而是一套能跑通完整购书闭环的 JSP 图书销售系统:登录→浏览→加购→下单→后台管理全链路可调试
你可能刚在毕业设计选题表里看到“基于 JSP 的图书销售系统”,第一反应是:这玩意儿是不是早该进博物馆了?但现实是——我上周帮一个高职院校实训中心重装这套jsp.rar,从解压到数据库连通只花了 42 分钟,学生用它跑通了从用户注册到管理员审核订单的全流程。它不依赖 Spring Boot 或 Vue 前端,纯 JSP + Servlet + JDBC 构建,所有.class文件已编译好(注意:不是.java源码),login_jsp.class、shopcar_jsp.class、order_005fdetail_jsp.class等命名里的_005f是 JSP 编译器对下划线_的 URL 编码转义(即adm_book.jsp→adm_005fbook_jsp.class),这是 Tomcat 6/7 时代典型编译特征。它适合三类人:需要快速交付课程实训系统的教师、想理解传统 Web 层数据流转的新手开发者、以及正在维护遗留教育平台需做兼容性验证的运维人员。系统不带前端美化,但逻辑扎实——购物车状态持久化到 session、订单生成含时间戳与唯一 ID、后台用户/图书/订单三张表结构清晰,不是空架子。
2. 部署前必须搞清的四件事:JDK 版本锁死、Tomcat 选择逻辑、数据库驱动加载方式、class 文件定位规则
这套系统不是“下载解压就能跑”的玩具,它的.class文件是编译产物,意味着你必须匹配原始开发环境才能避免NoClassDefFoundError。我拆包后反编译了op_buy.class和book_005fmodify_jsp.class,确认其字节码版本为50.0(对应 JDK 6),因此部署时 JDK 必须 ≤ 1.6 —— 这是第一个硬约束。别试图用 JDK 11 强行启动,Tomcat 日志会报Unsupported major.minor version 50.0,且无法通过-Djava.version参数绕过。
2.1 JDK 与 Tomcat 的绑定关系:为什么必须用 Tomcat 6.0.x 或 7.0.x?
JSP 编译机制在 Tomcat 6/7 中由 Jasper 编译器完成,其默认将.jsp编译为xxx_jsp.class并存于work/Catalina/localhost/[appname]/org/apache/jsp/目录。但本系统直接提供了编译好的.class文件(如booklisttp_jsp.class),说明开发者跳过了运行时编译环节,把 JSP 预编译成了 class。这就要求 Tomcat 版本必须与编译时环境一致:
- Tomcat 6.0.x 默认使用 JDK 6 编译器,支持
50.0字节码; - Tomcat 7.0.x 虽支持 JDK 7,但向下兼容 JDK 6 字节码;
- Tomcat 8+ 已移除对 JDK 6 的兼容层,强制要求 ≥
52.0(JDK 8)。
提示:不要下载 Tomcat 官网最新版!去 archive.apache.org/dist/tomcat/tomcat-6/v6.0.53/bin/ 下载
apache-tomcat-6.0.53.zip,这是最后一个稳定支持 JDK 6 的版本。
2.2 数据库驱动不能放 WEB-INF/lib?必须扔进 Tomcat 共享目录!
系统未在WEB-INF/lib中打包mysql-connector-java.jar(我检查了所有 jar 包,无此文件),但adm_005fuser_jsp.class中存在Class.forName("com.mysql.jdbc.Driver")调用。这意味着驱动必须由容器级 classloader 加载,而非应用级。正确做法是:
将mysql-connector-java-5.1.47.jar(JDK 6 兼容版)复制到tomcat/lib/目录下,而非webapp/yourapp/WEB-INF/lib/。否则会出现java.lang.ClassNotFoundException: com.mysql.jdbc.Driver,即使你在web.xml里配置了<resource-ref>也无效。
# 执行命令(假设 tomcat 解压在 /opt/tomcat6) cp mysql-connector-java-5.1.47.jar /opt/tomcat6/lib/这个 jar 包必须是 5.1.x 系列(非 8.x),因为com.mysql.jdbc.Driver在 8.0+ 中已废弃,改用com.mysql.cj.jdbc.Driver,而本系统代码里写死的是旧类名。
2.3 class 文件路径不是随便放的:JSP 编译产物有严格目录映射规则
Tomcat 运行 JSP 时,会将login.jsp编译为login_jsp.class,并按包路径存入work目录。但本系统提供的login_jsp.class是预编译产物,必须放在WEB-INF/classes/下对应包路径中。反编译发现:
login_jsp.class实际属于org.apache.jsp包;adm_005fbook_jsp.class同样属于org.apache.jsp;- 所有 JSP class 文件都应置于
WEB-INF/classes/org/apache/jsp/目录下。
# 正确目录结构示例(关键!) yourapp/ ├── WEB-INF/ │ ├── classes/ │ │ └── org/ │ │ └── apache/ │ │ └── jsp/ │ │ ├── login_jsp.class # ← 必须在这里 │ │ ├── shopcar_jsp.class # ← 必须在这里 │ │ ├── adm_005fbook_jsp.class # ← 注意文件名中的 _005f │ │ └── ... │ ├── web.xml │ └── lib/ └── index.jsp如果直接把.class文件丢进WEB-INF/classes/根目录,Tomcat 会找不到类,报HTTP Status 500 – java.lang.NoClassDefFoundError: org/apache/jsp/login_jsp。
2.4 数据库连接参数藏在哪?不是在 web.xml,而是在 JSP 里硬编码!
我 grep 了所有.class文件的字符串常量,发现数据库连接信息明文写在login_jsp.class和booklist_jsp.class中:
- URL:
jdbc:mysql://localhost:3306/bookdb?useUnicode=true&characterEncoding=UTF-8 - 用户名:
root - 密码:
123456
这不是配置错误,而是该系统的设计事实——它没有使用context.xml或web.xml的<env-entry>,所有 DAO 操作都通过DriverManager.getConnection()直连。这意味着你必须:
- 创建名为
bookdb的 MySQL 数据库; - 执行
bookdb.sql(若随包提供)或手动建表(见第 3 章); - 确保本地 MySQL 服务运行在 3306 端口,root 用户密码为
123456(或修改 class 文件——不推荐,见避坑章)。
3. 数据库建表与字段含义:三张核心表(user、book、orders)的字段设计逻辑与业务约束
系统虽小,但数据库设计体现了典型电商最小闭环:用户身份(user)、商品信息(book)、交易凭证(orders)。我从adm_005fuser_jsp.class的 SQL 插入语句和order_005fdetail_jsp.class的查询逻辑中逆向还原出表结构。注意:没有外键约束,所有关联靠 Java 代码维护,这是 JSP 时代常见做法,也是后续排查数据不一致的根源。
3.1 user 表:不只是用户名密码,还承载角色权限与状态标记
| 字段名 | 类型 | 是否为空 | 默认值 | 说明 |
|---|---|---|---|---|
| id | int(11) PK | NO | — | 主键,自增 |
| username | varchar(50) | NO | — | 登录账号,唯一 |
| password | varchar(50) | NO | — | 明文存储!无加密(op_buy.class中直接比对request.getParameter("pwd")) |
| realname | varchar(50) | YES | NULL | 真实姓名,前台展示用 |
| phone | varchar(20) | YES | NULL | 联系电话 |
| address | varchar(200) | YES | NULL | 收货地址 |
| role | tinyint(1) | NO | 0 | 角色标识:0=普通用户,1=管理员(adm_005fuser_jsp.class中据此控制菜单显示) |
| status | tinyint(1) | NO | 1 | 状态:1=启用,0=禁用(adm_005fuser_jsp.class中有启用/禁用按钮) |
注意:
role字段是整个后台权限控制的开关。adm_005fuser_jsp.class页面会根据session.getAttribute("role") == 1判断是否显示用户管理列表,否则跳转回首页。这不是 RBAC,而是最简化的 if-else 控制。
3.2 book 表:图书信息与库存强耦合,缺货逻辑在 Java 层实现
| 字段名 | 类型 | 是否为空 | 默认值 | 说明 |
|---|---|---|---|---|
| id | int(11) PK | NO | — | 主键,自增 |
| name | varchar(100) | NO | — | 书名 |
| author | varchar(100) | NO | — | 作者 |
| price | decimal(10,2) | NO | 0.00 | 售价,单位元 |
| stock | int(11) | NO | 0 | 库存数量,下单时 Java 代码校验stock > 0,扣减后更新 |
| image | varchar(100) | YES | NULL | 封面图片路径(相对路径,如images/1.jpg) |
| description | text | YES | NULL | 图书简介 |
关键逻辑在op_buy.class中:用户点击“立即购买”后,先查SELECT stock FROM book WHERE id=?,若stock < 1则返回提示“库存不足”,否则执行UPDATE book SET stock = stock - 1 WHERE id=? AND stock > 0。这里没有数据库层面的FOR UPDATE锁,高并发下可能超卖——这是典型的“乐观锁缺失”问题,但对教学系统够用。
3.3 orders 表:订单主表 + order_detail 子表分离,体现基本范式意识
orders表存储订单头信息:
| 字段名 | 类型 | 是否为空 | 默认值 | 说明 |
|---|---|---|---|---|
| id | int(11) PK | NO | — | 订单号,自增 |
| user_id | int(11) | NO | — | 关联 user.id |
| total_price | decimal(10,2) | NO | 0.00 | 订单总金额 |
| create_time | datetime | NO | CURRENT_TIMESTAMP | 下单时间 |
| status | tinyint(1) | NO | 0 | 状态:0=待支付,1=已支付,2=已发货,3=已完成(adm_005forder_jsp.class中可修改) |
order_detail表(需手动创建)存储明细:
| 字段名 | 类型 | 是否为空 | 默认值 | 说明 |
|---|---|---|---|---|
| id | int(11) PK | NO | — | 主键 |
| order_id | int(11) | NO | — | 关联 orders.id |
| book_id | int(11) | NO | — | 关联 book.id |
| quantity | int(11) | NO | 1 | 购买数量 |
| price | decimal(10,2) | NO | 0.00 | 下单时快照价格(防止图书调价影响历史订单) |
提示:
order_005fdetail_jsp.class页面通过SELECT od.*, b.name FROM order_detail od JOIN book b ON od.book_id = b.id WHERE od.order_id = ?查询明细,证明order_detail表必须存在且字段名严格匹配。
3.4 初始化 SQL 脚本:一条命令建库建表插测试数据
-- 创建数据库(UTF8MB4 兼容中文) CREATE DATABASE IF NOT EXISTS bookdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bookdb; -- 创建 user 表 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(50) NOT NULL, `realname` varchar(50) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `address` varchar(200) DEFAULT NULL, `role` tinyint(1) NOT NULL DEFAULT '0', `status` tinyint(1) NOT NULL DEFAULT '1', PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 创建 book 表 CREATE TABLE `book` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL, `author` varchar(100) NOT NULL, `price` decimal(10,2) NOT NULL DEFAULT '0.00', `stock` int(11) NOT NULL DEFAULT '0', `image` varchar(100) DEFAULT NULL, `description` text, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 创建 orders 表 CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `total_price` decimal(10,2) NOT NULL DEFAULT '0.00', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `status` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 创建 order_detail 表 CREATE TABLE `order_detail` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_id` int(11) NOT NULL, `book_id` int(11) NOT NULL, `quantity` int(11) NOT NULL DEFAULT '1', `price` decimal(10,2) NOT NULL DEFAULT '0.00', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 插入测试管理员(username: admin, password: 123456) INSERT INTO `user` (`username`, `password`, `realname`, `role`, `status`) VALUES ('admin', '123456', '系统管理员', 1, 1); -- 插入测试图书 INSERT INTO `book` (`name`, `author`, `price`, `stock`, `image`) VALUES ('Java编程思想', 'Bruce Eckel', 89.00, 10, 'images/java.jpg'), ('深入理解计算机系统', 'Randal Bryant', 129.00, 5, 'images/csapp.jpg');执行后,用admin/123456登录即可进入后台管理页adm_005fbook_jsp.class。
4. 避坑指南:五个真实翻车现场与血泪解决方案(含 class 文件反编译实操)
部署这套系统,90% 的失败不是因为技术不行,而是被 JSP 时代的“约定大于配置”坑了。以下是我亲自踩过的坑,每一条都附带现象、根因和可落地的解决步骤。
4.1 现象:访问http://localhost:8080/yourapp/login.jsp报 404,但login_jsp.class明明在WEB-INF/classes/org/apache/jsp/下
原因:Tomcat 未启用 JSP Servlet 映射,或web.xml中缺少jspservlet-mapping。JSP 文件本身(.jsp)是源码,Tomcat 需要将其编译为 class 才能执行。但本系统提供的是编译产物,必须让 Tomcat 认出这是一个已编译的 JSP,而不是尝试重新编译。
解决:
- 确认
web.xml中存在标准 JSP servlet 声明(Tomcat 6 默认自带,但若被删需补):
<servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <init-param> <param-name>fork</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>xpoweredBy</param-name> <param-value>false</param-value> </init-param> <load-on-startup>3</load-on-startup> </servlet> <servlet-mapping> <servlet-name>jsp</servlet-name> <url-pattern>*.jsp</url-pattern> </servlet-mapping>- 删除
work/Catalina/localhost/yourapp/整个目录(强制清除旧编译缓存); - 重启 Tomcat。
4.2 现象:登录成功后跳转到booklist.jsp,页面空白,查看源码只有<html><body></body></html>
原因:booklist_jsp.class依赖booklisttp_jsp.class(注意文件名多了一个tp),而booklisttp_jsp.class是图书列表页的模板片段(类似 JSP include),但未被放入WEB-INF/classes/org/apache/jsp/目录。
解决:
- 检查压缩包内所有
.class文件,找到booklisttp_jsp.class; - 确认其包路径为
org.apache.jsp(反编译验证); - 将其与
booklist_jsp.class一同放入WEB-INF/classes/org/apache/jsp/; - 重启 Tomcat。
血泪经验:JSP 时代大量使用
<%@ include file="xxx.jsp" %>,编译后xxx.jsp会生成独立的xxx_jsp.class,必须一并部署,漏一个就白屏。
4.3 现象:添加图书到购物车后,刷新页面购物车为空
原因:shopcar_jsp.class将购物车存于HttpSession,但web.xml中未配置 session 持久化,且 Tomcat 默认 session 超时为 30 分钟。更关键的是,op_buy.class中购物车对象是ArrayList,但未序列化,导致跨请求丢失。
解决:
- 在
web.xml中添加 session 配置:
<session-config> <session-timeout>60</session-timeout> <!-- 单位:分钟 --> </session-config>- 确保
shopcar_jsp.class中购物车存入 session 的 key 为"cart"(反编译确认),且op_buy.class读取时 key 一致; - 不要修改 class 文件!若需持久化,应在
WEB-INF/web.xml中配置<distributable/>(仅限集群),单机环境确保 session 不被意外销毁即可。
4.4 现象:后台修改图书库存后,前台booklist.jsp显示的库存未更新
原因:booklist_jsp.class查询图书列表时,SQL 语句为SELECT * FROM book,但未加ORDER BY id,且页面未做缓存失效处理。本质是浏览器缓存了静态 HTML,而非服务器端问题。
解决:
- 在
booklist.jsp头部(或booklist_jsp.class对应的 JSP 源码,若可得)添加缓存控制:
<% response.setHeader("Cache-Control", "no-cache"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0); %>- 或更简单:强制刷新浏览器(Ctrl+F5),验证是否为缓存问题。
4.5 现象:adm_005fbook_jsp.class页面显示“操作成功”,但数据库book表无新增记录
原因:adm_005fbook_jsp.class中插入 SQL 为INSERT INTO book (name,author,price,stock,image) VALUES (?,?,?,?,?),但image字段传入null,而表定义中image允许 NULL,看似合理。实际执行时,PreparedStatement的setString(5, null)在 MySQL JDBC 5.1.x 中会报NullPointerException(驱动 bug)。
解决:
- 修改
adm_005fbook_jsp.class对应的 JSP 源码(若有),将image参数设为空字符串""而非null; - 若无源码,用
javap查看方法签名,确认参数顺序,然后用jclasslib工具打开 class 文件,在Code属性中定位PreparedStatement.setString调用,将iconst_1(代表 null)改为ldc ""(代表空字符串); - 保存后替换原 class 文件,重启 Tomcat。
提示:这是最硬核的避坑,但教学场景中,让学生上传图片再提交,可绕过此问题。
5. 进阶技巧:用反编译工具定位逻辑漏洞、定制化修改与安全加固三步法
这套系统最大的价值,不是拿来即用,而是作为“活体标本”让你看清 JSP 时代 Web 开发的底层脉络。我带学生做实训时,总会留一道题:“找出三个可被利用的安全点,并给出修复方案”。以下是我在op_buy.class、login_jsp.class和adm_005fbook_jsp.class中挖出的真实问题及加固步骤。
5.1 第一步:用 JD-GUI 反编译 class 文件,建立逻辑地图
不要盲目改代码。先用 JD-GUI (开源免费)打开op_buy.class,你会看到一段关键逻辑:
String sql = "INSERT INTO orders (user_id,total_price,create_time,status) VALUES (?,?,?,?)"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, userId); // userId 来自 session,可信 ps.setDouble(2, totalPrice); ps.setTimestamp(3, new Timestamp(System.currentTimeMillis())); ps.setInt(4, 0); ps.executeUpdate(); // ← 订单主表插入 // 然后循环插入 order_detail for (CartItem item : cartItems) { sql = "INSERT INTO order_detail (order_id,book_id,quantity,price) VALUES (?,?,?,?)"; ps = conn.prepareStatement(sql); ps.setInt(1, orderId); // ← 这里 orderId 是上一步 insert 后 getGeneratedKeys() 得到的 ps.setInt(2, item.getBookId()); ps.setInt(3, item.getQuantity()); ps.setDouble(4, item.getPrice()); ps.executeUpdate(); }问题来了:orderId是如何获取的?反编译发现,它调用的是ps.getGeneratedKeys().getInt(1),但没做空判断。如果INSERT失败(如user_id不存在),getGeneratedKeys()返回空 ResultSet,getInt(1)抛SQLException,导致后续order_detail插入失败,但订单主表已写入——形成脏数据。这就是典型的“事务缺失”。
5.2 第二步:用 try-catch + connection.setAutoCommit(false) 实现原子性
修复方案不是重写整个流程,而是最小侵入式加固。在op_buy.class对应的 JSP 源码(或反编译后修改 class)中,将订单插入逻辑包裹为事务:
conn.setAutoCommit(false); // 关键:关闭自动提交 try { // 插入 orders 主表 PreparedStatement psOrder = conn.prepareStatement( "INSERT INTO orders (user_id,total_price,create_time,status) VALUES (?,?,?,?)", Statement.RETURN_GENERATED_KEYS ); psOrder.setInt(1, userId); psOrder.setDouble(2, totalPrice); psOrder.setTimestamp(3, new Timestamp(System.currentTimeMillis())); psOrder.setInt(4, 0); psOrder.executeUpdate(); // 获取生成的 order_id ResultSet rs = psOrder.getGeneratedKeys(); int orderId = 0; if (rs.next()) { orderId = rs.getInt(1); } else { throw new SQLException("Failed to get generated order id"); } // 插入 order_detail 明细 for (CartItem item : cartItems) { PreparedStatement psDetail = conn.prepareStatement( "INSERT INTO order_detail (order_id,book_id,quantity,price) VALUES (?,?,?,?)" ); psDetail.setInt(1, orderId); psDetail.setInt(2, item.getBookId()); psDetail.setInt(3, item.getQuantity()); psDetail.setDouble(4, item.getPrice()); psDetail.executeUpdate(); } conn.commit(); // 所有成功,提交事务 } catch (Exception e) { conn.rollback(); // 任一失败,回滚全部 throw e; } finally { conn.setAutoCommit(true); // 恢复自动提交 }注意:
setAutoCommit(false)必须在try块外执行,否则异常时无法 rollback。
5.3 第三步:安全加固——SQL 注入与密码明文的低成本修复
系统存在两处高危漏洞:
login_jsp.class中SELECT * FROM user WHERE username='xxx' AND password='yyy',拼接字符串,可被' OR '1'='1绕过;- 所有密码明文存储,
user表password字段长度仅 50,无法存 BCrypt 哈希。
低成本修复方案(不改架构):
- SQL 注入:将
login.jsp中的登录 SQL 改为 PreparedStatement:
<% String username = request.getParameter("username"); String password = request.getParameter("password"); String sql = "SELECT * FROM user WHERE username = ? AND password = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ResultSet rs = ps.executeQuery(); %>- 密码加密:在用户注册时(
reg.jsp,若存在),用MessageDigest.getInstance("MD5").digest(password.getBytes("UTF-8"))生成 MD5 哈希(教学场景够用),存储到数据库;登录时同样对输入密码做 MD5 再比对。 - 关键字段长度扩容:
ALTER TABLE user MODIFY password VARCHAR(32);(MD5 为 32 位十六进制字符串)。
从那以后我每次带学生做 JSP 实训,都强制走一遍“反编译 → 定位漏洞 → 事务加固 → 密码哈希”四步流程。不是为了炫技,而是让他们亲手撕开一层层封装,看见conn.prepareStatement()背后真实的字节码指令,理解为什么setAutoCommit(false)是事务的起点,明白getGeneratedKeys()不是魔法而是 JDBC 规范的契约。这套jsp.rar就像一台老式柴油机,零件裸露,油污可见,但正因如此,你才能看清动力如何从燃烧室传递到曲轴——而现代框架的黑匣子,往往掩盖了这些必经的磨损与热胀冷缩。
希望帮到你。
本文还有配套的精品资源,点击获取