尧图精选

JSP+MVC+MySQL图书购物系统:JavaWeb毕设实战与核心链路解析

🕒 发布时间:2026/10/1 22:22:54 📁 来源:尧图网络
简介这是一套基于JSP与MVC设计模式、以MySQL为数据库的网上图书购物系统源码面向Java Web初学者与进阶学习者可作为毕业设计、课程设计、大作业或工程实训的参考项目。压缩包共76个文件约47.8MB包含14个jsp页面文件、12个java源文件与12个class编译文件另有css样式、xml配置、sql数据库脚本及项目展示视频等覆盖前端页面、控制层与数据层的主要结构。资源中附有数据库文件与README说明便于读者理解MVC分层思路、页面跳转逻辑与数据表设计并在此基础上自行调试、修改与扩展功能。目前已有136人学习下载。需要说明的是代码仅作参考资料需具备一定基础方能看懂并自行排错不能直接照搬复制。1. 从零搭一套 JSPMVCMySQL 图书购物系统为什么它仍是 JavaWeb 毕设的硬通货打开招聘网站搜 Java 后端十有八九写着「熟悉 Spring 全家桶」可回到课程设计和毕业设计JSP MVC MySQL 这套组合依然是绕不开的硬通货。原因很实在它把 Servlet、JSP、JavaBean、JDBC、会话管理、过滤器这些 JavaWeb 的底层零件全暴露在你面前没有框架帮你兜底写一遍就等于把整个请求响应链路走通了。网上图书购物系统这个题目恰好把「用户登录注册、图书列表分页、购物车增删改、订单生成、后台管理」这些典型场景全塞进一个项目里业务闭环完整数据表关系清晰特别适合拿来练手。这篇笔记不讲空话我会按「环境怎么搭、MVC 怎么分层、购物车和订单怎么落地、部署和排错怎么做」的顺序把一套能跑起来的方案拆开讲清楚。适合正在找基于 JSP 的毕设选题、或者想补 JavaWeb 基础的同学也适合工作后回头补底层原理的开发者。2. 环境搭建与 MVC 三层架构的落地方式2.1 JDK、Tomcat、MySQL 的版本搭配与安装要点先把地基打牢。这套技术栈对版本不算挑剔但搭配错了会出各种玄学问题。我一般用 JDK 8 或 JDK 11Tomcat 8.5 或 9.0MySQL 5.7 或 8.0IDE 用 IntelliJ IDEA。JDK 版本别超过 Tomcat 支持的编译级别否则启动时直接报 UnsupportedClassVersionError。MySQL 安装是新手翻车最多的地方。Windows 下推荐用官方 installer安装时勾选 MySQL Server 和 MySQL Workbench服务名默认 MySQL80。安装过程中会让你设 root 密码记牢后面 JDBC 连接要用。Linux 下如果用 rpm 安装注意先装 mysql-community-common 再装 server顺序错了依赖会报错。装完执行mysql -u root -p能进命令行就说明服务起来了。# 检查 MySQL 服务状态Windows net start | findstr MySQL # Linux 下启动并设置开机自启 systemctl start mysqld systemctl enable mysqld # 登录后创建项目专用数据库字符集用 utf8mb4 mysql -u root -p CREATE DATABASE bookshop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;字符集这一步别省。用默认的 latin1 建库中文图书名存进去就是乱码后面改起来要重建表血泪经验。utf8mb4 比 utf8 多支持 emoji 和部分生僻字图书简介里出现特殊符号也不会炸。Tomcat 的配置重点在端口和编码。默认 8080 端口如果被占用改 conf/server.xml 里的 Connector port。URIEncoding 建议显式设成 UTF-8避免 GET 请求中文参数乱码。!-- conf/server.xml 中 Connector 节点 -- Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /2.2 MVC 三层架构在图书系统里的职责划分MVC 不是三个文件夹那么简单核心是职责边界。Model 层管数据和业务View 层只管展示Controller 层负责接收请求、调业务、选视图。落到代码结构上我一般这样分层次包名职责典型类视图/webapp/jsp页面展示尽量少写 Java 代码book_list.jsp、cart.jsp控制com.bookshop.controller接收请求、参数校验、跳转BookServlet、CartServlet业务com.bookshop.service业务逻辑、事务控制BookService、OrderService数据com.bookshop.dao数据库增删改查BookDao、UserDao实体com.bookshop.entity与表对应的 JavaBeanBook、User、OrderController 里不要直接写 SQLService 里不要碰 HttpServletRequest这是两条铁律。见过太多项目把 JDBC 代码塞进 Servlet 的 doGet 里后期加个功能要改五六个文件维护成本直接爆炸。一个典型的 Servlet 请求处理长这样// BookServlet.java 处理图书列表请求 WebServlet(/book/list) public class BookServlet extends HttpServlet { private BookService bookService new BookService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 取分页参数给默认值防止空指针 int pageNum 1; int pageSize 8; String pageStr req.getParameter(pageNum); if (pageStr ! null !pageStr.isEmpty()) { pageNum Integer.parseInt(pageStr); } // 2. 调业务层拿分页数据 PageBeanBook page bookService.findByPage(pageNum, pageSize); // 3. 数据放请求域转发到 JSP req.setAttribute(page, page); req.getRequestDispatcher(/jsp/book_list.jsp).forward(req, resp); } }参数说明pageNum 是当前页码pageSize 是每页条数PageBean 是自定义分页封装类包含 list、totalCount、totalPage、currentPage 四个字段。转发用 forward 不用 redirect因为要把 request 域里的数据带到页面。如果这里用重定向页面拿不到 page 对象列表就是空的。2.3 数据库连接池的引入与 JDBC 工具类封装每次请求都 DriverManager.getConnection 是性能杀手连接建立开销大并发一上来数据库就顶不住。常见做法是引入 Druid 或 C3P0 连接池。Druid 配置简单监控功能也够用。# druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/bookshop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot password你的密码 initialSize5 maxActive20 maxWait3000注意 url 里的几个参数useSSLfalse 关掉 SSL 警告serverTimezone 必须设否则 MySQL 8 会报时区错误。maxActive 设 20 对课程设计够用生产环境要按 QPS 调。封装一个 JDBCUtils 工具类统一获取连接和释放资源public class JDBCUtils { private static DataSource dataSource; static { try { Properties props new Properties(); props.load(JDBCUtils.class.getClassLoader() .getResourceAsStream(druid.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(连接池初始化失败); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 按 rs - stmt - conn 顺序关闭每个都判空 try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }关闭顺序不能反先关结果集再关语句再关连接反了会报「Operation not allowed after ResultSet closed」。连接池里的 close 不是真关闭是归还连接所以放心调。3. 用户、图书、购物车三大核心模块的实现3.1 用户注册登录与 Session 会话管理用户模块是入口涉及密码存储和会话保持两个安全点。密码绝对不能明文存用 MD5 加盐或者 BCrypt。课程设计里 MD5 够用但要知道它不安全生产环境必须换。// 注册时对密码做 MD5 摘要 public static String md5(String password) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(password.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(MD5 计算失败, e); } }登录成功后把 user 对象放进 session后续判断登录状态就靠它// LoginServlet 登录校验通过后 HttpSession session req.getSession(); session.setAttribute(user, user); session.setMaxInactiveInterval(30 * 60); // 30 分钟超时 resp.sendRedirect(req.getContextPath() /book/list);session 超时时间默认 30 分钟可以在 web.xml 里统一配。购物车数据如果放 session用户关浏览器就丢了所以购物车建议落库登录用户存 user_id 关联未登录用户可以用 Cookie 临时存。3.2 图书列表分页查询的 SQL 与 PageBean 封装分页是列表页的核心。MySQL 用 LIMIT offset, size 实现offset (pageNum - 1) * pageSize。但要注意先查总数再查当前页两次查询。-- 查总记录数 SELECT COUNT(*) FROM book WHERE status 1; -- 查当前页数据按上架时间倒序 SELECT id, name, author, price, stock, cover, category_id FROM book WHERE status 1 ORDER BY create_time DESC LIMIT ?, ?;PageBean 封装类public class PageBeanT { private int currentPage; // 当前页 private int pageSize; // 每页条数 private int totalCount; // 总记录数 private int totalPage; // 总页数 private ListT list; // 当前页数据 // totalPage 由 totalCount 和 pageSize 计算得出 public int getTotalPage() { return (totalCount % pageSize 0) ? totalCount / pageSize : totalCount / pageSize 1; } // getter/setter 省略 }参数说明currentPage 从 1 开始pageSize 建议 8 到 12太多页面加载慢太少翻页频繁。totalPage 用向上取整别用整除否则最后一页数据会丢。JSP 页面用 JSTL 的 c:forEach 遍历 list分页条根据 currentPage 和 totalPage 动态渲染上一页下一页。3.3 购物车的两种存储方案与增删改逻辑购物车有 session 存和数据库存两种。session 方案简单适合未登录场景但换设备就没了。数据库方案用 cart 表字段 user_id、book_id、quantity登录用户持久化体验更好。我一般两者结合未登录存 session登录后合并到数据库。// CartService 添加购物车已存在则数量累加 public void addToCart(int userId, int bookId, int quantity) { Cart cart cartDao.findByUserAndBook(userId, bookId); if (cart null) { cartDao.insert(new Cart(userId, bookId, quantity)); } else { cartDao.updateQuantity(cart.getId(), cart.getQuantity() quantity); } }库存校验不能漏。加购时查 book 表的 stock数量超过库存要拦下来否则下单时才发现库存不足用户体验很差。// 加购前校验库存 Book book bookDao.findById(bookId); if (book.getStock() quantity) { throw new BusinessException(库存不足当前仅剩 book.getStock() 本); }购物车页面用表格展示每行有数量输入框和删除按钮。修改数量用 AJAX 提交避免整页刷新。删除就是按 cart_id 删记录注意校验这个 cart 是不是当前用户的防止越权删别人的购物车。4. 订单生成、后台管理与项目部署4.1 下单流程的事务控制与订单号生成下单是整个系统最需要事务的地方。要同时做三件事写 order 主表、写 order_item 明细表、扣减 book 库存。任何一步失败都要回滚否则会出现「订单生成了但库存没扣」或者「库存扣了订单没生成」的脏数据。// OrderService 下单用事务保证一致性 public String createOrder(int userId, ListCart cartList) { Connection conn null; try { conn JDBCUtils.getConnection(); conn.setAutoCommit(false); // 开启事务 String orderNo generateOrderNo(); BigDecimal total BigDecimal.ZERO; for (Cart cart : cartList) { Book book bookDao.findById(conn, cart.getBookId()); if (book.getStock() cart.getQuantity()) { throw new BusinessException(《 book.getName() 》库存不足); } // 扣库存用乐观锁防止超卖 int rows bookDao.reduceStock(conn, book.getId(), cart.getQuantity()); if (rows 0) { throw new BusinessException(库存扣减失败请重试); } total total.add(book.getPrice() .multiply(new BigDecimal(cart.getQuantity()))); } orderDao.insert(conn, orderNo, userId, total); orderItemDao.batchInsert(conn, orderNo, cartList); cartDao.clearByUser(conn, userId); conn.commit(); return orderNo; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException(下单失败 e.getMessage(), e); } finally { try { if (conn ! null) conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }扣库存的 SQL 用UPDATE book SET stock stock - ? WHERE id ? AND stock ?把库存判断放进 WHERE 条件靠数据库行锁保证并发安全这就是乐观锁的思路。返回影响行数为 0 说明库存不够抛异常回滚。订单号生成别用时间戳高并发会重复。常见做法是「时间戳 用户 ID 后四位 随机数」或者用数据库自增 ID 拼前缀。private String generateOrderNo() { String time new SimpleDateFormat(yyyyMMddHHmmss).format(new Date()); int random new Random().nextInt(9000) 1000; return time random; }4.2 后台图书管理的 CRUD 与文件上传后台管理是管理员用的核心是图书的增删改查和封面图上传。上传用 Servlet 3.0 的 Part 接口或者 Commons FileUpload。Part 更简单不用引额外依赖。// 图书新增处理封面文件上传 WebServlet(/admin/book/add) MultipartConfig(maxFileSize 2 * 1024 * 1024) public class BookAddServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String name req.getParameter(name); String author req.getParameter(author); BigDecimal price new BigDecimal(req.getParameter(price)); int stock Integer.parseInt(req.getParameter(stock)); Part part req.getPart(cover); String fileName System.currentTimeMillis() _ part.getSubmittedFileName(); String savePath getServletContext().getRealPath(/upload); File dir new File(savePath); if (!dir.exists()) dir.mkdirs(); part.write(savePath File.separator fileName); Book book new Book(name, author, price, stock, upload/ fileName); bookService.add(book); resp.sendRedirect(req.getContextPath() /admin/book/list); } }参数说明maxFileSize 限制单文件 2MB防止传大文件拖垮服务。文件名加时间戳前缀避免重名覆盖。getRealPath 拿到的是部署目录重启 Tomcat 后上传的文件可能丢生产环境要存到项目外的固定目录数据库只存相对路径。4.3 打包 WAR 部署到 Tomcat 的完整步骤IDEA 里配好 Artifact选 Web Application: Archive输出 war 包。或者用 Maven 的 package 命令pom.xml 里 packaging 设成 war。# Maven 打包 mvn clean package -DskipTests # 生成的 war 在 target 目录下复制到 Tomcat cp target/bookshop.war $CATALINA_HOME/webapps/ # 启动 Tomcat $CATALINA_HOME/bin/startup.sh # Linux # 或 Windows 下双击 startup.batTomcat 会自动解压 war 包访问http://localhost:8080/bookshop/就能看到首页。如果 404先看 webapps 下有没有解压出目录再看 logs/catalina.out 有没有报错。常见问题是 web.xml 版本和 Tomcat 不匹配或者 Servlet 注解没被扫描到。5. 这套系统最容易翻车的五个地方5.1 中文乱码POST 和 GET 要分开治现象表单提交的中文图书名存进数据库变成问号或者页面显示乱码。原因POST 请求默认用 ISO-8859-1 解码GET 请求在 Tomcat 8 之前也用 ISO-8859-1。数据库连接和表的字符集不统一也会乱。解决POST 在 Servlet 第一行加req.setCharacterEncoding(UTF-8)或者写个 CharacterEncodingFilter 统一处理。GET 在 server.xml 的 Connector 加 URIEncodingUTF-8。数据库连接 url 加 characterEncodingutf8建库建表都用 utf8mb4。三处都对齐才彻底。5.2 连接池耗尽连接没归还现象系统跑一会儿就卡死报「wait millis 3000, active 20」之类的连接池等待超时。原因DAO 里拿了 Connection 没在 finally 里 close连接被占满。或者事务里抛异常没回滚连接一直挂着。解决所有 JDBC 操作都用 try-finally 保证 close或者用 try-with-resources。事务代码的 catch 块里必须 rollbackfinally 里恢复 autoCommit 并 close。用 Druid 的话开监控页面看 activeCount 是不是一直不降。5.3 购物车数量为负并发和校验缺失现象用户快速点加购或者改数量购物车里出现负数或者超大数字。原因前端没做数量下限校验后端 update 直接覆盖没判断 quantity 是否合法。解决前端 input 加 min1后端 update 前判断 quantity 0 且不超过库存。改数量用UPDATE cart SET quantity ? WHERE id ? AND user_id ?把 user_id 带上防止越权。5.4 订单重复提交刷新页面就多一单现象用户下单后按 F5 刷新又生成一笔订单。原因下单是 POST 请求刷新会重发。或者用户网络慢连点两次提交按钮。解决下单成功后用 redirect 跳转到订单详情页别用 forward。前端提交按钮点击后 disable。更稳的做法是生成一个 token 存 session下单时校验并删除同一 token 只能用一次。5.5 部署后图片 404路径写成了绝对路径现象本地跑得好好的部署到服务器后图书封面全裂了。原因JSP 里图片 src 写成了C:/upload/xxx.jpg这种绝对路径或者用了getRealPath但重启后文件丢了。解决图片路径存相对路径JSP 里用${pageContext.request.contextPath}/upload/xxx.jpg拼。上传目录配到项目外Tomcat 的 server.xml 里加 Context 的 docBase 指向外部目录这样重新部署 war 包不会覆盖上传的文件。6. 用过滤器统一编码和登录校验的进阶技巧前面每个 Servlet 都写一遍setCharacterEncoding太啰嗦登录校验也是每个后台页面都要判断 session重复代码多。用 Filter 一次性解决这是这套系统从「能跑」到「像样」的关键一步。编码过滤器WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // POST 编码 request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }登录校验过滤器只拦后台路径WebFilter(/admin/*) public class AdminAuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(); Object admin session.getAttribute(admin); if (admin null) { response.sendRedirect(request.getContextPath() /jsp/admin_login.jsp); return; } chain.doFilter(request, response); } }参数说明WebFilter(/*)拦截所有请求/admin/*只拦后台。过滤器顺序按类名字母序或者 web.xml 里的配置顺序编码过滤器要排在前面否则后面过滤器拿到的参数还是乱码。验证过滤器有没有生效可以在 doFilter 里打日志看请求进来时编码是不是已经设好。如果登录校验没拦住检查 session 里存的 key 和过滤器里取的 key 是不是一致这种低级错误我踩过不止一次。最后说个习惯每加一个功能先在脑子里过一遍「请求从浏览器到 Servlet 到 Service 到 DAO 到数据库再回来」这条链路哪一环可能出问题心里就有数了。这套 JSP MVC MySQL 的图书购物系统代码量不大但把 JavaWeb 的核心链路全串了一遍认真写完一遍后面学 Spring 会轻松很多。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →