ARTICLE DETAIL

资讯详情

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

JavaWeb超市会员管理系统:JSP+Servlet+JDBC毕设实战解析

JavaWeb超市会员管理系统:JSP+Servlet+JDBC毕设实战解析 简介这是一套基于Javaweb的超市会员管理系统毕业设计项目面向计算机相关专业正在做毕设的学生以及需要项目实战练习的Java学习者也可作为课程设计或期末大作业使用。系统采用JSP、Servlet、JDBC配合MySQL数据库开发环境为JDK、Eclipse和Tomcat功能涵盖会员管理、超市运营等核心模块界面简洁、操作便捷。压缩包共742个文件、约15.72MB以jpg/png图片、java源码、js脚本、css样式、html页面和sql数据库脚本为主辅以xml配置与项目说明目录清晰方便导入开发工具直接运行。资源经严格调试确保可运行附带数据库脚本、软件工具与项目使用说明帮助快速搭建环境并理解完整实现逻辑。目前已有668人学习下载适合需要完整毕业设计参考方案或实战练习的读者。1. 这套Javaweb超市会员管理系统毕设能跑、课设能交、练习能上手做毕设最怕的不是题目难而是拿到一套号称能运行的源码导入Eclipse后满屏红叉Tomcat一启动就报ClassNotFound。这套基于Javaweb的超市会员管理系统是我近期拆过的最省心的一套JSPServletJDBC老牌技术栈项目。后台没有花哨的Spring全家桶就是最经典的JSP页面、Servlet控制层、JDBC操作MySQL界面用Bootstrap框架搭的功能覆盖会员管理、商品管理、积分兑换这些超市核心业务场景。对计算机专业正在做毕业设计的学生来说它可以直接当毕设交对想练Javaweb基本功的初学者来说它比SSM项目更好读懂请求流转过程。项目自带数据库脚本和工具包配置好环境就能跑。2. 系统功能拆解与技术选型为什么JSPServlet这套老技术还能打2.1 功能模块全览会员、商品、积分、消费记录这套系统解压后先从web.xml入手看了一遍映射关系能清楚看到它把超市会员业务拆成了几个核心模块。模块核心功能对应Servlet/Dao会员管理会员注册、信息查询、余额充值MemberServlet、MemberDao商品管理商品录入、上下架、库存修改GoodsServlet、GoodsDao消费结算会员购物结账、积分累计CheckoutServlet积分管理积分累计、积分兑换商品/优惠ScoreServlet系统管理管理员登录、密码修改AdminServlet这个功能划分很典型每个业务域对应一个Servlet接收请求再转给DAO层操作数据库。JSP只负责展示数据没有把业务逻辑写在页面里——这是这套源码比很多培训机构的“纯JSP项目”强的地方。会员管理是核心链路从会员注册到消费积分再到积分兑换闭环完整答辩时讲解器来非常顺手。2.2 为什么选ServletJDBC而不是MyBatis现在Javaweb毕设很多用Spring BootMyBatis但这套系统坚持用ServletJDBC反而成了优势。至少有三点值得说第一包结构一目了然。controller、dao、entity、util四个包把职责分清楚Entity对应数据库表字段DAO层写JDBC代码Servlet里做请求参数封装和转发跳转。这比Spring Boot里动不动就十几层目录结构好理解得多。第二能清楚看到一次HTTP请求从JSP表单到Servlet再到数据库的完整路径。比如会员注册功能JSP里form表单提交到MemberServlet的doPost方法Servlet里调用MemberDao的addMember方法DAO里用PreparedStatement执行SQL。这是最原始的Javaweb请求流程面试笔试常考答辩时老师问“数据是怎么存进去的”直接对着代码讲就行。第三对做课设的学生来说JDBC手动管理连接能让你真正理解数据库连接池存在的意义。项目里用的是DriverManager获取连接性能上不如连接池但对于几百条测试数据量的课程设计完全够用而且不需要额外配置c3p0或Druid依赖环境复杂度更低。2.3 项目目录结构与配置文件说明拿到项目后理解目录结构是第一步。SupermarketMember/ ├── src/ │ ├── com/supermarket/entity/ # 实体类Member.java、Goods.java等 │ ├── com/supermarket/dao/ # 数据访问层JDBC操作 │ ├── com/supermarket/servlet/ # 控制层接收请求、跳转页面 │ ├── com/supermarket/util/ # 工具类DBUtil.java、MD5Util.java │ └── db.properties # 数据库连接配置 ├── WebContent/ │ ├── css/ # Bootstrap、自定义样式 │ ├── js/ # jQuery、jQuery UI │ ├── images/ # 图片资源 │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ └── lib/ # JSTL等依赖jar包 │ └── *.jsp # 页面文件 └── sql/ └── supermarket.sql # 数据库初始化脚本重点关注db.properties这是数据库连接的关键配置。常见做法是把数据库驱动类、连接URL、用户名、密码都放在这个文件里DBUtil读取Properties文件来创建连接。我一般建议学生改完这个文件后先单独写个测试类跑一下连接不要一上来就启动Tomcat。3. 数据库设计与初始化从SQL脚本到关系梳理3.1 核心数据表设计这套系统的数据库脚本里包含了六张核心表表间关系紧密覆盖了超市会员业务的主体流程。第一次打开sql目录下的supermarket.sql别急着执行先看表结构。-- 管理员表 CREATE TABLE admin ( admin_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 管理员ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(64) NOT NULL COMMENT 密码(MD5加密) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 会员表 CREATE TABLE member ( member_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 会员ID, card_no VARCHAR(20) NOT NULL UNIQUE COMMENT 会员卡号, name VARCHAR(50) NOT NULL COMMENT 姓名, phone VARCHAR(11) COMMENT 手机号, balance DECIMAL(10,2) DEFAULT 0.00 COMMENT 账户余额, score INT DEFAULT 0 COMMENT 积分, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品表 CREATE TABLE goods ( goods_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 商品ID, goods_name VARCHAR(100) NOT NULL COMMENT 商品名称, price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT DEFAULT 0 COMMENT 库存, status TINYINT DEFAULT 1 COMMENT 1上架 0下架 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这个建表语句有几个细节值得注意一是字符集用了utf8mb4而不是utf8避免了生僻字和特殊符号存不进去的隐患二是余额字段用DECIMAL(10,2)而不是FLOAT金额计算不会出现精度误差——这一点在答辩时如果被问到“为什么不用double”能答上来会很加分三是会员卡号设置了UNIQUE约束防止重复办卡。3.2 表关系与业务流转会员表与积分明细表、消费记录表是多对一关系。消费记录表是关键表它关联会员和商品结构大致如下CREATE TABLE consume_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL COMMENT 所属会员, goods_id INT NOT NULL COMMENT 购买商品, amount DECIMAL(10,2) NOT NULL COMMENT 消费金额, score_earned INT DEFAULT 0 COMMENT 获得积分, consume_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_record_member FOREIGN KEY (member_id) REFERENCES member(member_id), CONSTRAINT fk_record_goods FOREIGN KEY (goods_id) REFERENCES goods(goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;理解了表关系看代码就有方向了。会员消费时做的事情是插入一条消费记录、更新会员表里的余额和积分、扣减商品库存。这三步操作在DAO层里分别对应三个方法设计中比较理想的做法是把它们放到一个事务里执行过程中任何一步失败都要回滚。这套源码里的事务处理分两种写法CheckoutServlet里的操作在Service层包裹了事务控制而部分简单的更新操作是单条SQL执行——复现时建议重点看消费结算这条链路的事务处理。3.3 初始化数据库的两种方式执行SQL脚本有两种方式我用Navicat和命令行都跑通过。方式一用Navicat或其他可视化工具导入打开Navicat创建数据库supermarket字符集选utf8mb4然后右键选择“运行SQL文件”选中项目附带的supermarket.sql。执行完毕后在表列表里能看到全部六张表以及预置的测试数据。方式二命令方式执行mysql -uroot -p123456 sql/supermarket.sql参数说明-u后面跟MySQL用户名-p后面跟密码注意-p和密码之间没有空格。执行后可以用SHOW TABLES;确认一下是否建表成功。3.4 数据库连接配置与JDBC工具类数据库建好后修改src目录下的db.propertiesjdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456我强烈建议你把serverTimezoneAsia/Shanghai加上去MySQL 8.x版本如果不指定时区连接时会直接报The server time zone value йʱ is unrecognized这个报错我见过太多学生踩过。另外useSSLfalse也建议保留本地开发环境不需要SSL加密省去证书相关的麻烦。DBUtil工具类里的代码是老套路加载驱动、读取Properties、创建连接public class DBUtil { private static String driver; private static String url; private static String username; private static String password; // 静态代码块只执行一次加载配置 static { try { Properties props new Properties(); InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties); props.load(in); driver props.getProperty(jdbc.driver); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); Class.forName(driver); } catch (Exception e) { e.printStackTrace(); } } // 获取连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }这段代码的逻辑静态代码块在类加载时执行把db.properties里的四项配置读进静态变量然后Class.forName注册驱动。后续DAO层直接调用DBUtil.getConnection()就能拿到连接。4. 项目导入与运行避坑EclipseTomcat环境下的常见问题排查4.1 导入Eclipse的具体步骤这套项目在Eclipse中导入时按标准动态Web项目的方式处理。新手最容易在这一步卡住。File - Import - General - Existing Projects into Workspace然后Select root directory选中项目根目录就是包含pom.xml或.project文件的层级Finish完成导入。如果导入后没有显示成Dynamic Web Project右键项目 - Properties - Project Facets勾选Dynamic Web Module 3.0和Java 1.8点击Apply。这一步很关键很多同学导入后看到的是普通Java项目JSP文件没有语法高亮就是Project Facets没配置对。我一般会顺手检查一下Java Compiler的版本是否与JDK匹配如果不匹配会直接导致编译失败。4.2 Tomcat版本选择与启动这套系统是Servlet 3.0规范Tomcat 7及以上都能跑但我推荐用Tomcat 8.5。Tomcat 9和10对老项目的兼容性有时会出问题尤其是javax.servlet包名在Tomcat 10里换成了jakarta.servlet——如果项目里import的是javax.servletTomcat 10直接编译报错。这是血泪经验。# 检查Tomcat是否正常启动 sudo sh startup.sh # 或者Windows下双击startup.bat # 启动后访问 http://localhost:8080/supermarket/login.jsp如果端口被占用在Tomcat安装目录的conf/server.xml里修改端口Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 /把8080改成8088或其他空闲端口改完重启Tomcat再访问http://localhost:8088/supermarket/login.jsp。4.3 三个高频报错与解决方案现象一启动Tomcat时报ClassNotFoundException: com.mysql.jdbc.Driver。原因MySQL驱动jar包没有放到WEB-INF/lib目录下或者放到了但没被编译。IDE里虽然能看到jar包但运行时找不到是部署配置问题。解决确认WEB-INF/lib目录里有mysql-connector-java的jar包然后在Eclipse里右键项目 - Properties - Deployment Assembly点击Add - Java Build Path Entries把MySQL驱动jar包加进去保存后重新发布项目。不同IDE的Project Structure里也需要确认Artifacts中是否包含了这个jar包。现象二页面中文乱码。原因jsp页面编码、MySQL连接参数、表字符集三层中至少有一层没统一。解决三步走。第一步把db.properties的jdbc.url加上characterEncodingutf8第二步确认建表时用了utf8mb4字符集第三步在JSP页面顶部加上% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %如果还乱码检查一下HTML的meta charsetUTF-8标签。这四层统一后乱码问题基本绝迹。现象三修改了Java代码但运行时页面没变化。原因Tomcat没有热部署或者IDE里没勾选自动编译。解决Eclipse里双击Servers视图中的Tomcat实例在Server Options里勾选Serve modules without publishing和Automatically reload when resources change。改代码后手动CtrlS保存再刷新浏览器。要注意的是修改web.xml或lib目录下的jar包时还是要重启TomcatJSP和Java类的热部署范围是有限制的。4.4 MySQL 8.x兼容性排查很多学生机器上装的是MySQL 8.x而老项目用的驱动可能是5.x版本。常见做法是换用与MySQL 8.x匹配的驱动dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency同时确认db.properties里的driver类名是com.mysql.cj.jdbc.Driver不要漏掉中间的cj。MySQL 8下还要注意时区参数这个前面提过了。另外MySQL 8默认的认证插件是caching_sha2_password老版本驱动可能不兼容如果连接时报Auth plugin错误需要给MySQL用户改回兼容的加密规则ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456;这是一条非常实用的命令写完这条再重启Tomcat连接就回来了。5. 核心业务代码走读会员注册、消费结算的实现链路5.1 会员注册从JSP表单到DAO入库会员注册是整个系统最基础也最完整的链路。JSP页面接收用户输入表单提交到MemberServletWebServlet(/member/register) public class MemberRegisterServlet extends HttpServlet { private MemberDao memberDao new MemberDao(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 设置编码防止中文乱码 request.setCharacterEncoding(UTF-8); // 2. 接收表单参数 String name request.getParameter(name); String phone request.getParameter(phone); String cardNo request.getParameter(cardNo); // 3. 封装实体对象 Member member new Member(); member.setName(name); member.setPhone(phone); member.setCardNo(cardNo); member.setBalance(0.0); member.setScore(0); // 4. 调用DAO层入库 boolean success memberDao.addMember(member); if (success) { response.sendRedirect(request.getContextPath() /member/list); } else { request.setAttribute(errorMsg, 注册失败请检查信息); request.getRequestDispatcher(/member/register.jsp).forward(request, response); } } }这段代码的要点第一步一定要先设置request编码否则中文姓名和手机号在参数获取阶段就乱码了第三步手动封装实体对象这是Servlet时代最标准的写法后续框架里的自动参数绑定就是替代了这个枯燥的过程第四步根据DAO层返回值决定跳转还是转发——重定向是重新发起一次请求能避免表单重复提交转发则保留request域里的数据用来显示错误信息。这个差异经常被问建议记住。MemberDao里的addMember方法用PreparedStatement执行插入public boolean addMember(Member member) { String sql INSERT INTO member(card_no, name, phone, balance, score) VALUES(?,?,?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, member.getCardNo()); ps.setString(2, member.getName()); ps.setString(3, member.getPhone()); ps.setDouble(4, member.getBalance()); ps.setInt(5, member.getScore()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }这里用了try-with-resources语法连接和PreparedStatement在try结束后自动关闭不用手动写finally关了是JDK 7引入的写法。PreparedStatement有个天然优势——预编译防SQL注入参数用?占位调用setString时驱动会处理特殊字符所以不要用字符串拼接SQL这是老项目里最常见的坑。5.2 消费结算事务与积分计算消费结算是整套系统里逻辑最复杂的模块。会员购买商品时涉及到三张表同时更新public boolean checkout(int memberId, int goodsId, int quantity) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 1. 查询商品信息单价、库存 Goods goods goodsDao.getGoodsById(conn, goodsId); double totalAmount goods.getPrice() * quantity; // 2. 更新商品库存 int stock goods.getStock() - quantity; goodsDao.updateStock(conn, goodsId, stock); // 3. 更新会员余额 memberDao.updateBalance(conn, memberId, totalAmount); // 4. 插入消费记录累计积分 int earnedScore (int)(totalAmount / 10); consumeDao.insertRecord(conn, memberId, goodsId, totalAmount, earnedScore); memberDao.updateScore(conn, memberId, earnedScore); conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { e.printStackTrace(); try { conn.rollback(); // 一旦出错全部回滚 } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { try { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }事务的核心在于setAutoCommit(false)之后所有SQL都驻留在同一个事务中只有调用了commit才真正落盘。如果在第3步更新余额失败第2步已经改掉了库存这时候必须rollback把库存改回来否则就出现“库存减了但钱没扣”的数据不一致。这是数据库事务ACID里一致性最典型的体现答辩时讲这个例子非常加分。积分计算规则是每消费10元累计1分(int)(totalAmount / 10)这个写法对金额做了向下取整——消费99元得9分。这个规则在业务层写死了如果题目要求改成“消费1元累计1分”只需修改这一行的除数。5.3 分页查询与模糊搜索会员列表页用了jQuery UI和Bootstrap框架做的分页组件后端DAO层是标准的LIMIT分页查询public ListMember findMembers(String keyword, int pageNum, int pageSize) { ListMember members new ArrayList(); StringBuilder sql new StringBuilder(SELECT * FROM member WHERE 11); ListObject params new ArrayList(); if (keyword ! null !keyword.isEmpty()) { sql.append( AND (name LIKE ? OR phone LIKE ? OR card_no LIKE ?)); String pattern % keyword %; params.add(pattern); params.add(pattern); params.add(pattern); } sql.append( LIMIT ?, ?); params.add((pageNum - 1) * pageSize); params.add(pageSize); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } ResultSet rs ps.executeQuery(); while (rs.next()) { Member m new Member(); m.setMemberId(rs.getInt(member_id)); m.setName(rs.getString(name)); m.setPhone(rs.getString(phone)); m.setCardNo(rs.getString(card_no)); m.setBalance(rs.getDouble(balance)); m.setScore(rs.getInt(score)); members.add(m); } } catch (SQLException e) { e.printStackTrace(); } return members; }这里用了StringBuilder拼接SQL两边加%实现模糊匹配。用WHERE 11构造动态条件是为了后续不管有没有keyword都能直接拼接。LIMIT后面的(pageNum-1)*pageSize是分页的核心计算第一页从第0条开始取第二页从第10条开始取假设pageSize10。这个写法要理解答辩时老师经常会问“分页为什么是从0开始而不是从1”。6. Tomcat部署上线技巧把毕设从本地搬到服务器6.1 导出WAR包手动部署本地跑通只是第一步答辩演示时的稳妥做法是直接在Tomcat的webapps目录下运行。在Eclipse里右键项目 - Export - WAR file导出一个supermarket.war包放到Tomcat的webapps目录下。启动Tomcat后它会自动解压部署访问路径就是http://你的IP:8080/supermarket/。6.2 生产环境下的关键调优本地开发环境追求能跑就行但部署到答辩演示的服务器或虚拟机时有些细节值得调整也很容易在答辩时被问到。连接池替换在本机测几十条数据时每次手动建连接完全可行但一旦数据量上来或者并发访问量变大频繁创建连接的开销不可忽视。常见做法是把源码里的DriverManager连接方式换成DBCP或Druid连接池。用Druid时只需要在DBUtil的静态代码块里初始化一个DruidDataSource后续getConnection时从池里拿private static DruidDataSource dataSource new DruidDataSource(); static { try { dataSource.setDriverClassName(com.mysql.cj.jdbc.Driver); dataSource.setUrl(jdbc:mysql://localhost:3306/supermarket?...); dataSource.setUsername(root); dataSource.setPassword(123456); dataSource.setInitialSize(5); dataSource.setMaxActive(20); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }Druid连接池的核心参数initialSize是启动时创建的物理连接数maxActive是最大连接数超过这个数的新请求就要排队等待连接归还。讲课设项目时把DriverManager替换成连接池原理说清楚这段属于加分项。JVM内存参数如果Tomcat在答辩演示的过程中页面上有大表格或图片可能导致卡顿可以在catalina.sh里调整JVM启动参数JAVA_OPTS-Xms512m -Xmx1024m -XX:MaxPermSize256m这个参数的意义JVM初始分配512MB堆内存最大允许1024MB。堆内存过小时频繁触发Full GC会导致页面卡顿设大一些能缓解大数据量加载时的压力。MySQL乱码一劳永逸方案修改MySQL配置文件my.iniWindows或my.cnfLinux底部的[mysqld]段落加入[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci改完后重启MySQL服务新创建的数据库默认就是utf8mb4不需要每条建表语句都手动指定字符集。这个统一配置比在JDBC连接上加characterEncoding参数更彻底因为后者只对当前连接生效前者对所有连接生效。6.3 答辩演示前的验证清单最后分享一个我每次上线前都会跑一遍的检查流程。先是登录管理员账号试着新增一个会员再去消费结算最后看记录把窗口关掉重开确认数据还在——验证持久化没问题。改掉db.properties里的数据库密码重启项目确认正常用新密码连接——这一步是为了防止答辩时换了机器漏改了配置。再确认一下WEB-INF/lib目录下的mysql驱动版本与服务器MySQL版本匹配MySQL 8就配8.0.x的驱动别混用。这套流程走下来大概也就十分钟但能避开九成以上的现场翻车。从那以后我每次帮学生部署毕设项目都强制自己先跑一遍这套验证清单再收工。希望这些细节能帮到你少走点我当年走过的弯路。本文还有配套的精品资源点击获取
返回列表