JavaWeb底层实战:手写Servlet+JSP+MySQL实现MVC购书系统

发布时间:2026/9/28 8:22:18
JavaWeb底层实战:手写Servlet+JSP+MySQL实现MVC购书系统 简介本资源是一套完整的JavaWeb期末大作业级在线购书系统实现面向高校计算机类专业学生及Java初学者解决课程设计与期末项目选题难、代码质量参差、部署调试门槛高等实际问题。压缩包共75个文件含14个JSP页面负责视图展示、12个Java类Controller与Model层核心逻辑、12个Class字节码便于直接运行验证、3个SQL脚本含完整MySQL数据库建表与初始化数据、3个XML配置文件Servlet与MVC框架配置、16张界面截图直观呈现首页、登录、购物车、后台管理等模块效果以及项目说明文档、Eclipse工程配置文件和1个课程设计演示视频整体大小为47.77MB。已有349人学习下载资源代码注释详尽、结构清晰遵循MVC三层架构支持一键部署运行涵盖用户注册登录、图书浏览/搜索/下单、购物车管理、订单处理及后台图书/用户/订单管理等全业务流程是高分交付、快速上手的典型教学实践案例。1. 这不是“抄个源码交作业”而是一次对 JavaWeb 底层链路的完整穿刺用 JSPServletMySQL 搭建在线购书系统把 MVC 三层架构从教科书里拽出来跑通、调透、压稳你手头那份标着“满分项目”的javaweb期末大作业-在线购书系统 jspServletmysql采用mvc三层架构源码数据库绝不是拿来改个包名、换张首页图就能蒙混过关的压缩包。它是一套未经 Spring Boot 封装、裸露着 HTTP 协议握手、Servlet 生命周期、JDBC 连接池、JSP 翻译机制和 MySQL 事务边界的原始技术栈切片。很多同学在 IDEA 里点开index.jsp能看到页面但一加购物车就 404一登录就空指针一查订单就SQLException: No suitable driver found——问题不在代码而在你根本没摸清这串技术名词之间真实的咬合关系JSP 怎么把请求甩给 ServletServlet 又凭什么信任 DAO 层返回的 ResultSetModel 层的 JavaBean 是怎么被 EL 表达式“看懂”的MVC 不是三个字母而是三道防火墙View 不该碰 SQLController 不该写 HTMLService 更不该直接 new Connection。本篇不讲“怎么新建一个 Dynamic Web Project”而是带你从 Tomcat 启动日志第一行开始逐帧解析一次用户点击“加入购物车”后字节流如何穿越 7 层网络模型、触发 Servlet 实例化、执行事务回滚、最终渲染出带实时库存的 JSP 页面。适合正在赶毕设、被导师要求“必须手写 Servlet 不许用框架”、或想真正吃透 JavaWeb 底层脉络的开发者。别怕报错——那些HTTP Status 500和ClassNotFoundException正是你理解 MVC 真实边界的坐标原点。2. 从零初始化环境不是配 IDEA而是重建 Tomcat 与 MySQL 的信任链提示本章所有操作均基于Windows 10/11 JDK 8u291必须JDK 11 会导致部分 JSP 标签库兼容问题 Tomcat 9.0.83 MySQL 8.0.33。不要用最新版 Tomcat 10其 Jakarta EE 命名空间变更会让javax.servlet.*全面失效——这是你第一个血泪经验。2.1 Tomcat 9 的“静默启动”陷阱为什么你的 index.jsp 死活不加载很多同学在 IDEA 里右键Run on Server浏览器打开http://localhost:8080/却显示 404控制台却没有任何报错。这不是项目没部署而是 Tomcat 根本没加载你的 Web 应用。原因在于Tomcat 9 默认禁用自动部署autoDeploy且不扫描webapps下的 WAR 包。你必须手动干预# 进入 Tomcat 安装目录下的 conf 目录 cd C:\apache-tomcat-9.0.83\conf # 编辑 server.xml在 Host 标签内添加 Context path/bookstore docBaseC:\your-project-path\bookstore reloadabletrue /逻辑说明path/bookstore定义了应用上下文路径访问地址变为http://localhost:8080/bookstore/docBase必须是绝对路径指向你项目根目录即包含WEB-INF文件夹的目录reloadabletrue开启热重载——这是开发期救命开关但上线必须关掉。参数说明reloadable若为 false修改 JSP 后需重启 Tomcat若为 trueTomcat 会每 15 秒扫描 class 文件变化可调backgroundProcessorDelay但会显著增加 CPU 占用。2.2 MySQL 8 驱动的“SSL 强制握手”为什么连 localhost 都报Public Key Retrieval is not allowed当你在DBUtil.java里写下DriverManager.getConnection(jdbc:mysql://localhost:3306/bookstore, root, 123456)控制台立刻抛出SQLException: Public Key Retrieval is not allowed这不是密码错了而是 MySQL 8 默认启用caching_sha2_password认证插件并强制要求 SSL 连接。而mysql-connector-java-5.1.47.jarJSP 项目最常用版本根本不支持该协议。解决方案只有两个降级认证方式推荐开发期-- 用 root 登录 MySQL执行 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456; FLUSH PRIVILEGES;升级驱动并显式关闭 SSL更安全下载mysql-connector-java-8.0.33.jar注意不是 5.x 版本替换WEB-INF/lib/下旧驱动修改 JDBC URLString url jdbc:mysql://localhost:3306/bookstore?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue;关键参数说明useSSLfalse关闭 SSL开发环境可接受生产必须开serverTimezoneAsia/Shanghai解决时区错误The server time zone value ... is unrecognizedallowPublicKeyRetrievaltrue允许公钥检索绕过caching_sha2_password握手2.3 IDEA 中的“伪动态 Web 项目”为什么新建项目后没有 web.xmlIDEA 默认创建的是 Maven Web 项目但你的“满分源码”大概率是传统 Dynamic Web Project无 pom.xml。强行导入会导致web.xml缺失、lib依赖不识别。正确做法是File → New → Project → Java Enterprise → Web Application不勾选 Create project from archetype在向导中指定Target runtime Apache Tomcat 9.0.83必须提前在 IDEA Settings → Application Servers 中配置好勾选Add framework support → Java EE Web Module → Version 3.1对应 Servlet 3.1兼容 JSP 2.3完成后手动将源码中的src、WebContent或WebRoot文件夹拖入对应位置并确保WebContent/WEB-INF/web.xml存在且内容完整为什么必须是 Servlet 3.1因为你的源码中web.xml里servlet标签使用了load-on-startup1/load-on-startup这是 Servlet 3.0 规范才支持的特性。用 2.5 版本会直接忽略该配置导致 Servlet 延迟初始化首次请求极慢。3. MVC 三层的“物理分界”不是包名分层而是类加载器与线程上下文的真实隔离MVC 在教科书里是三个包com.bookstore.controller、com.bookstore.service、com.bookstore.dao。但在运行时它们是三道由 ClassLoader 和 ThreadLocal 构筑的墙。拆不穿这堵墙你就永远在“调用方法”而不是“践行架构”。3.1 Controller 层Servlet 不是“处理逻辑”而是“协调资源生命周期”看你的CartServlet.java它大概长这样public class CartServlet extends HttpServlet { private CartService cartService new CartService(); // 错大错 protected void doPost(HttpServletRequest request, HttpServletResponse response) { String action request.getParameter(action); if (add.equals(action)) { int bookId Integer.parseInt(request.getParameter(bookId)); cartService.addBookToCart(bookId); // 问题在这里 } } }致命错误new CartService()破坏了单例边界且无法注入 DAO 依赖。正确做法是利用 Servlet 容器的初始化机制public class CartServlet extends HttpServlet { private CartService cartService; Override public void init() throws ServletException { // 从 ServletContext 获取预初始化的服务实例模拟 IoC 容器 ServletContext context getServletContext(); this.cartService (CartService) context.getAttribute(cartService); if (this.cartService null) { throw new ServletException(CartService not initialized in ServletContext); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) { // ... 业务逻辑 cartService.addBookToCart(bookId); // 此时 cartService 是共享单例 } }逻辑说明init()方法在 Servlet 第一次被请求前执行且只执行一次。我们把 Service 实例挂到ServletContext整个 Web 应用的全局上下文上所有 Servlet 都能共享它。这避免了每次请求都 new 对象也保证了事务一致性后续 Service 层会用 ThreadLocal 绑定 Connection。参数说明ServletContext的生命周期 Tomcat 启动 → 应用部署 → 应用卸载 → Tomcat 关闭。比HttpSession用户级和HttpServletRequest请求级范围更大。3.2 Service 层事务不是“加个注解”而是 Connection 的线程绑定你的CartService.java如果直接 newCartDAO并调用insert()那事务根本不存在。真正的事务控制必须在 Service 层统一管理 Connectionpublic class CartService { private static final ThreadLocalConnection connectionHolder new ThreadLocal(); public void addBookToCart(int bookId) throws SQLException { Connection conn null; try { conn getConnection(); // 从 ThreadLocal 获取当前线程的 Connection conn.setAutoCommit(false); // 开启事务 // 调用 DAO传入同一 Connection CartDAO cartDAO new CartDAO(); cartDAO.insertCart(conn, bookId); BookDAO bookDAO new BookDAO(); bookDAO.updateStock(conn, bookId, -1); // 扣减库存 conn.commit(); // 提交事务 } catch (SQLException e) { if (conn ! null) conn.rollback(); // 回滚 throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); // 恢复自动提交 connectionHolder.remove(); // 清理 ThreadLocal防内存泄漏 } } } private Connection getConnection() throws SQLException { Connection conn connectionHolder.get(); if (conn null || conn.isClosed()) { conn DBUtil.getConnection(); // 从连接池获取新连接 connectionHolder.set(conn); } return conn; } }关键设计点ThreadLocalConnection确保同一线程内所有 DAO 操作共享同一 Connection这是事务原子性的物理基础connectionHolder.remove()是防内存泄漏的后悔药Tomcat 使用线程池线程会被复用若不清理旧 Connection 会一直挂在 ThreadLocal 上setAutoCommit(false)必须在getConnection()之后立即调用否则 DAO 的 insert 可能已自动提交3.3 DAO 层ResultSet 不是“数据”而是未解码的字节流你的BookDAO.java可能这样写public ListBook findAllBooks() throws SQLException { String sql SELECT * FROM books; PreparedStatement ps connection.prepareStatement(sql); ResultSet rs ps.executeQuery(); ListBook books new ArrayList(); while (rs.next()) { Book book new Book(); book.setId(rs.getInt(id)); // 这里可能翻车 book.setName(rs.getString(name)); books.add(book); } return books; }风险点rs.getInt(id)若数据库字段为BIGINT在 MySQL 8 驱动下会抛SQLDataException: Cannot convert to java.lang.Integer。正确写法必须严格匹配字段类型while (rs.next()) { Book book new Book(); book.setId(rs.getLong(id)); // 改为 getLong适配 BIGINT book.setName(rs.getString(name)); book.setPrice(rs.getBigDecimal(price)); // 价格必须用 BigDecimal防浮点误差 book.setStock(rs.getInt(stock)); // stock 是 INT用 getInt 安全 books.add(book); }为什么价格必须用BigDecimal因为float/double在数据库存储和 Java 计算中存在精度丢失。例如0.1 0.2 ! 0.3。购书系统涉及金额必须用BigDecimal保证计算准确。rs.getBigDecimal(price)直接映射 MySQL 的DECIMAL(10,2)类型无转换损耗。4. JSP 的“编译黑匣子”EL 表达式、JSTL 标签、自定义标签的底层执行链JSP 不是 HTML它是服务器端模板引擎。% request.getAttribute(books) %能输出对象是因为 Tomcat 在第一次访问时已将.jsp文件翻译成.java源码再编译成.class。这个过程藏满了玄学。4.1 EL 表达式失效的三大元凶pageContext、scope、getter 方法命名你在book_list.jsp里写c:forEach items${bookList} varbook ${book.name} !-- 显示为空 -- /c:forEach但后台request.setAttribute(bookList, books)已执行。问题往往出在Scope 错误bookList是request.setAttribute()设置的但 EL 默认从 page → request → session → application 逐级查找。若你在include的 JSP 中使用pageContext可能覆盖了 request scope。强制指定 scope${requestScope.bookList[0].name} !-- 明确从 request scope 取 --JavaBean 规范破坏Book类的getName()方法若写成getname()小写 nEL 就找不到。JSP 的 EL 解析器严格遵循 JavaBean 规范getXXX()→xxxisXXX()→xxx。检查你的Book.javapublic class Book { private String name; public String getName() { return name; } // ✅ 必须是 getName() // public String getname() { return name; } // ❌ EL 会忽略 }pageContext 的隐式对象污染若在header.jsp中写了% pageContext.setAttribute(title, 购书系统); %它会污染当前页面的pageContext导致后续${title}输出异常。解决方案所有pageContext.setAttribute()改为request.setAttribute()保持 scope 清晰。4.2 JSTL 标签库的“双 jar 诅咒”c.tld和jstl.jar必须同源你的web.xml里声明了% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %但页面报org.apache.jasper.JasperException: The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved。这不是 URI 写错了而是jstl.jar和standard.jar或taglibs-standard-impl.jar版本不匹配。JSTL 1.2 必须配jstl-1.2.jartaglibs-standard-impl-1.2.5.jar。常见错误组合错误组合后果jstl-1.2.jarstandard.jar旧版c:forEach报NoClassDefFoundError: javax/servlet/jsp/jstl/core/Configjstl-1.1.jartaglibs-standard-impl-1.2.5.jarc:if标签完全不解析页面原样输出c:if test...解决方案删除WEB-INF/lib/下所有jstl*和standard*jar下载jstl-1.2.jar和taglibs-standard-impl-1.2.5.jar来自 Apache Taglibs 官网重新部署重启 Tomcat4.3 自定义标签的“TLD 文件注册”为什么my:formatPrice标签不生效你想封装价格格式化逻辑写了PriceTag.java但my:formatPrice value${book.price}/不渲染。缺的不是 Java 类而是 TLDTag Library Descriptor注册在WEB-INF/下新建mytags.tld?xml version1.0 encodingUTF-8? taglib xmlnshttp://java.sun.com/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd version2.1 tlib-version1.0/tlib-version short-namemy/short-name urihttp://example.com/mytags/uri tag nameformatPrice/name tag-classcom.bookstore.tag.PriceTag/tag-class body-contentempty/body-content attribute namevalue/name requiredtrue/required rtexprvaluetrue/rtexprvalue /attribute /tag /taglib在 JSP 顶部声明% taglib prefixmy urihttp://example.com/mytags %关键参数说明rtexprvaluetrue表示该属性支持运行时表达式如${book.price}若为 false则只能写死值my:formatPrice value99.99/body-contentempty表示该标签无体内容即不能写my:formatPrice.../my:formatPrice5. 避坑那些让“满分项目”在答辩现场集体翻车的 5 个真实场景这些不是理论问题而是我在三次毕设答辩现场亲眼所见、亲手修复的血泪记录。每一条都对应一个HTTP 500或白屏。5.1 现象登录成功后跳转到login.jspURL 变成http://localhost:8080/bookstore/login.jsp?error1但页面不显示错误信息原因login.jsp中用% request.getParameter(error) %获取参数但response.sendRedirect(login.jsp?error1)发送的是 302 重定向浏览器发起新请求原 request 对象已销毁getParameter(error)返回 null。解决改用request.setAttribute(error, 用户名或密码错误)request.getRequestDispatcher(login.jsp).forward(request, response)用forward保持 request 上下文。5.2 现象添加图书到购物车后库存没扣减但购物车里有商品原因CartService.addBookToCart()中调用了bookDAO.updateStock()但该方法内部又new DBUtil().getConnection()获取了新 Connection与 Service 层的 Connection 不是同一个事务失效。解决DAO 方法必须接收Connection参数updateStock(Connection conn, int bookId, int delta)禁止 DAO 自己获取连接。5.3 现象在book_detail.jsp中点击“加入购物车”控制台报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver原因MySQL 8 驱动类名已从com.mysql.jdbc.Driver变更为com.mysql.cj.jdbc.Driver但你的DBUtil.java中仍写Class.forName(com.mysql.jdbc.Driver)。解决更新为Class.forName(com.mysql.cj.jdbc.Driver)并确认WEB-INF/lib/下是mysql-connector-java-8.0.33.jar。5.4 现象order_list.jsp中用c:forEach遍历订单列表但页面空白无任何报错原因Order实体类中getOrderDate()返回java.util.Date但 JSTL 的c:forEach在遍历时会调用toString()而Date.toString()输出Wed Dec 25 10:30:45 CST 2024其中空格和冒号被 JSP 解析器误认为标签结束。解决在Order类中重写toString()或改用SimpleDateFormat格式化后存入 requestSimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); request.setAttribute(formattedDate, sdf.format(order.getOrderDate()));5.5 现象Tomcat 启动后http://localhost:8080/bookstore/显示 404但http://localhost:8080/manager/html能进 Tomcat Manager原因web.xml中welcome-file-list配置了index.html但你的首页是index.jsp且未在welcome-file-list中声明。解决在web.xml的welcome-file-list内添加welcome-file-list welcome-fileindex.jsp/welcome-file welcome-fileindex.html/welcome-file /welcome-file-list6. 进阶验证用 Postman MySQL Workbench Tomcat 日志三线程交叉验证 MVC 数据流写完代码不验证等于没写。真正的“满分”不是界面漂亮而是你能用工具证明每一个 HTTP 请求都精准命中了 Controller → Service → DAO → MySQL 的每一环且数据在各层间零损耗传递。6.1 用 Postman 模拟真实用户行为捕获 Servlet 入口流量不要只在浏览器点点点。用 Postman 发送结构化请求观察响应头和 body请求URLMethodBody (x-www-form-urlencoded)验证点登录http://localhost:8080/bookstore/LoginServletPOSTusernameadminpassword123456响应状态码 302Location头是否为/bookstore/home.jsp加购http://localhost:8080/bookstore/CartServletPOSTactionaddbookId101响应 body 是否含success:true需在 Servlet 中response.getWriter().print({\success\:true})查订单http://localhost:8080/bookstore/OrderServletGET—响应是否为 JSON 数组[{orderId:20241225001,total:99.99}]关键技巧在LoginServlet.doPost()开头加日志System.out.println([DEBUG] LoginServlet received: username username , password password);启动 Tomcat 后观察catalina.out日志确认请求是否真的到达 Servlet——这是排查 404 的第一道防线。6.2 用 MySQL Workbench 实时监控事务执行验证 DAO 层 SQL 正确性在 Workbench 中执行-- 开启通用查询日志临时 SET GLOBAL general_log ON; SET GLOBAL log_output TABLE; -- 查看最近 10 条 SQL SELECT * FROM mysql.general_log ORDER BY event_time DESC LIMIT 10;然后在浏览器操作“下单”立即在 Workbench 中刷新general_log表。你将看到类似INSERT INTO orders (user_id, total, create_time) VALUES (1, 99.99, 2024-12-25 10:30:45); UPDATE books SET stock stock - 1 WHERE id 101;这证明DAO 层生成的 SQL 语法正确无拼写错误事务确实跨了多条 SQLINSERTUPDATE在同一事务中stock字段被正确扣减而非设为固定值若只看到INSERT没有UPDATE说明 Service 层事务回滚了需检查updateStock()是否抛异常。6.3 用 Tomcat Manager 的“应用重载”功能验证热部署可靠性在web.xml中添加一个测试 servlet 映射servlet servlet-nameTestServlet/servlet-name servlet-classcom.bookstore.test.TestServlet/servlet-class /servlet servlet-mapping servlet-nameTestServlet/servlet-name url-pattern/test/url-pattern /servlet-mapping然后访问http://localhost:8080/bookstore/test确认返回 “Test OK”。接着不重启 Tomcat直接修改TestServlet.java中的输出字符串保存再到 Tomcat Manager 页面点击 “Reload” 按钮。刷新/test页面确认内容已更新。这验证了reloadabletrue配置生效Tomcat 的类加载器能正确卸载旧 class、加载新 class你的项目结构符合 Tomcat 的热部署规范class 文件在WEB-INF/classes/下非 jar 包内6.4 最后一道防线用jstack抓取线程快照确认无 Connection 泄漏当系统运行 1 小时后页面变慢、数据库连接数飙升执行# 找到 Tomcat 的 Java 进程 PID jps -l # 抓取线程堆栈PID 替换为实际值 jstack 12345 tomcat-thread-dump.txt在tomcat-thread-dump.txt中搜索java.sql.Connection若发现大量java.lang.Thread.State: WAITING状态的线程且堆栈指向DBUtil.getConnection()说明 Connection 未被close()。此时必须检查所有 DAO 方法确保finally块中有finally { if (rs ! null) try { rs.close(); } catch (SQLException e) {} if (ps ! null) try { ps.close(); } catch (SQLException e) {} if (conn ! null) try { conn.close(); } catch (SQLException e) {} // 关键 }我的血泪习惯在DBUtil.closeAll()方法中对conn的close()调用前加一行日志System.out.println([CONNECTION CLOSED] Thread: Thread.currentThread().getName());这样每次连接关闭日志里都有迹可循。希望帮到你。本文还有配套的精品资源点击获取