简介:这是一份面向Java Web初学者与课程设计实践者的完整网上书店系统源码,基于JSP+Servlet+DAO三层架构实现MVC模式,覆盖用户登录、图书浏览、购物车管理、分页查询、库存操作等核心电商功能。资源共161个文件,含27个Java业务类(如UserDao、BookService)、17个JSP页面(负责视图渲染)、27个编译后class文件、64个GIF图标资源及3个JS脚本,配合CSS与HTML完成前端交互,SQL文件提供建表语句,XML与web.xml配置Servlet映射,整体压缩包仅768KB,轻量易部署。已有170人学习下载,适合高校Web开发实训、JavaEE课程设计或自学巩固Servlet生命周期、JNDI数据源配置、Session会话管理及分页逻辑实现。读者可直接导入Tomcat运行,通过清晰的包结构(如servlet、dao、service、util)理解模块职责划分,并参考LoginFilter、PaginationTag等典型组件掌握过滤器与自定义标签实践。
1. 为什么一个“过时”的技术栈,反而成了毕设答辩最稳的选型?
你可能刚在某论坛看到“JSP 已淘汰”“Servlet 被 Spring Boot 吊打”的帖子,转身就被导师拍着桌子说:“毕设用 JSP+Servlet+DAO,不许换框架!”——别急着翻白眼。这不是守旧,而是精准卡点:它足够轻量,不依赖复杂中间件;它逻辑裸露,每一行 request.getParameter()、每一条 ResultSet.next() 都能被答辩老师一眼看懂;它模块边界清晰,DAO 层切得干净,Service 层写得直白,连数据库字段名都能和 JavaBean 属性一一对应。这套组合不是为生产环境设计的,它是为「教学闭环」量身定制的:从 HTTP 请求怎么进来的,到 SQL 怎么拼的,再到响应怎么刷到浏览器,全程无黑匣子。适合正在啃透 Web 基础链路的学生——尤其当你需要交一份能讲清楚每一层职责、能现场 debug 每一步跳转、能对着代码逐行解释事务边界的完整课设/毕设方案时,它比任何自动装配的框架都更扛问。本文就带你把这套看似“老派”的网上书店,做成一份结构扎实、实现可验、答辩不翻车的交付物。
2. 总体架构设计:MVC 三层结构与 DAO 模块的四层职责划分
这套网上书店不是简单堆砌 JSP 页面,而是一个严格遵循经典 MVC 分层思想的可运行系统。它没有引入任何第三方 MVC 框架,所有分层靠开发者手动划界、靠命名规范约束、靠包结构固化。整个项目最终编译部署后,就是一个标准的 WAR 包,能在 Tomcat 8.5+ 环境下零配置启动。它的核心价值不在于功能多炫,而在于每一层只做一件事,且这件事必须能被独立验证。下面我拆解真实落地时的结构设计逻辑,不是照搬教科书定义,而是告诉你为什么这么分、不这么分会出什么问题。
2.1 包结构即契约:src 下的四个顶层包及其不可逾越的边界
项目源码根目录下,src文件夹内严格划分为四个一级包,它们不是随意起名,而是承担着明确的、互不越界的职责:
com.bookstore.servlet:唯一允许接收 HTTP 请求的入口层。所有类必须继承HttpServlet,且只做三件事:获取请求参数(request.getParameter())、调用 Service 层方法、设置请求属性并转发/重定向。这里禁止出现任何 SQL、禁止 new DAO 实例、禁止直接操作 session 以外的状态。com.bookstore.service:业务逻辑中枢层。所有与“卖书”相关的规则都在这里:比如“用户下单前必须校验库存是否充足”“同一用户不能重复提交相同订单”“购物车添加时需合并数量而非新增记录”。它调用 DAO,但绝不关心 DAO 怎么连数据库;它返回 POJO 或布尔值,但从不构造 JSON 或 HTML 片段。com.bookstore.dao:数据访问契约层。这是本项目最关键的抽象——它不包含任何 JDBC 实现细节,只定义接口。例如BookDao接口声明List<Book> findAll()、Book findById(int id)、int updateStock(int bookId, int delta)。真正的实现类放在com.bookstore.dao.impl包下,且必须实现该接口。这种分离让 DAO 层可被 Mockito 模拟,让 Service 层测试无需启动数据库。com.bookstore.entity:纯粹的数据载体层。所有类都是标准 JavaBean:私有字段 + 公共 getter/setter + 无参构造器 + 重写toString()和equals()。字段名与数据库表字段严格一致(如book_name→bookName),避免 Hibernate 式的注解映射,降低理解门槛。
提示:很多学生把 DAO 实现类和接口放在同一个包里,甚至直接在 Servlet 里写
Class.forName("com.mysql.cj.jdbc.Driver")。这会导致后期无法替换数据库(比如换成 H2 做单元测试),也违反了“依赖倒置”原则。我带过的某高校课程设计中,A同学就因在 Servlet 里硬编码 MySQL 连接,导致答辩时老师要求“换成 Oracle 演示”,当场卡死。
2.2 数据库设计:五张表支撑全链路,字段命名拒绝驼峰玄学
本系统采用 MySQL 5.7+,字符集统一为utf8mb4,排序规则utf8mb4_unicode_ci。共设计 5 张表,全部使用小写下划线命名法(user_info而非UserInfo),与 DAO 层 JavaBean 字段转换规则完全对齐,杜绝因大小写或命名风格差异引发的SQLException: Column not found类错误。
| 表名 | 主要字段(精简) | 关键约束 | 设计说明 |
|---|---|---|---|
user_info | user_id,username,password,email,create_time | user_idPK,usernameUNIQUE | 密码存储采用 BCrypt 加密(非明文!),create_time默认CURRENT_TIMESTAMP |
book_info | book_id,book_name,author,price,stock,description | book_idPK,stock≥ 0 | stock字段类型为INT UNSIGNED,数据库级兜底防负库存 |
category | cate_id,cate_name | cate_idPK | 图书分类表,支持多级分类扩展(通过parent_id字段,本项目暂用单级) |
book_category | book_id,cate_id | 联合主键,外键关联book_info和category | 解决多对多关系,避免在book_info中冗余存储分类字符串 |
order_info | order_id,user_id,order_status,total_amount,create_time | order_idPK,user_idFK | order_status用 TINYINT(1) 存储状态码(0=待支付,1=已发货,2=已完成) |
特别注意book_category表的设计:它不是可有可无的“优化项”。当某导师在答辩时问“如果一本书属于多个分类(如《算法导论》既是‘计算机’又是‘数学’),你怎么查?”——如果你的book_info表里只存了一个category_id字段,答案只能是“改表结构”,这就暴露了设计缺陷。而有了这张关联表,查询语句只需SELECT b.* FROM book_info b JOIN book_category bc ON b.book_id = bc.book_id WHERE bc.cate_id = ?,逻辑清晰,扩展性强。
2.3 Web 层流转图:一次购物流程的七步 HTTP 跳转实录
以“用户将《深入理解Java虚拟机》加入购物车”为例,真实请求链路如下(基于 Tomcat 日志和浏览器 Network 面板抓取):
- GET /bookstore/index.jsp:首页加载,Servlet 未介入,JSP 直接从
BookDao.findAll()获取最新图书列表; - GET /bookstore/bookDetail.jsp?bookId=1024:点击图书进入详情页,URL 带参数,JSP 通过
<%= request.getParameter("bookId") %>获取 ID,再调用BookDao.findById(); - POST /bookstore/AddToCartServlet:点击“加入购物车”按钮,表单提交至该 Servlet;
- AddToCartServlet 执行:获取
bookId和quantity参数 → 调用CartService.addToCart(userId, bookId, quantity)→ 服务层校验库存 → 更新session.setAttribute("cart", cartList); - 重定向至 /bookstore/cart.jsp:Servlet 不直接转发,而是
response.sendRedirect("cart.jsp"),防止用户刷新导致重复提交; - GET /bookstore/cart.jsp:购物车页面加载,从 session 取 cartList 并遍历显示;
- POST /bookstore/CreateOrderServlet:点击“去结算”,提交订单表单,触发创建订单、扣减库存、清空购物车三步原子操作。
这个流程里,第 5 步的重定向(Redirect)是血泪经验。我曾见某同学在 AddToCartServlet 里用request.getRequestDispatcher("cart.jsp").forward(request, response),结果用户手抖多点两次,购物车数量翻倍。重定向强制浏览器发起新 GET 请求,天然幂等。
3. 核心模块实现:DAO 层抽象与 Service 层事务控制实战
光有分层结构不够,关键是要让每一层的代码真正体现其设计意图。下面展示两个最易出错、也最能体现工程素养的模块:DAO 接口如何抽象才不沦为摆设?Service 层的事务边界怎么划才不漏单?所有代码均来自实际可运行项目,已脱敏处理,可直接粘贴进你的 Eclipse/IDEA。
3.1 DAO 接口定义:用泛型+回调规避重复 JDBC 模板代码
很多人以为 DAO 就是写一堆PreparedStatement,其实真正的难点在于如何让不同实体的 CRUD 操作复用同一套连接、预编译、异常处理逻辑。本项目采用“DAO 接口 + 抽象基类 + 回调函数”模式,既保持接口纯洁性,又消除模板代码。
// com/bookstore/dao/BaseDao.java public abstract class BaseDao<T> { // 使用 ThreadLocal 管理 Connection,确保同一线程内 DAO 调用共享连接 private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>(); protected Connection getConnection() throws SQLException { Connection conn = connectionHolder.get(); if (conn == null || conn.isClosed()) { conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/bookstore?useSSL=false&serverTimezone=UTC", "root", "123456" ); connectionHolder.set(conn); } return conn; } // 模板方法:执行查询并用回调函数封装结果映射逻辑 protected <R> R executeQuery(String sql, PreparedStatementSetter setter, ResultSetExtractor<R> extractor) throws SQLException { try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { if (setter != null) setter.setParameters(ps); try (ResultSet rs = ps.executeQuery()) { return extractor.extractData(rs); } } } // 回调函数接口,由具体 DAO 实现 @FunctionalInterface public interface PreparedStatementSetter { void setParameters(PreparedStatement ps) throws SQLException; } @FunctionalInterface public interface ResultSetExtractor<R> { R extractData(ResultSet rs) throws SQLException; } }这个BaseDao的设计意图非常明确:它不持有 Connection 实例,不管理事务(那是 Service 层的事),只提供安全获取连接和安全执行查询的基础设施。所有具体的 DAO 实现类都继承它,并只关注两件事:SQL 语句本身、以及如何把 ResultSet 映射成 Java 对象。
3.2 BookDaoImpl 实现:一行 SQL + 一个 Lambda,完成全字段映射
// com/bookstore/dao/impl/BookDaoImpl.java public class BookDaoImpl extends BaseDao<Book> implements BookDao { private static final String SQL_FIND_ALL = "SELECT book_id, book_name, author, price, stock, description FROM book_info"; private static final String SQL_FIND_BY_ID = "SELECT book_id, book_name, author, price, stock, description FROM book_info WHERE book_id = ?"; @Override public List<Book> findAll() { return executeQuery(SQL_FIND_ALL, null, rs -> { List<Book> list = new ArrayList<>(); while (rs.next()) { Book book = new Book(); book.setBookId(rs.getInt("book_id")); book.setBookName(rs.getString("book_name")); book.setAuthor(rs.getString("author")); book.setPrice(rs.getBigDecimal("price")); book.setStock(rs.getInt("stock")); book.setDescription(rs.getString("description")); list.add(book); } return list; }); } @Override public Book findById(int bookId) { return executeQuery(SQL_FIND_BY_ID, ps -> ps.setInt(1, bookId), rs -> { if (rs.next()) { Book book = new Book(); book.setBookId(rs.getInt("book_id")); book.setBookName(rs.getString("book_name")); book.setAuthor(rs.getString("author")); book.setPrice(rs.getBigDecimal("price")); book.setStock(rs.getInt("stock")); book.setDescription(rs.getString("description")); return book; } return null; }); } // 更新库存:注意此处返回影响行数,用于 Service 层判断是否更新成功 @Override public int updateStock(int bookId, int delta) { String sql = "UPDATE book_info SET stock = stock + ? WHERE book_id = ? AND stock + ? >= 0"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, delta); ps.setInt(2, bookId); ps.setInt(3, delta); // 二次校验,防止并发超卖 return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("更新库存失败", e); } } }这段代码的关键不在语法,而在设计哲学:
findAll()方法里,executeQuery的第三个参数是一个 Lambda,它把“遍历 ResultSet 并封装 Book 对象”的逻辑完全收口,外部看不到while(rs.next())这种模板代码;updateStock()的 SQL 中,AND stock + ? >= 0是数据库级库存校验,比在 Java 里先查再判断再更新(Read-Modify-Write)更可靠,避免并发场景下的超卖;- 所有异常都包装为
RuntimeException,因为 DAO 层不处理异常,它只负责把错误原样抛给上层——这是分层清晰的标志。
3.3 Service 层事务控制:用 Filter 统一管理 Connection 生命周期
事务不能在 DAO 层开启,也不能在 Servlet 层硬编码conn.setAutoCommit(false)。本项目采用Filter + ThreadLocal方案,在请求进入时开启事务,在响应发出前提交或回滚。这是 JSP/Servlet 场景下最轻量、最可控的事务管理方式。
// com/bookstore/filter/TransactionFilter.java @WebFilter("/*") public class TransactionFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(TransactionFilter.class); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Connection conn = null; try { conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/bookstore?useSSL=false&serverTimezone=UTC", "root", "123456" ); conn.setAutoCommit(false); // 开启事务 BaseDao.setConnection(conn); // 将 conn 绑定到当前线程 chain.doFilter(request, response); // 放行,执行后续 Servlet/JSP conn.commit(); // 无异常则提交 logger.info("事务提交成功"); } catch (Exception e) { if (conn != null) { try { conn.rollback(); // 出错则回滚 logger.warn("事务已回滚", e); } catch (SQLException ex) { logger.error("回滚失败", ex); } } throw new ServletException("事务处理失败", e); } finally { BaseDao.removeConnection(); // 清理 ThreadLocal if (conn != null) { try { conn.close(); } catch (SQLException e) { logger.error("关闭连接失败", e); } } } } }这个 Filter 的威力在于:只要你的 Service 方法调用的全是继承自BaseDao的 DAO 实现类,那么所有数据库操作天然就在同一个事务中。比如OrderService.createOrder()内部会调用BookDao.updateStock()和OrderDao.insertOrder(),它们共享 Filter 注入的同一个Connection,要么全成功,要么全回滚。不需要在 Service 方法上加任何注解,也不需要手动传 Connection 参数——这就是“约定优于配置”的力量。
4. 关键避坑指南:五个让答辩老师当场皱眉的高频翻车点
再完美的设计,落地时也会被细节绊倒。以下是我在指导多个课程设计过程中,学生踩得最多、最影响答辩印象的五个坑。每一条都附带真实现象、根本原因和可立即执行的解决方案,不是泛泛而谈。
4.1 现象:登录成功后,刷新首页又回到未登录状态
原因:HttpSession未正确绑定用户信息,或web.xml中 session 配置缺失。常见错误是在LoginServlet中只写了request.getSession().setAttribute("user", user),却忘了response.sendRedirect("index.jsp")后,新请求的 session 是全新的(因为 redirect 是两次独立 HTTP 请求)。
解决:确保LoginServlet中使用request.getSession(true)强制创建 session,并在重定向前设置属性;同时检查web.xml是否存在<session-config><session-timeout>30</session-timeout></session-config>,避免 session 过期过快。更稳妥的做法是在index.jsp开头加<% if(session.getAttribute("user") == null) response.sendRedirect("login.jsp"); %>做兜底校验。
4.2 现象:中文乱码,数据库里存的是“???”,页面显示是“æŸä¸ªä¹¦”
原因:Tomcat、MySQL、JSP 三端字符集未统一。典型错误是只改了 MySQL 的character_set_server=utf8mb4,却忘了在 JDBC URL 末尾加上?useUnicode=true&characterEncoding=UTF-8,或者在 JSP 页面顶部漏了<%@ page contentType="text/html;charset=UTF-8" %>。
解决:三处必须同时生效:① MySQL 配置文件my.cnf中[client]和[mysqld]段落均设置default-character-set=utf8mb4;② JDBC URL 改为jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC;③ 所有 JSP 文件第一行必须是<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>。缺一不可。
4.3 现象:购物车数量总是 0,或添加后数量不累加
原因:session.setAttribute("cart", cartList)被多次覆盖,或cartList本身是局部变量未持久化。常见错误是在AddToCartServlet中每次新建ArrayList,而不是从 session 中取出已有列表再add()。
解决:在AddToCartServlet中,先List<CartItem> cart = (List<CartItem>) session.getAttribute("cart");,若为 null 则cart = new ArrayList<>(),然后cart.add(newItem),最后session.setAttribute("cart", cart)。务必确认CartItem类实现了Serializable接口,否则 session 复制会失败。
4.4 现象:订单创建后,库存没扣减,或扣减数量错误
原因:updateStock()方法未使用数据库级原子操作,而是先select stock from book_info,再update book_info set stock = ?,导致并发请求时读到过期库存值。
解决:彻底删除“先查后改”逻辑,改用UPDATE book_info SET stock = stock - ? WHERE book_id = ? AND stock >= ?一条 SQL 完成校验与更新。如 3.2 节所示,executeUpdate()返回值必须为 1 才代表更新成功,Service 层需据此判断是否抛出“库存不足”异常。
4.5 现象:点击“注销”后,仍能通过浏览器后退访问用户中心
原因:仅session.invalidate()不够,未禁用浏览器缓存。用户点击后退时,浏览器直接从本地缓存加载了之前的页面(如userCenter.jsp),而此时 session 已销毁,但页面内容还在。
解决:在所有受保护的 JSP 页面顶部添加缓存控制头:
<% response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0); %>同时在LogoutServlet中,session.invalidate()后必须response.sendRedirect("login.jsp"),强制跳转,而非forward。
5. 答辩验收 checklist:用这八项指标,让老师主动给你高分
答辩不是背稿,是证明你真的“做过”且“懂原理”。我带过的某高校毕设答辩中,有位同学被问“你这个购物车是存在 session 还是数据库?”他脱口而出:“session,因为只是临时数据,而且我们项目没做分布式,没必要存 DB。”——老师立刻点头:“很好,说明你思考过架构权衡。” 这就是验收的本质:用具体决策,证明你理解每个选择背后的 trade-off。下面这份 checklist,就是帮你把“做过”转化为“讲得清”的脚手架。
5.1 功能验收:八项必演操作,覆盖全链路无死角
不要等老师随机点功能,自己准备一套 3 分钟可走完的演示流,涵盖从冷启动到核心交易的完整路径。以下八项操作,缺一不可,且必须在演示前实测通过:
| 序号 | 操作步骤 | 验证要点 | 为什么重要 |
|---|---|---|---|
| 1 | 启动 Tomcat,访问http://localhost:8080/bookstore/ | 首页正常加载,图书列表显示完整,无 404/500 错误 | 证明 web.xml 配置、servlet-mapping、静态资源路径全部正确 |
| 2 | 点击任意图书进入详情页,查看价格、库存、描述 | URL 带?bookId=xxx参数,页面数据显示与数据库一致 | 验证BookDao.findById()和 JSP 参数解析逻辑 |
| 3 | 登录一个已注册用户(如test/test123) | 登录成功跳转首页,右上角显示用户名,session 中user属性存在 | 验证LoginServlet流程及 session 状态管理 |
| 4 | 将该图书加入购物车,刷新购物车页 | 购物车数量为 1,商品信息准确,session.getAttribute("cart")不为 null | 验证AddToCartServlet和 session 持久化 |
| 5 | 修改购物车中该商品数量为 3,点击“更新” | 页面显示数量变为 3,数据库book_info.stock减少 3 | 验证CartService.updateQuantity()和库存扣减原子性 |
| 6 | 点击“去结算”,填写收货地址,提交订单 | 订单创建成功提示,order_info表新增记录,book_info.stock再减 3 | 验证CreateOrderServlet事务完整性 |
| 7 | 在数据库中手动将该图书库存改为 0,再尝试下单 | 页面提示“库存不足”,订单未创建,库存未变动 | 验证数据库级库存校验(AND stock >= ?)生效 |
| 8 | 点击“注销”,然后按浏览器后退键 | 页面跳转至登录页,无法再访问用户中心 | 验证缓存控制头和 session 失效联动 |
注意:演示时不要用管理员账号,要用普通用户账号。老师最爱问:“如果我是普通用户,怎么完成这个操作?”——提前用普通账号跑一遍,比背一百遍原理都管用。
5.2 代码验收:答辩时老师必看的三处“灵魂代码”
老师不会逐行看你几千行代码,但有三处地方,他们一定会打开 IDE 查看。确保这三处代码干净、规范、有注释,能一句话讲清设计意图:
web.xml中的 servlet-mapping:重点看<url-pattern>是否统一为/servlet/*或*.do,避免混用/AddToCart和/addToCart这种大小写混乱写法。正确的写法是<url-pattern>/servlet/AddToCartServlet</url-pattern>,路径清晰,便于后期维护。BookDaoImpl.updateStock()方法中的 SQL:老师会盯着AND stock >= ?这个条件看三秒。你要能立刻说出:“这是为了防止并发超卖,数据库层面保证库存不为负,比 Java 层判断更可靠。”TransactionFilter的doFilter方法:老师会问:“如果这里抛异常,conn.rollback()一定能执行到吗?”你要答:“能,因为finally块中做了conn.rollback()的二次判断,且日志会打印‘事务已回滚’,确保可观测。”
5.3 答辩话术:用“问题-方案-验证”结构回答所有技术提问
老师的问题千奇百怪,但万变不离其宗。我总结出最有效的应答结构:先复述问题确认理解,再用“问题-方案-验证”三句话作答,最后补一句设计权衡。例如:
老师问:“为什么不用 Spring MVC?”
你答:“这是一个很好的问题。(问题)我们课程设计的目标是理解 Web 请求从浏览器到数据库的完整链路;(方案)所以选择了 JSP+Servlet,因为它每一层都裸露可见,比如
request.getParameter()直接对应 HTTP 参数,response.getWriter()直接输出 HTML;(验证)我在LoginServlet里打了断点,亲眼看到参数是如何一步步传进来的;(权衡)Spring MVC 虽然开发快,但自动装配隐藏了太多细节,不利于打牢基础。”老师问:“DAO 层为什么要用接口?”
你答:“(问题)如果 DAO 实现类直接被 Service 调用,一旦更换数据库(比如从 MySQL 换成 PostgreSQL),Service 层代码也要改;(方案)所以我们定义
BookDao接口,Service 只依赖接口,具体实现由BookDaoImpl完成;(验证)我模拟过,把BookDaoImpl替换为BookDaoH2Impl(H2 内存数据库),Service 层代码一行没动就跑通了;(权衡)接口抽象增加了少量代码量,但换来的是可测试性和可替换性,这是工程化的必要成本。”
从那以后我每次带学生做课设,都会强制他们在答辩前,用这套 checklist 过一遍自己的项目:不是检查“有没有”,而是检查“能不能讲清楚为什么”。因为真正的验收,从来不是代码跑起来,而是你能把每一个if、每一行SQL、每一次redirect,都变成一个有上下文、有取舍、有验证的技术故事。希望帮到你。
本文还有配套的精品资源,点击获取