资讯详情

JSP+Servlet+MySQL实现MVC三层架构的在线购书系统

📅 2026/10/8 19:35:23 | 华诺云谱 👁 阅读
JSP+Servlet+MySQL实现MVC三层架构的在线购书系统
简介这是一份面向JavaWeb初学者与高校课程设计学生的完整在线购书系统实战项目采用JSPServletMySQL实现MVC三层架构覆盖用户注册登录、图书浏览、购物车管理、订单生成与后台图书管理等核心电商功能助力期末大作业高效交付与高分呈现。资源包共75个文件含14个JSP页面前端交互、12个Java类Controller与DAO逻辑、12个Class字节码编译验证、3个SQL脚本数据库建表与初始化、1个MP4项目演示视频及README说明文档辅以CSS样式与图片资源整体压缩后47.77MB结构规范、注释详尽新手可快速部署运行。已有349人学习下载配套视频直观展示系统全流程操作源码按MVC分层组织清晰数据库脚本开箱即用特别适合零基础学生理解Web开发全流程与工程化实践。1. 这不是“抄个JSP页面就能交差”的期末作业一个真正跑得通、改得动、能 debug 的在线购书系统为什么必须用 JSPServletMySQL 搭 MVC 三层架构你手头那份被标为“满分项目”的《在线购书系统》源码如果只是把 index.jsp 放 Tomcat 里一扔就弹出首页那它连及格线都没摸到——真正的 JavaWeb 期末大作业核心不在“能显示”而在“能拆解、能修改、能定位问题”。我带过 7 届学生做 JavaWeb 课设90% 的翻车现场都发生在点击“加入购物车”报 500 错误却找不到是 Servlet 没映射、DAO 层 SQL 写错、还是 MySQL 连接池爆了改个用户登录逻辑结果订单页直接 404因为没理清 Controller 转发路径和 JSP 页面跳转的耦合关系。这个项目之所以被反复标注“满分”恰恰因为它用最朴素的 JSPServletMySQL 组合把 MVC 三层表现层 JSP、控制层 Servlet、数据层 JDBCDAO的职责边界划得清清楚楚JSP 只负责渲染不写业务逻辑Servlet 只做请求分发和参数校验不碰 SQLMySQL 表结构设计预留了扩展字段比如book.is_deleted和user.last_login_time不是为应付检查而是为后续加“软删除”或“登录统计”留活口。它适合两类人一是刚学完 Servlet 生命周期、还没碰 Spring 的大三学生需要一个能亲手打断点、看 request.getParameter() 值、单步进 DAO 方法的“黑匣子”二是想验证自己是否真懂 JavaWeb 底层通信机制的自学者——当你在浏览器地址栏敲下http://localhost:8080/bookstore/login?usernameadminpassword123你能清晰说出 Tomcat 如何把 URL 解析成 Servlet 映射、如何调用doPost()、如何通过RequestDispatcher转发到 success.jsp而不是依赖 IDEA 自动配置的WebServlet(/login)注解。这不是怀旧是夯实地基。2. 从零启动用最简环境跑通“用户登录-查看图书-下单”主链路2.1 环境准备拒绝“一键安装包”手动配 JDKTomcatMySQL 才能看清每一步依赖很多同学卡在第一步IDEA 里新建 Dynamic Web Project 后右键 Run on Server 报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。这不是 jar 包没导对而是根本没搞清类加载路径。正确做法是手动配不依赖 IDE 自动补全# 1. JDK 8u291必须JSP EL 表达式在 JDK 11 有兼容性问题 $ java -version java version 1.8.0_291 # 2. Tomcat 9.0.83Tomcat 10 默认用 Jakarta EE 命名空间JSP 标签库会报错 $ cd /opt/tomcat/bin $ ./startup.sh # Linux/macOSWindows 用 startup.bat # 3. MySQL 5.7.44注意MySQL 8.0 默认启用 caching_sha2_password 认证插件JDBC 8.0 驱动才支持但本项目用的是 mysql-connector-java-5.1.47.jar $ mysql -u root -p mysql CREATE DATABASE bookstore CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql CREATE USER bookuserlocalhost IDENTIFIED BY bookpass123; mysql GRANT ALL PRIVILEGES ON bookstore.* TO bookuserlocalhost; mysql FLUSH PRIVILEGES;提示mysql-connector-java-5.1.47.jar必须放在WEB-INF/lib/目录下不能只丢在 IDEA 的 Libraries 里。Tomcat 启动时只扫描WEB-INF/lib下的 jar这是 ClassLoader 的双亲委派机制决定的——你改了驱动版本却没更新 lib 目录重启 Tomcat 也无效。2.2 数据库初始化执行建表脚本前先确认字符集和引擎项目给的bookstore.sql脚本常被直接source bookstore.sql导入结果中文乱码或外键失效。必须先检查脚本头部声明-- bookstore.sql 开头必须有这两行否则 utf8mb4 不生效 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS 0; CREATE TABLE users ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT BCrypt 加密后的密码, email varchar(100) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 关键ENGINEInnoDB支持事务和外键不是 MyISAM -- CHARSETutf8mb4支持 emoji 和生僻字不是 utf8MySQL 中的 utf8 实际是 utf8mb3导入后立刻验证mysql USE bookstore; mysql SHOW CREATE TABLE users\G # 输出中必须看到 ENGINEInnoDB 和 DEFAULT CHARSETutf8mb4 mysql SELECT character_set_database, collation_database; # 必须返回 utf8mb4 和 utf8mb4_unicode_ci2.3 项目结构还原MVC 三层不是目录名是代码职责的物理隔离把源码解压后别急着 run。先打开src/目录确认三层结构是否真实存在src/ ├── controller/ # Servlet 类命名如 LoginServlet.java只含 doGet/doPost 和 request.setAttribute() ├── service/ # 业务逻辑如 UserServiceImpl.java调用 DAO处理密码加密、库存校验等 ├── dao/ # 数据访问如 BookDao.java只写 JDBC 操作SQL 字符串硬编码或用 properties ├── entity/ # POJO如 User.java字段与数据库表严格一一对应含 getter/setter └── util/ # 工具类如 DBUtil.java封装 Connection 获取和关闭、BCryptUtil.java密码加盐关键区别如果你在controller/LoginServlet.java里看到Connection conn DriverManager.getConnection(...)或PreparedStatement ps conn.prepareStatement(UPDATE users SET ...)这就是典型的 MVC 失败——DAO 层被绕过了。正确的流程是Servlet → Service → DAO → DBUtil。2.4 最小可运行链路只启动登录功能屏蔽其他模块干扰先删掉所有无关文件只保留web/login.jsp登录表单action/loginsrc/controller/LoginServlet.java处理/login请求src/service/UserService.java和src/service/impl/UserServiceImpl.javasrc/dao/UserDao.java和src/dao/impl/UserDaoImpl.javasrc/entity/User.javasrc/util/DBUtil.java然后在LoginServlet.java的doPost()里打个断点启动 Tomcat用 curl 测试curl -X POST http://localhost:8080/bookstore/login \ -H Content-Type: application/x-www-form-urlencoded \ -d usernameadmin -d password123观察控制台输出是否进入doPost()request.getParameter(username)是否拿到值userService.login()返回 true 还是 false只有这一步通了再加“查图书列表”和“下单”功能才有意义。别一上来就跑整个系统——那是生产环境思维不是调试思维。3. MVC 三层落地细节JSP 不写 Java 代码、Servlet 不拼 SQL、DAO 不管业务逻辑3.1 JSP 层EL 表达式和 JSTL 是底线scriptlet% %是红线index.jsp里如果出现% ListBook books (ListBook) request.getAttribute(bookList); for(Book b : books) { % div% b.getTitle() %/div % } %这就是灾难。正确写法必须用 EL JSTL% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !-- 引入 JSTL 核心标签库 -- c:forEach items${bookList} varbook div${book.title}/div div¥${book.price}/div /c:forEach为什么必须这样EL${book.title}会自动调用getTile()且对 null 安全不会抛 NullPointerException而% b.getTitle() %在 b 为 null 时直接 500 错误。JSTLc:forEach是编译期检查IDEA 能提示bookList是否存在scriptlet 是运行时执行错语法只能等页面报错。更重要的是JSP 一旦混入 Java 代码就彻底违背了“表现层只负责展示”的 MVC 原则——你改个价格显示格式就得动 Java 逻辑耦合度爆炸。3.2 Servlet 层请求分发是核心业务校验是边界LoginServlet.java的doPost()必须长这样protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取参数职责解析 HTTP 请求 String username request.getParameter(username); String password request.getParameter(password); // 2. 基础校验职责参数合法性非业务逻辑 if (username null || username.trim().isEmpty() || password null || password.length() 6) { request.setAttribute(errorMsg, 用户名或密码格式错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; // 必须 return否则继续执行 } // 3. 交给 Service 处理职责仅分发不碰业务规则 UserService userService new UserServiceImpl(); User user userService.login(username, password); // 这里才校验密码是否正确、账号是否禁用 if (user ! null) { // 登录成功存 session重定向到首页 request.getSession().setAttribute(currentUser, user); response.sendRedirect(request.getContextPath() /index.jsp); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } }血泪经验return语句不能少没有它forward 之后还会执行后续代码导致IllegalStateException: Cannot forward after response has been committed。response.sendRedirect()用于登录成功避免刷新重复提交request.getRequestDispatcher().forward()用于登录失败保持 URL 不变方便用户重填。request.getContextPath()是动态获取应用上下文路径如/bookstore硬编码/index.jsp在部署到非根路径时必挂。3.3 DAO 层JDBC 模板化封装SQL 与 Java 严格分离BookDaoImpl.java不能写public ListBook findAll() throws SQLException { Connection conn null; PreparedStatement ps null; ResultSet rs null; ListBook list new ArrayList(); try { conn DBUtil.getConnection(); ps conn.prepareStatement(SELECT * FROM books WHERE status1 ORDER BY create_time DESC); rs ps.executeQuery(); while(rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setTitle(rs.getString(title)); // ... 20 行 setXXX() list.add(b); } } finally { // 必须关闭资源顺序rs → ps → conn if(rs ! null) rs.close(); if(ps ! null) ps.close(); if(conn ! null) conn.close(); } return list; }正确做法是抽象模板方法// src/dao/BaseDao.java public abstract class BaseDaoT { public ListT query(String sql, RowMapperT rowMapper, Object... args) { Connection conn null; PreparedStatement ps null; ResultSet rs null; ListT list new ArrayList(); try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); // 参数绑定避免 SQL 注入 for(int i 0; i args.length; i) { ps.setObject(i 1, args[i]); } rs ps.executeQuery(); while(rs.next()) { list.add(rowMapper.mapRow(rs)); // 由子类实现映射逻辑 } } catch (SQLException e) { throw new RuntimeException(e); } finally { DBUtil.close(rs, ps, conn); // 封装统一关闭逻辑 } return list; } } // src/dao/impl/BookDaoImpl.java public class BookDaoImpl extends BaseDaoBook implements BookDao { Override public ListBook findAll() { String sql SELECT * FROM books WHERE status ? ORDER BY create_time DESC; return query(sql, new BookRowMapper(), 1); // status1 表示上架 } // 内部类实现映射彻底解耦 SQL 和 Java 对象 private static class BookRowMapper implements RowMapperBook { Override public Book mapRow(ResultSet rs) throws SQLException { Book b new Book(); b.setId(rs.getInt(id)); b.setTitle(rs.getString(title)); b.setAuthor(rs.getString(author)); b.setPrice(rs.getBigDecimal(price)); b.setStock(rs.getInt(stock)); return b; } } }参数说明RowMapperT是函数式接口让 DAO 只关注“怎么取数据”不关心“取什么数据”——BookRowMapper专属于 BookUserRowMapper专属于 User。query(sql, rowMapper, args...)中的args...是可变参数对应?占位符天然防 SQL 注入。DBUtil.close()必须按rs → ps → conn顺序关闭反序会导致SQLException: Operation not allowed after ResultSet closed。4. 避坑指南那些让“满分项目”变成“0 分答辩”的 5 个致命细节4.1 现象登录成功后点击“我的订单”报 404URL 显示http://localhost:8080/bookstore/order/list原因Servlet 映射路径写错或web.xml中servlet-mapping的url-pattern与前端 form action 不一致。常见错误是OrderListServlet映射为/order/list但 JSP 表单写actionorder/list缺前导/导致相对路径解析成http://localhost:8080/bookstore/order/order/list。解决统一用绝对路径。Servlet 映射保持/order/listJSP 中写action${pageContext.request.contextPath}/order/list。永远不要信浏览器地址栏的当前路径。4.2 现象添加购物车后数据库cart_items表里book_id是 0quantity是 1但商品实际存在原因前端传参时input typehidden namebookId value%book.getId()%中book.getId()返回 null因为book对象没正确 set 进 requestJSP 渲染成input namebookId valueServlet 用Integer.parseInt(request.getParameter(bookId))得到NumberFormatException捕获异常后默认赋值 0。解决在 Servlet 获取参数后立即校验String bookIdStr request.getParameter(bookId); if (bookIdStr null || bookIdStr.trim().isEmpty()) { request.setAttribute(errorMsg, 商品ID缺失); request.getRequestDispatcher(/book/detail.jsp?id id).forward(request, response); return; } int bookId Integer.parseInt(bookIdStr); // 此时才 parse4.3 现象MySQL 中users.password字段存的是明文123不是 BCrypt 加密字符串原因注册时调用了new BCryptPasswordEncoder().encode(123)但 DAO 层插入 SQL 写成了INSERT INTO users (username, password) VALUES (?, 123)——硬编码了123没用?占位符。解决所有 SQL 必须用PreparedStatement密码字段必须是ps.setString(2, encodedPassword)。检查UserDaoImpl.register()方法确认password参数是第几个?。4.4 现象Tomcat 控制台狂刷WARNING: An illegal reflective access operation has occurred但功能正常原因JDK 9 的模块化机制限制了反射访问而mysql-connector-java-5.1.47.jar内部用了sun.misc.Unsafe。这不是错误是警告不影响运行。解决升级驱动到mysql-connector-java-8.0.33.jar需同步升级 MySQL 到 8.0 并配置useSSLfalseserverTimezoneAsia/Shanghai或忽略警告JavaWeb 期末作业允许忽略此警告。4.5 现象在 IDEA 中 Debug断点打在LoginServlet.doPost()里但请求发过去却不命中原因IDEA 的 Tomcat 配置里“On ‘Update’ action” 和 “On frame deactivation” 选项选了 “Update classes and resources”但项目没勾选 “Build project before run”。解决File → Project Structure → Project确认 Project SDK 是 JDK 1.8Project language level 是 8。Run → Edit Configurations → Tomcat Server → Deployment确保bookstore:war exploded已添加Application context 是/bookstore。Run → Edit Configurations → Tomcat Server → Server勾选 “Build project before run”。右键项目 →Reload project再 Clean Tomcat work directory/tomcat/work/Catalina/localhost/bookstore。5. 进阶验证用 Postman 日志 数据库快照三步锁定真实瓶颈5.1 用 Postman 替代浏览器排除前端干扰浏览器会自动携带 Cookie、缓存 GET 请求、重定向跳转掩盖真实问题。必须用 Postman 发原始请求POST http://localhost:8080/bookstore/loginBody → x-www-form-urlencodedusernameadmin,password123查看 Response Headers确认Set-Cookie: JSESSIONIDxxx是否存在Location是否指向/index.jsp。查看 Response Body如果是 JSP 页面 HTML说明转发成功如果是空白或 500说明 Servlet 逻辑异常。技巧在 Postman 的 Tests 标签页写 JS 断言// 验证登录成功后返回 302 重定向 pm.test(Status code is 302, function () { pm.response.to.have.status(302); }); // 验证重定向 URL 包含 index.jsp pm.test(Redirect to index.jsp, function () { pm.expect(pm.response.headers.get(Location)).to.include(index.jsp); });5.2 在关键节点加日志比 Debug 更高效log4j.properties配置太重期末作业用最简System.out.println// 在 LoginServlet.doPost() 开头 System.out.println([LoginServlet] Start login process, username username); // 在 UserServiceImpl.login() 返回前 System.out.println([UserServiceImpl] login result for username : (user ! null)); // 在 UserDaoImpl.findByUsername() 执行后 System.out.println([UserDaoImpl] Found user: user);为什么比 Debug 快Debug 需要启停、断点、单步一次登录要 2 分钟日志一行打印1 秒定位。日志能暴露异步问题比如System.out.println(Before update)和System.out.println(After update)之间隔了 5 秒说明数据库 update 卡住了立刻去查 MySQLSHOW PROCESSLIST。5.3 数据库快照对比确认 DAO 层是否真的执行了 SQL登录成功后立刻在 MySQL 命令行执行-- 查看 users 表最后登录时间是否更新 SELECT username, last_login_time FROM users WHERE usernameadmin; -- 查看 cart_items 表是否有新记录测试加购 SELECT * FROM cart_items WHERE user_id (SELECT id FROM users WHERE usernameadmin) ORDER BY create_time DESC LIMIT 5; -- 查看订单表确认下单事务是否完整 SELECT o.id, o.total_amount, o.status, GROUP_CONCAT(oi.book_title) as books FROM orders o LEFT JOIN order_items oi ON o.id oi.order_id WHERE o.user_id (SELECT id FROM users WHERE usernameadmin) GROUP BY o.id;关键技巧把每次操作前后的 SQL 结果截图存档。比如“点击下单前”和“点击下单后”的orders表对比能一眼看出是INSERT INTO orders没执行还是INSERT INTO order_items失败导致事务回滚——这比看 Java 异常堆栈快 10 倍。5.4 一个真实教训我曾花 3 小时找 bug最后发现是web.xml里welcome-file-list写成了welcome-fileindex.html/welcome-file而项目只有index.jsp那天我反复检查LoginServlet的映射、web.xml的 servlet 配置、甚至重装了 Tomcat直到灵光一闪http://localhost:8080/bookstore/访问时Tomcat 默认去找index.html404 后才轮到index.jsp。我把welcome-file改成welcome-fileindex.jsp/welcome-file问题消失。所以现在我养成了习惯任何“页面打不开”的问题第一件事就是 curl-I看响应头curl -I http://localhost:8080/bookstore/ # 如果返回 HTTP/1.1 404 Not Found说明 welcome-file 配置错了 # 如果返回 HTTP/1.1 200 OK说明页面找到了问题在 JSP 渲染或 Servlet 逻辑希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑