news 2026/10/26 22:47:59

JavaWeb MVC实战:JSP+Servlet+MySQL购书系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb MVC实战:JSP+Servlet+MySQL购书系统

简介:这是一份面向JavaWeb初学者与高校计算机专业学生的期末大作业级实战项目,完整实现基于JSP+Servlet+MySQL的在线购书系统,严格遵循MVC三层架构设计,覆盖用户注册登录、图书浏览/搜索、购物车管理、订单提交与后台图书管理等核心电商功能,兼顾界面美观性与操作便捷性,特别适合作为课程设计或高分结课项目。资源包共75个文件,含14个JSP页面(前端交互)、12个Java类(Controller与Service逻辑)、12个Class字节码(编译验证)、3个SQL脚本(含完整数据库建表与初始化数据)、3个XML配置文件(web.xml及框架配置)、16张界面截图与1个项目演示MP4视频,整体压缩包大小为47.77MB。已有349人学习下载,资源代码注释详尽、结构清晰、部署简易,附带README说明与项目展示视频,新手可快速理解模块职责与调用流程,是深入掌握JavaWeb开发全流程的优质实践范例。

1. 这不是“抄作业”,是能跑通的 MVC 实战闭环:一个带完整数据库、注释拉满、IDEA 可一键部署的 JSP+Servlet+MySQL 在线购书系统

你交的 JavaWeb 期末大作业,是不是还在用index.jsp里硬写 SQL、<% %>里拼 HTML、登录校验靠 session.setAttribute("user", "admin") 然后全项目到处 getAttribute?别慌——这个「在线购书系统」不是模板套壳,它是一套真实跑在 Tomcat 上、三层职责清晰、DAO 层封装了 Connection 获取与关闭、Service 层做了事务控制、Controller 层严格遵循 Servlet 规范、JSP 页面只做展示不掺逻辑的可调试、可打断点、可改需求的 MVC 实战样本。它解决的不是“怎么交差”,而是“怎么让老师一眼看出你真懂 MVC 分层价值”:比如用户下单时库存扣减和订单生成必须原子性,它用Connection.setAutoCommit(false)+try-catch-rollback实现;比如图书列表分页,它没用前端 JS 模拟,而是BookService.listByPage(pageNum, pageSize)真查数据库;比如管理员后台的图书增删改,每个操作都有独立 Servlet 处理,URL 路由清晰(/admin/addBook,/admin/deleteBook?id=123)。适合刚学完 JDBC 和 Servlet 生命周期、正卡在“知道 MVC 概念但写不出分层代码”的人——下载解压后,按本文第 3 章配好 MySQL 和 Tomcat,mvn clean compile(或直接导入 IDEA)→ 启动 → 访问http://localhost:8080/bookstore,首页就弹出来,不用改一行代码。这不是玩具 demo,是老师打分时会圈出“DAO 层异常处理规范”“Servlet doGet/doPost 职责分离合理”的满分项目。


2. 从零部署:环境准备、数据库初始化与项目导入三步落地

2.1 环境清单与版本兼容性确认(别跳这步!)

这个项目基于传统 JavaWeb 技术栈,不依赖 Maven 仓库自动拉包,所有 jar 包已打包进WEB-INF/lib/目录。但环境版本必须对齐,否则你会卡在ClassNotFoundException或NoClassDefFoundError:

  • JDK 版本:必须为JDK 8(1.8)。项目.classpath文件中明确指定<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>。若你本地是 JDK 17,Tomcat 启动时会报Unsupported major.minor version 52.0(这是 JDK 8 的 class 文件版本号),必须切回 JDK 8。
  • Tomcat 版本:推荐Tomcat 8.5.x 或 9.0.x。项目web.xml中<web-app>声明为version="3.1",对应 Servlet 3.1 规范,Tomcat 7 不支持(其最高为 Servlet 3.0)。
  • MySQL 版本:5.7.x 或 8.0.x 均可。SQL 文件mobiledatabase.sql使用标准 ANSI SQL,无存储过程或窗口函数,兼容性极强。但注意 MySQL 8.0 默认认证插件为caching_sha2_password,而项目使用的mysql-connector-java-5.1.47.jar(在WEB-INF/lib/下)不支持该插件,需手动修改 MySQL 用户认证方式(见 2.2 节)。

提示:不要用最新版 IDEA 直接“Open”项目根目录!它会误判为普通文件夹。正确做法是选File → New → Project from Existing Sources...,指向解压后的main文件夹(即含.project和src的目录),IDEA 会自动识别为 Eclipse Web Project。

2.2 数据库初始化:建库、建表、导入数据、修复 MySQL 8.0 认证问题

项目提供mobiledatabase.sql,但直接执行会失败——尤其在 MySQL 8.0+ 环境下。以下是安全初始化流程:

# 1. 登录 MySQL(假设 root 密码为 '123456') mysql -u root -p # 2. 创建数据库(字符集必须为 utf8mb4,避免中文乱码) CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 3. 切换到新库 USE bookstore; # 4. 执行 SQL 文件(注意路径要替换成你本地的实际路径) SOURCE /path/to/your/download/mobiledatabase.sql;

如果执行SOURCE时卡住或报错ERROR 1045 (28000): Access denied for user 'root'@'localhost',大概率是 MySQL 8.0 认证插件不兼容。解决方案(仅针对 MySQL 8.0+):

-- 登录 MySQL 后执行(需 root 权限) ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;

参数说明:mysql_native_password是旧版认证插件,mysql-connector-java-5.1.47.jar唯一支持的类型。FLUSH PRIVILEGES强制刷新权限缓存,否则修改不生效。此操作不影响安全性,仅解决连接兼容性。

mobiledatabase.sql创建了 5 张核心表:

  • users:用户账号密码(明文存储!教学项目,非生产环境)
  • books:图书信息(ISBN、书名、作者、价格、库存)
  • orders:订单主表(订单号、用户ID、总金额、状态)
  • order_items:订单明细(关联订单与图书,含购买数量)
  • admin_users:管理员账号(用于后台登录)

所有表均设定了ENGINE=InnoDB和DEFAULT CHARSET=utf8mb4,确保事务支持与中文存储。

2.3 IDEA 中导入并配置 Tomcat 运行

  1. 导入项目:File → New → Project from Existing Sources...→ 选择解压后的main文件夹 → 选中Import project from external model→ 选Eclipse→ Finish。
  2. 配置 Tomcat:
    • Run → Edit Configurations...→+→Tomcat Server → Local
    • Application server:点击Configure...,指向你的 Tomcat 安装目录(如D:\apache-tomcat-8.5.99)
    • Deployment标签页 →+→Artifact→ 选bookstore:war exploded(项目名可能显示为bookstore或main,认准war exploded类型)
    • Application context改为/bookstore(这样访问地址就是http://localhost:8080/bookstore)
  3. 设置 JDK:
    • File → Project Structure → Project→Project SDK选 JDK 1.8
    • Project language level选8 - Lambdas, type annotations etc.
  4. 启动:点击绿色三角形 ▶️,等待 Tomcat 启动日志出现INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [xxx] milliseconds,浏览器打开http://localhost:8080/bookstore即可见首页。

注意:首次启动时,IDEA 会自动编译src下的 Java 文件到out/artifacts/bookstore_war_exploded/WEB-INF/classes/。若页面 404,请检查Deployment中的Application context是否为/bookstore,且Artifact是否选对war exploded。


3. MVC 三层拆解:看懂每一层在做什么、为什么这么分

3.1 Controller 层:Servlet 是路由中枢,不是业务处理器

项目中所有请求入口都是Servlet,位于src/com/bookstore/servlet/下。以用户登录为例:

// src/com/bookstore/servlet/LoginServlet.java public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); // 依赖 Service 层 @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 调用 Service 层验证用户 User user = userService.login(username, password); if (user != null) { // 2. 登录成功:存入 session,重定向到首页 request.getSession().setAttribute("user", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { // 3. 登录失败:设置错误信息,转发回登录页 request.setAttribute("msg", "用户名或密码错误!"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }

关键设计点:

  • doPost只做三件事:取参 → 调 Service → 控制跳转。绝不写 SQL、不操作数据库、不拼 HTML。
  • request.getSession().setAttribute("user", user)将用户对象存入 Session,供后续页面(如header.jsp)通过 EL 表达式${sessionScope.user.username}获取。
  • response.sendRedirect(...)是客户端重定向(URL 变),request.getRequestDispatcher(...).forward(...)是服务器端转发(URL 不变),此处登录成功用重定向,避免刷新重复提交。

web.xml中的映射定义了 URL 路由:

<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.bookstore.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> <!-- 访问 /login 即触发此 Servlet --> </servlet-mapping>

3.2 Service 层:业务逻辑的“守门人”,事务与规则在此集中

src/com/bookstore/service/下的类是真正的业务大脑。以OrderService.placeOrder()为例:

// src/com/bookstore/service/OrderService.java public class OrderService { private OrderDao orderDao = new OrderDao(); private BookDao bookDao = new BookDao(); public boolean placeOrder(User user, List<OrderItem> items) { Connection conn = null; try { conn = JdbcUtil.getConnection(); // 从工具类获取连接 conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 1. 保存订单主表 String orderId = "ORD" + System.currentTimeMillis(); // 简单生成订单号 Order order = new Order(orderId, user.getId(), getTotalAmount(items), "待支付"); orderDao.save(conn, order); // 2. 保存订单明细,并扣减库存 for (OrderItem item : items) { orderDao.saveItem(conn, item); // 保存明细 bookDao.updateStock(conn, item.getBookId(), -item.getQuantity()); // 库存减 } conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); // 任一环节失败,回滚 } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { JdbcUtil.close(conn); // 必须关闭连接 } } }

为什么必须有 Service 层?

  • 事务控制:订单创建涉及多张表(orders,order_items,books),必须原子性。DAO 层只负责单表 CRUD,事务边界在 Service 层划定。
  • 业务规则集中:库存扣减逻辑(bookDao.updateStock(...))与订单生成耦合,若放在 Servlet 里,下次加“库存不足时提示”就得改两个地方;放在 Service 里,只需改placeOrder()方法。
  • 复用性:同一个placeOrder()方法,既被OrderServlet调用,也可被后台定时任务调用,无需重复写逻辑。

3.3 DAO 层:数据库操作的“搬运工”,屏蔽 JDBC 细节

src/com/bookstore/dao/下的类只做一件事:把 Java 对象和 SQL 语句来回转换。以BookDao为例:

// src/com/bookstore/dao/BookDao.java public class BookDao { public List<Book> findAll(Connection conn) throws SQLException { String sql = "SELECT id, isbn, title, author, price, stock, image FROM books"; PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery(); List<Book> books = new ArrayList<>(); while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setIsbn(rs.getString("isbn")); book.setTitle(rs.getString("title")); book.setAuthor(rs.getString("author")); book.setPrice(rs.getDouble("price")); book.setStock(rs.getInt("stock")); book.setImage(rs.getString("image")); books.add(book); } rs.close(); ps.close(); return books; // 返回纯 POJO 列表,不含 Connection 或 ResultSet } }

DAO 层的契约精神:

  • 所有方法参数都接收Connection,由调用方(Service)统一管理连接生命周期,DAO 不自己getConnection()。
  • 所有方法返回值都是领域对象(Book,User,Order)或集合,绝不返回ResultSet或PreparedStatement。
  • 异常只抛SQLException,不捕获,让 Service 层决定如何处理(重试、回滚、记录日志)。

4. 避坑指南:五个血泪经验总结,专治部署失败、功能失灵、老师质疑

4.1 现象:启动 Tomcat 后访问http://localhost:8080/bookstore显示 404,但控制台无报错

原因:IDEA 的 Artifact 配置未指向war exploded,或Application context设置为空(默认为/),导致项目部署在根路径,而index.jsp实际在/bookstore/下。
解决:Run → Edit Configurations...→Deployment标签页 → 确认Artifact是bookstore:war exploded(不是bookstore:war),且Application context明确填写/bookstore。

4.2 现象:登录时提示 “java.lang.ClassNotFoundException: com.mysql.jdbc.Driver”

原因:项目使用mysql-connector-java-5.1.47.jar,其 Driver 类名为com.mysql.jdbc.Driver,但 MySQL 8.0+ 的官方驱动类名已改为com.mysql.cj.jdbc.Driver。虽然 jar 包自带,但若你手动替换了更高版本驱动,就会报此错。
解决:删除WEB-INF/lib/下所有mysql-connector*.jar,只保留项目自带的mysql-connector-java-5.1.47.jar。检查src/com/bookstore/util/JdbcUtil.java中Class.forName("com.mysql.jdbc.Driver");未被修改。

4.3 现象:图书列表页中文显示为??,数据库里存的是乱码

原因:MySQL 服务端、数据库、表、连接四层字符集未统一为utf8mb4,或 JDBC 连接 URL 缺少characterEncoding=utf8mb4参数。
解决:

  1. 确认 MySQL 配置文件my.ini(Windows)或my.cnf(Linux)中有:
    [client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
  2. 重启 MySQL 服务。
  3. 检查JdbcUtil.java中连接 URL:
    String url = "jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8";

4.4 现象:管理员后台添加图书时,图片上传失败,image字段存入数据库为null

原因:JSP 页面使用了input type="file",但 Servlet 未用Apache Commons FileUpload解析 multipart 请求,而是直接request.getParameter("image")—— 这永远返回null。
解决:该项目实际未实现图片上传功能!image字段是手动插入 SQL 的字符串路径(如"images/java-web.jpg"),前端img src="${book.image}"直接引用。若你误以为支持上传,请勿修改addBook.jsp的 form enctype,否则request.getParameter()会失效。正确做法:将图片放入web/images/目录,SQL 中写相对路径。

4.5 现象:下单后库存未减少,或订单明细未生成

原因:OrderService.placeOrder()中的事务未生效,常见于conn.setAutoCommit(false)后忘记conn.commit(),或catch块中rollback()未执行(因conn为 null)。
解决:

  • 在placeOrder()方法开头加日志:System.out.println("Starting transaction...");
  • 在conn.commit()后加日志:System.out.println("Transaction committed.");
  • 在catch块中加System.out.println("Transaction rolled back.");
  • 若只看到 start 日志,看不到 commit 日志,说明执行到某处抛异常,检查bookDao.updateStock()的 SQL 是否语法错误(如字段名写错)。

5. 功能验证与高分技巧:三个必测场景 + 一个让老师眼前一亮的改造

5.1 三类核心场景验证清单(动手前先过一遍)

场景操作步骤预期结果验证点
用户购物流程1. 访问/bookstore/login.jsp,用admin/123登录
2. 浏览图书,点击“加入购物车”
3. 点击“去结算”,填写收货信息提交
1. 登录后 header 显示“欢迎 admin”
2. 购物车右上角数字+1
3. 订单生成后,orders表新增记录,order_items表新增明细,books表对应图书stock减1
检查数据库bookstore的三张表数据是否同步变更
管理员后台管理1. 访问/bookstore/admin/login.jsp,用admin/123登录
2. 点击“图书管理” → “添加图书”,填 ISBN/书名/价格/库存
3. 点击“用户管理”,查看用户列表
1. 添加后列表立即显示新书
2.books表新增一行,image字段为手动填的路径(如"images/spring-boot.jpg")
3.users表数据可读,无敏感信息泄露
确认admin/下所有 Servlet 的web.xml映射正确,且AdminFilter(若有)拦截有效
异常流程防御1. 在商品详情页,手动修改 URL 中id=123为id=999999(不存在的 ID)
2. 尝试用错误密码登录
3. 库存为 0 的图书点击“加入购物车”
1. 显示“商品不存在”提示页(bookDetail.jsp有空指针判断)
2. 登录页显示红色错误提示“用户名或密码错误!”
3. 加入购物车按钮置灰或提示“库存不足”
检查BookServlet、LoginServlet中的if (book == null)和if (user == null)判断是否健壮

5.2 让老师刮目相看的改造:给订单增加“支付状态”与“取消订单”功能

原项目订单状态只有“待支付”,缺乏电商核心状态流转。只需 30 分钟,就能补全这个高分点:

Step 1:扩展orders表

ALTER TABLE orders ADD COLUMN pay_status ENUM('unpaid', 'paid', 'cancelled') DEFAULT 'unpaid', ADD COLUMN cancel_reason VARCHAR(255) NULL;

Step 2:新增PayServlet(处理支付)

// src/com/bookstore/servlet/PayServlet.java public class PayServlet extends HttpServlet { private OrderService orderService = new OrderService(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String orderId = request.getParameter("orderId"); // 模拟支付成功(实际应对接支付网关) if (orderService.payOrder(orderId)) { request.setAttribute("msg", "支付成功!"); } else { request.setAttribute("msg", "支付失败,请重试"); } request.getRequestDispatcher("/orderDetail.jsp?orderId=" + orderId).forward(request, response); } }

Step 3:在orderDetail.jsp添加按钮

<c:if test="${order.payStatus == 'unpaid'}"> <form action="${pageContext.request.contextPath}/pay" method="post"> <input type="hidden" name="orderId" value="${order.orderId}"> <button type="submit">立即支付</button> </form> <a href="${pageContext.request.contextPath}/cancelOrder?orderId=${order.orderId}">取消订单</a> </c:if>

Step 4:实现CancelOrderServlet(关键:恢复库存)

// src/com/bookstore/servlet/CancelOrderServlet.java public class CancelOrderServlet extends HttpServlet { private OrderService orderService = new OrderService(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String orderId = request.getParameter("orderId"); if (orderService.cancelOrder(orderId)) { request.setAttribute("msg", "订单已取消,库存已恢复"); } request.getRequestDispatcher("/orderList.jsp").forward(request, response); } }

为什么这个改造加分?它体现了你对业务状态机的理解(unpaid → paid → cancelled)、对数据一致性的把控(取消订单必须回滚库存)、以及对MVC 分层的熟练运用(状态变更逻辑在 Service,UI 控制在 JSP,路由在 Servlet)。老师批注时会写:“状态设计合理,事务处理严谨”。


6. 从那以后我每次交 JavaWeb 大作业,都强制走一遍这三步验证

第一,启动前必查web.xml和JdbcUtil.java:web.xml里所有<servlet-mapping>的url-pattern是否与 JSP 表单的action匹配(比如action="login"对应<url-pattern>/login</url-pattern>);JdbcUtil.java的url、username、password是否与你本地 MySQL 一致,characterEncoding参数是否带上。有一次我因为复制粘贴漏了&serverTimezone=GMT%2B8,时间戳全错成 1970 年,debug 两小时才发现。

第二,功能测试必过“脏数据”关:不只测正常流程,一定手动构造异常输入——在登录框输' OR '1'='1测试 SQL 注入防护(本项目未做预编译,所以会失败,但你要知道这是个漏洞);在 URL 里把id=1改成id=-1看是否报 500(应该友好提示);上传一个超大图片文件(100MB)看是否内存溢出(本项目无上传,但你要意识到边界)。老师最看重的不是功能多炫,而是你有没有工程思维。

第三,答辩前必导出一份“干净版”:删掉所有System.out.println()日志,注释掉调试用的Thread.sleep(5000),把README.md里的部署步骤重写成“三步法”(1. 配 JDK8 2. 执行 SQL 3. IDEA 导入),再截图首页、后台、订单页各一张。答辩时老师问“你最大的收获”,我就说:“以前觉得 MVC 是三个文件夹,现在明白它是责任契约——Servlet 不碰 SQL,Service 不管跳转,DAO 不管事务。这份代码让我第一次写出能被别人接手的 JavaWeb。”

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 22:47:37

OpenClaw中文乱码解决指南:从字符编码到UTF-8的排查路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 4:17:25

从零搞懂Socket:TCP通讯编程原理与C#/Python实战避坑指南

1. 从一次车间调试说起&#xff1a;为什么搞懂socket是绕不过去的事大概两年前&#xff0c;我在一个自动化项目现场遇到过这么一件事。客户产线上有一台MCGS触摸屏&#xff0c;要跨网段去读西门子S7-1500 PLC的数据&#xff0c;中间还隔着一台工控机做数据中转。当时设备厂家的…

作者头像 李华
网站建设 2026/10/13 6:38:50

npm 安装 OpenClaw 后 401 报错:把 endpoint 改到 TaoToken 的排查记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华