ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaWeb超市管理系统:Servlet/JSP三层架构源码拆解与部署避坑指南

JavaWeb超市管理系统:Servlet/JSP三层架构源码拆解与部署避坑指南 简介JavaWeb超市管理系统是一套基于MVC架构的JavaWeb实战项目面向正在学习Servlet、JSP、JDBC的开发者也适合用作课程设计或毕业设计参考。整合包共150个文件大小5.08MB含33个Java源码、18个JSP页面、19个XML配置、1个SQL脚本以及class字节码、jar依赖库和少量前端样式图片源码负责业务逻辑JSP完成页面渲染XML配置Servlet映射SQL脚本可直接初始化超市管理的商品、用户、订单等数据表。压缩包以rar格式打包目录结构清晰便于按模块对比学习。已有424人浏览学习。项目覆盖登录认证、权限控制、购物车结算、支付接口集成等完整交易链路并包含错误处理与日志记录设计通过对照源码和数据库脚本可以系统理解MVC分层、JDBC事务一致性处理及Web前后端交互范式同时借助项目中的经典模块快速提升实际开发与排错能力。1. JavaWeb 超市管理系统不只是一个课设更是一份能跑通全流程的源码包做 JavaWeb 开发的人手里多少都存过几个课设项目但大多数是「能启动、能点两下、答辩完就吃灰」的壳子。这份超市管理系统源码包不一样的地方在于它把 JavaWeb 面试和工作中最常碰到的几条主线——MVC 分层、Servlet 请求处理、JSP 页面渲染、JDBC 数据库访问、购物车事务、用户登录会话——全串在了一个完整业务里。如果你正在学 JavaWeb或者想找一个能反复改着练手的真实项目这份资源比那些只有 CRUD 的 demo 值钱得多。它适合两类人一是刚学完 Servlet 和 JSP想看看一个完整项目怎么组织类、怎么分包、怎么写 Dao 和 Servlet 的新手二是准备面试或做课程设计需要一个能讲清楚业务逻辑、能上手扩展的源码包的在校生。我拆过不少类似资源这个项目的类目很典型——ShoppingServiceImpl、UserDaoImpl、CarAddServlet、FavoriteDaoImpl、EvaluateDaoImpl看类名就知道覆盖了用户、购物车、收藏、评价四块核心业务。下面我按自己实际复现的路径把它的架构逻辑、部署步骤、踩过的坑一次说清楚。2. MVC 与三层架构从类名看懂这个项目的分层设计很多新手拿到源码第一件事就是找入口类结果在 src 目录里翻半天越看越乱。其实先读类名就能把架构猜个七八成。这个项目里既有 Service 层ShoppingServiceImpl也有 Dao 层UserDaoImpl、FavoriteDaoImpl、EvaluateDaoImpl还有 Servlet 控制器CarAddServlet典型的三层结构——Controller 接收请求、调 Service 处理业务、Service 再调 Dao 访问数据库JSP 只负责展示。2.1 Servlet 控制器层以 CarAddServlet 为例看请求如何流转Servlet 在这个项目里的角色就是「前台接待员」。以 CarAddServlet 为例它处理的是一次添加购物车的请求。前端页面上用户点了「加入购物车」表单或 AJAX 把商品 ID、数量、用户 ID 发到这个 ServletServlet 里先做参数解析再调用业务层方法最后根据结果跳转或返回 JSON。WebServlet(/carAdd) public class CarAddServlet extends HttpServlet { private ShoppingService shoppingService new ShoppingServiceImpl(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String userId req.getParameter(userId); String productId req.getParameter(productId); String quantity req.getParameter(quantity); if (userId null || productId null || quantity null) { resp.sendRedirect(login.jsp?errormissingParam); return; } boolean ok shoppingService.addToCart( Integer.parseInt(userId), Integer.parseInt(productId), Integer.parseInt(quantity)); if (ok) { resp.sendRedirect(cart.jsp?msgadded); } else { resp.sendRedirect(cart.jsp?msgfailed); } } }这段代码展示了 Servlet 最核心的职责取参数、转类型、调 Service、控制页面跳转。注意Integer.parseInt之前没有做异常捕获真实项目里一般会加 try-catch 或统一异常处理器但在教学项目里这样写反而方便初学者看清楚流程。req.setCharacterEncoding(UTF-8)必须放在读取参数之前否则中文参数会乱码。2.2 Service 业务层ShoppingServiceImpl 里的事务边界Service 层是很多自学者容易忽略的一层。有人觉得 Servlet 里直接写 JDBC 不就行了何必多套一层。但一旦涉及到购物车结算、下单扣库存这类跨多张表的操作没有 Service 层做事务控制数据就会出大问题。public class ShoppingServiceImpl implements ShoppingService { private CarDao carDao new CarDaoImpl(); Override public boolean addToCart(int userId, int productId, int quantity) { Connection conn null; try { conn JdbcUtils.getConnection(); conn.setAutoCommit(false); // 1. 查询商品库存 int stock carDao.getStock(conn, productId); if (stock quantity) { conn.rollback(); return false; } // 2. 检查购物车是否已有该商品 boolean exists carDao.isProductInCart(conn, userId, productId); if (exists) { carDao.updateQuantity(conn, userId, productId, quantity); } else { carDao.insertCartItem(conn, userId, productId, quantity); } // 3. 扣减库存 carDao.reduceStock(conn, productId, quantity); conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { JdbcUtils.close(conn); } } }这里的关键是setAutoCommit(false)和rollback()。一个 addToCart 操作要同时影响购物车表、商品表改库存如果第二步成功而第三步失败没有事务就会出现「已加购但库存没减」的脏数据。看到 ShoppingServiceImpl 的类名就要反应过来它一定封装了这类跨表操作。教学项目里事务写在一个方法里没问题但注意 Connection 对象不能跨 Service 方法传递否则事务边界就乱了。2.3 JDBC 数据访问层UserDaoImpl 与连接池的选择Dao 层的核心就是 JDBC 的增删改查。UserDaoImpl 这个类名暗示了它是针对用户表的操作集合——按用户名查用户、插入新用户、更新用户信息等。典型的 getUserByUsername 写法如下public class UserDaoImpl implements UserDao { Override public User getUserByUsername(String username) { String sql SELECT id, username, password, nickname, role FROM user WHERE username ?; try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); user.setNickname(rs.getString(nickname)); user.setRole(rs.getString(role)); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }这个项目的数据库连接如果是用DriverManager.getConnection()每次新建并发上来会很吃力。常见的改进方案是换成 Druid 或 C3P0 连接池。用连接池时Dao 层代码几乎不用改只要把JdbcUtils里的getConnection()换成从连接池拿即可。另外注意虽然这里用了 try-with-resources但如果是手动关闭必须先关 ResultSet、再关 Statement、最后关 Connection顺序反了可能会造成资源泄漏。JSP 视图层的逻辑其实比较简单用 EL 表达式$(cartItems)直接输出集合用 JSTL 的c:forEach遍历商品列表。新手常犯的错误是在 JSP 里写大量% %Java 脚本片段这个项目如果用了 EL 和 JSTL说明作者有意识地控制了页面逻辑复杂度。拿到源码后先看 page 指令里的isELIgnored是否为 false再确认 JSP 顶部有没有% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %这两个点决定了 EL 能不能正常运行。3. 把源码跑起来JDK、Tomcat、MySQL 的配置与部署步骤源码拿到手第一步不是急着看代码而是先把环境对齐。我拆过的 JavaWeb 项目十有八九运行失败是因为环境版本不匹配——代码是 JDK 8 写的你拿 JDK 17 跑 Tomcat 9就可能遇到javax.servlet包不存在的问题。3.1 环境选型JDK 8 Tomcat 8.5/9 MySQL 5.7 最稳这个项目的类名和代码风格是典型的传统 JavaWeb 写法用的还是javax.servlet而不是jakarta.servlet。javax.servlet是 JDK 8 时代的包名从 Tomcat 10 开始换成了jakarta.servlet所以如果你用 Tomcat 10 跑旧项目会直接报 ClassNotFoundException。我一般推荐的组合是组件推荐版本理由JDK1.88u202 或更高兼容性最好所有老项目都能编译Tomcat8.5.x 或 9.0.x仍在javax.*命名空间避免包名迁移问题MySQL5.7 或 8.05.7 最稳妥8.0 注意驱动版本要匹配MySQL Connector/J5.1.49配 5.7或 8.0.33配 8.0驱动类名不同8.0 是com.mysql.cj.jdbc.Driver如果你机器上只装了高版本 JDK可以在 IDEA 里给项目单独配一个 JDK 8 的 SDK不必卸载当前的 JDK 17。Tomcat 也一样IDEA 里可以同时配置多个 Tomcat Server每个项目独立指定。3.2 导入工程与修改数据库配置在 IDEA 中导入工程时选「Open」而不是「New」然后定位到源码解压目录。如果项目里有.idea或.project文件IDEA 会识别为已有工程没有的话就直接把 src 目录标记为 Sources Root把 web 目录标记为 Web Resources。接下来最重要的一步是改数据库配置。项目里的JdbcUtils.java或db.properties文件里会有数据库连接信息默认密码是 root如果你的 MySQL 密码不一样运行时会直接在建立连接时报错。jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456注意几个关键点useSSLfalse是为了避免 MySQL 8.0 的 SSL 警告serverTimezoneAsia/Shanghai是 MySQL 8.x 必须加的否则 JDBC 会报时区错误characterEncodingutf-8保证中文不乱码。把密码改成你自己的然后启动 MySQL导入项目附带的supermarket.sql数据库脚本。导入数据库用命令行就行mysql -uroot -p123456 supermarket.sql如果supermarket.sql文件路径里有空格先把文件复制到纯英文目录再执行。导入完成后用show tables验证一下应该有 user、product、cart、favorite、evaluate 等表。3.3 部署到 Tomcat 并验证核心流程在 IDEA 里配置 TomcatRun - Edit Configurations - 点加号 - Tomcat Server - Local。在 Deployment 标签页里把项目 artifact 添加进去Application context 设置为/supermarket或根路径/。URL 地址填http://localhost:8080/或项目实际入口页面。启动成功后打开浏览器访问首页。先用项目自带的测试账号登录源码的数据库脚本里一般会初始化管理员和普通用户账号密码在 README 或 SQL 文件中登录成功后再走一遍「搜索商品 - 加入购物车 - 查看购物车 - 结算」的完整流程。如果每一步都能正常跳转和回显说明核心链路没断。验证购物车逻辑时要重点看两个地方一是购物车列表里的商品数量是否正确显示二是刷新页面后购物车内容有没有消失正常应该还在因为数据存到了数据库。如果刷新后购物车空了说明代码里用的是 session 存取而不是数据库存取这种实现虽然也能跑但不符合这个项目的 Dao 层设计。4. 核心业务模块拆解购物车、收藏、评价与用户认证这个项目真正有学习价值的地方在四个业务模块购物车的加购逻辑、收藏的幂等处理、评价的关联查询以及用户登录时的会话管理。每个模块都对应一个或几个 DaoImpl 类把它们拆开看就等于把 JavaWeb 的增删改查实战过了一遍。4.1 购物车模块从表结构到加减购的完整链路购物车表一般这样设计CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_product (user_id, product_id) );注意UNIQUE KEY uk_user_product (user_id, product_id)这个唯一约束是购物车表的关键。它保证了同一个用户不能对同一件商品插入多条记录如果已经存在就执行UPDATE quantity quantity 1而不是再插一条新数据。这是购物车模块第一个容易翻车的地方——很多新手在添加商品时不做存在性检查直接 insert结果购物车列表出现两行相同的商品。CarDaoImpl 里加购的 SQL 通常是public void saveOrUpdateCartItem(Connection conn, int userId, int productId, int quantity) { String checkSql SELECT id FROM cart WHERE user_id ? AND product_id ?; String updateSql UPDATE cart SET quantity quantity ? WHERE user_id ? AND product_id ?; String insertSql INSERT INTO cart(user_id, product_id, quantity) VALUES(?, ?, ?); // 先查再根据查询结果决定 update 还是 insert }这种写法叫「先查后改」在并发不高的教学环境里没问题。如果追求更好的性能可以用INSERT ... ON DUPLICATE KEY UPDATE quantity quantity VALUES(quantity)一条 SQL 搞定。项目原代码里大概率用的是前者你能看懂就够了。4.2 收藏与评价模块FavoriteDaoImpl 与 EvaluateDaoImpl 的典型写法收藏和评价是电商系统的标配模块。FavoriteDaoImpl 的核心逻辑是「判断是否已收藏」和「切换收藏状态」。收藏表同样建议加唯一约束(user_id, product_id)这样isFavorite的查询就变得很简洁public boolean isFavorite(Connection conn, int userId, int productId) { String sql SELECT COUNT(*) FROM favorite WHERE user_id ? AND product_id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setInt(2, productId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return rs.getInt(1) 0; } } } catch (SQLException e) { e.printStackTrace(); } return false; }SELECT COUNT(*)比SELECT *更适合用来判断存在性因为不需要传输具体字段结果集只有一行一列性能更好。切换收藏状态时先查 isFavorite再决定是 delete 还是 insert这就是「幂等」——无论前端点了多少次收藏按钮数据库最终状态只取决于最后一次操作。EvaluateDaoImpl 则涉及关联查询。评价列表页面通常要展示「商品名称 评价内容 评价人昵称」但评价表里只存了 product_id 和 user_id所以需要 join 一下SELECT e.id, e.content, e.score, e.create_time, p.name AS product_name, u.nickname AS user_nickname FROM evaluate e JOIN product p ON e.product_id p.id JOIN user u ON e.user_id u.id WHERE p.id ? ORDER BY e.create_time DESC这个 SQL 把三张表关联起来了。如果项目里写的是多段查询再在内存里拼装也没错但看 SQL 功底就看这里——一条 join 比三次单表查询效率高得多。4.3 用户认证session 管理与密码安全用户登录是 JavaWeb 里最常考的知识点之一。UserDaoImpl 里通常有getUserByUsernameAndPassword方法但这里的「密码」可以是明文也可以是哈希。教学项目里十有八九是明文存储这不安全但反而是教学的好素材——你可以自己改成 BCrypt 加密。登录成功的核心是 session 设置HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(3600); Cookie cookie new Cookie(JSESSIONID, session.getId()); cookie.setMaxAge(3600); resp.addCookie(cookie);setMaxInactiveInterval(3600)表示 session 一小时过期。Cookie 的作用是让浏览器在关闭后重新打开时还能找回 session但把 JSESSIONID 的 MaxAge 设为 3600 会让 cookie 变持久化 cookie关浏览器后登录态还在这既是方便也是安全隐患。实际项目里通常会设置cookie.setHttpOnly(true)防止 XSS 攻击通过 JavaScript 偷取 session ID。登出的时候要session.invalidate()而不是只session.removeAttribute(loginUser)。invalidate 会让整个 session 失效旧 session ID 作废更安全。这个细节如果面试时被问到能答出来就是加分项。5. 避坑指南导入运行阶段五个高频问题排查这个项目我跑了不止一遍每次换环境都踩过不同的坑。下面把这几个最常见的现象、原因和解决方式写清楚你照着排能省很多时间。5.1 现象Tomcat 启动即报 ClassNotFoundException启动日志里出现java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或者找不到某个 Servlet 类。原因是 MySQL 驱动 jar 包没有放到 WEB-INF/lib 目录下或者项目没有把驱动包添加到 artifact 里。IDEA 里单纯在「External Libraries」里添加了 jar 还不够必须确保 deploy 时 jar 会打包到WEB-INF/lib。解决把mysql-connector-java-x.x.x.jar复制到web/WEB-INF/lib目录下然后右键该 jar -Add as Library再重新启动 Tomcat。也可以打开 Project Structure - Artifacts在 Output Layout 里确认 jar 是否在 WEB-INF/lib 下。5.2 现象启动后访问页面报 CommunicationsException: Communications link failure页面打开时报CommunicationsException: Communications link failure或Connection refused。原因是数据库没启动、密码错误、或 URL 里的端口/IP 配置不对。链路失败的根因很多时候是 MySQL 服务没起来而不是代码问题。解决先在命令行执行mysql -uroot -p确认 MySQL 能连上。然后检查 db.properties 里的jdbc.url如果是jdbc:mysql://localhost:3306/supermarket确认端口是 3306 且数据库名正确。如果用了 MySQL 8.0连接驱动必须是com.mysql.cj.jdbc.DriverURL 里必须带serverTimezoneAsia/Shanghai。5.3 现象页面显示中文字符为问号或乱码浏览器里商品名称、用户昵称显示为???或乱码数据库里存的中文也是乱码。原因是三层链路中某一层编码不一致JSP 页面编码、Tomcat 请求编码、数据库连接编码、数据库表编码。常见的是数据库表默认是 latin1或者 JDBC URL 没带characterEncodingutf-8。解决先改 JDBC URL加?useUnicodetruecharacterEncodingutf-8。再检查数据库表用ALTER TABLE product CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;转换。JSP 文件顶部加上% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%。最后在 Tomcat 的server.xml里给 Connector 加一行URIEncodingUTF-8防止 GET 请求参数里的中文乱码。5.4 现象JSP 里 EL 表达式原样输出${}不被解析页面上直接显示$(name)或空值没有替换成变量内容。原因是 web.xml 里设置了isELIgnored或者 web.xml 版本太老2.3 及以下默认忽略 EL。也可能是 JSP 文件没有声明 JSTL 标签库。解决检查 web.xml 根元素确保它是 Servlet 3.0 以上版本web-app 的 version 属性为 3.0 或更高。然后在 JSP 里引入 JSTL core 标签库% taglib prefixc urihttp://java.sun.com/jsp/jstl/core%。如果项目里没有 JSTL jar去下载jstl-1.2.jar放到 WEB-INF/lib。5.5 现象购物车结算时数据不对出现负数库存或重复记录连续快速点击加购按钮购物车里出现两条相同商品或者结算时库存变成负数。原因是我在前面第 4 节里说过的没有对购物车表做唯一约束也没有在 Service 层处理并发请求。快速点击会导致两个请求同时进来都判断为「不存在」都执行 insert于是重复。解决给 cart 表加唯一索引UNIQUE KEY uk_user_product(user_id, product_id)同时在 CarDaoImpl 里用ON DUPLICATE KEY UPDATE替代「先查后改」。库存扣减的 SQL 也要改成UPDATE product SET stock stock - ? WHERE id ? AND stock ?这样即使并发数据库也会保证不会扣成负数。6. 进阶技巧把传统 JavaWeb 项目改造成 Maven 结构并配好 IDEA 调试最后这一步是我最推荐的落地练习。项目源码如果是普通目录没有 pom.xml依赖全靠手动拷 jar这种方式在团队协作和版本管理上非常痛苦。花半小时把它改成 Maven 结构不仅能让你看清楚依赖关系还能直接获得 IDEA 的自动构建和热部署体验。6.1 改造步骤从普通目录到 Maven Webapp先在项目根目录新建pom.xml指定打包方式为 warproject xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.supermarket/groupId artifactIdsupermarket-web/artifactId version1.0-SNAPSHOT/version packagingwar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency /dependencies build finalNamesupermarket/finalName /build /project注意javax.servlet-api的 scope 是provided因为 Tomcat 自带 Servlet 容器打包时不需要重复打进 war。但 JSTL 和 MySQL 驱动必须设为默认的 compile否则运行时找不到。创建 Maven 标准目录结构把原来的src下的com包移到src/main/java/com把web目录内容移到src/main/webapp把 SQL 脚本放到src/main/resources/db/supermarket.sql在 IDEA 里右键项目根目录选择Add Framework Support - MavenIDEA 会自动识别 pom.xml。如果原来的 WEB-INF/lib 下还有手动拷的 jar全部删掉让 Maven 统一管理。6.2 配置 Tomcat 调试并验证改造结果改造完成后重新配置 Tomcat这次用 Maven 的 war 包来部署。在 IDEA 的 Tomcat 配置页面Deployment 选项卡点击加号选择Artifact-supermarket:war exploded。这个探索模式exploded支持热部署修改 JSP 或静态资源后刷新页面就能看到效果不用每次重启 Tomcat。启动前先把原来的数据库删掉重建mysql -uroot -p -e DROP DATABASE IF EXISTS supermarket; CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4; mysql -uroot -p supermarket src/main/resources/db/supermarket.sql启动后如果页面正常显示且登录购物流程能走通说明改造成功。此刻你再改代码比如给 User 实体加一个 email 字段从 Mapper 到 Service 到 JSP 一整套改下来IDEA 的编译错误提示会让你深刻体会到 Maven 工程的好处——依赖缺失、类名拼写、版本冲突全部在编译期暴露而不是等到 Tomcat 启动才报红。从那以后我每次接手老项目第一件事就是检查有没有 pom.xml。没有的话先花半小时做一次这个改造把散落的 jar 清干净把编码统一把数据库脚本放进 resources后面所有调试成本都会大幅下降。改过一次之后你对这个超市系统的理解会比单纯跑通一遍深得多——因为你在重构它而不是在围观它。希望这篇笔记帮到你动手拆吧。本文还有配套的精品资源点击获取
返回列表