
简介一款基于Java、Swing和MySQL实现的超市商品管理系统源码包适合Java初学者、课程设计学生以及需要搭建小型进销存应用的开发者也适合作为课设与毕设的参考项目。系统围绕商品信息录入、库存跟踪、销售与进货等核心业务展开界面采用Swing组件构建数据由MySQL存储同时提供商品添加、查询、修改、删除以及供应商管理等操作模块。压缩包共46个文件包含11个Java源文件、28个class文件、SQL建表脚本、项目配置及说明文档整体大小1.59MB目录结构清晰便于直接导入IDE学习或二次开发。已有2305人学习下载尤其适合作为Java GUI编程与数据库综合应用的实践参考。资源附带了完整的界面代码、数据库表结构设计、事务与异常处理示例可帮助读者快速理解CRUD操作、系统分层思想及基本安全机制也能作为课程设计或毕业设计的模板使用。1. 这个 Java Swing MySQL 超市商品管理系统到底解决了什么问题如果你在超市收银台后面站过半天或者帮人理过货架就知道手工记账的痛商品名对不上、库存数靠脑子记、进价售价混在一张纸上。而拿到手里的这份 Java Swing MySQL 超市商品管理系统源码正是把「商品信息管理、库存变动、进货供应商关系、用户登录」这四件事统一收进一个桌面程序里。它的技术栈很经典——Java 写业务逻辑Swing 做图形界面MySQL 存数据适合正在做课设的在校生、想练手 Java GUI 的开发者以及需要快速搭一个轻量管理后台的从业者。它不是分布式高并发系统而是一个把 CRUD、布局、事件监听、异常处理讲清楚的教学型项目胜在结构完整能直接跑起来也能照着改。接下来我会带你从环境准备一路拆到事务控制把每一步的原理和坑都摊开讲。2. 环境与启动准备JDK 版本、MySQL 初始化与项目导入2.1 版本选型为什么 JDK 8 搭配 MySQL 5.7 最稳打开项目先看根目录goods_manager.rar、readme.md、src、bin、lib、swing_goods.sql、.classpath这些文件的排布暴露了这是一个 Eclipse 风格的传统 Java 项目。src 放源码bin 放编译产物lib 放第三方 jar 包swing_goods.sql 是数据库初始化脚本。我建议你复现时优先用 JDK 8别一上来上 JDK 17。原因是 Swing 在 JDK 8 之后几乎没有大的 API 变动但高版本 JDK 模块化之后部分旧项目的反射和内部类访问会报IllegalAccessError到时候排查起来很费时间。MySQL 建议 5.7 起步8.0 也能跑但 8.0 的密码加密插件和时区规则会让初次接触的人多踩两个坑这点在第 5 章会详细说。如果你本机已经装了 MySQL 8.0也不用急着卸载后面有对应的连接串参数改法。2.2 Eclipse 项目导入的三步操作拿到解压后的goods_manager文件夹不要直接双击运行里面的 class 文件那个只是编译中间产物。正确做法是把它当成一个完整项目导入 IDE。这里以 Eclipse 为例IDEA 的导入路径类似。打开 Eclipse菜单选File - Import - General - Existing Projects into Workspace然后点击Select root directory定位到goods_manager文件夹下方列表会出现一个名为 goods_manager 的项目勾选后点 Finish。注意导入时不要勾选Copy projects into workspace直接以原路径引用这样后续修改 sql 脚本和其他配置文件时不用在项目里外找两遍。导入后项目会报红绝大多数原因是缺少 MySQL 驱动 jar或者是 JRE 版本不匹配右键项目选Build Path - Configure Build Path在Libraries标签页里确认lib目录下的 jar 是否已经挂载如果没有点击Add JARs把 lib 里的驱动包添加进去。2.3 数据库初始化执行 swing_goods.sql数据库是这套系统的心脏swing_goods.sql这个文件负责建库建表。用命令行或者 Navicat 执行都行我这里说命令行方式因为最不容易出幺蛾子。打开终端登录 MySQLmysql -u root -p输入密码后执行下面的命令导入脚本source /你的绝对路径/swing_goods.sql;执行完后用show databases;检查是否出现指定的数据库名。一般这个脚本里会包含建库语句比如create database if not exists swing_goods;然后再use swing_goods;建表、插入初始数据。我一般会顺手执行下面这条 SQL 确认核心表存在use swing_goods; show tables;预期能看到 goods、supplier、user 之类的表名。如果执行 source 时报错ERROR 1064多半是文件路径里的反斜杠问题Windows 下把路径里的\改成/。如果报中文乱码那就是脚本文件的编码和 MySQL 客户端编码不一致最直接的解决方式是用 Notepad 打开 sql 文件确认右下角编码是 UTF-8不是 UTF-8 BOM然后把 MySQL 客户端的默认编码手动切一下set names utf8mb4;2.4 数据库连接参数配置的细节项目里一定有一个类负责创建数据库连接通常是DBUtil或ConnectionFactory之类的名字。打开这个类你会看到类似下面的静态配置块static { String driver com.mysql.jdbc.Driver; String url jdbc:mysql://localhost:3306/swing_goods?useUnicodetruecharacterEncodingutf8; String user root; String password 123456; }如果使用的是 MySQL 8.0驱动类要改成com.mysql.cj.jdbc.Driverurl 中建议追加两个参数jdbc:mysql://localhost:3306/swing_goods?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse不追加serverTimezone的话连接时很可能直接抛异常报错信息里明确写着Server returns invalid timezone。密码字段如果是你自己本地 MySQL 的密码务必改掉否则程序一启动连不上库界面还没看到就先看到一个红色堆栈。改完配置之后先不用急着打开主界面直接在 DBUtil 的main方法里跑一下连接测试这样能第一时间确认驱动 jar 和参数配置都对了。3. 数据库设计与 CRUD 落地从建表语句到功能界面3.1 核心表结构商品、供应商与用户谁在管理数据源码里swing_goods.sql是整个系统的地基它不仅定义了一张张表还把表之间的引用关系写清楚了。我打开这个脚本之后发现主表结构基本符合超市进销存的常规模型。商品表记录每个商品的具体信息比如商品编号、名称、价格、库存数量供应商表存的是供货商信息作用是在进货时知道货从哪来用户表存登录账号。这张表的字段命名是典型的 Java 风格驼峰式比如goodsId、goodsName、goodsPrice数据库和 Java 属性一一对应省了手动映射的麻烦。表之间的关联体现在外键上。比如商品表里有一个supplierId字段它引用供应商表的主键。这样设计的好处是你在修改供应商名称时商品表不用跟着改只改供应商表那一条记录就行了。下面是简化后的商品表创建脚本create table goods ( goodsId int primary key auto_increment, goodsName varchar(100) not null, goodsPrice decimal(10,2) default 0, goodsStock int default 0, supplierId int, constraint fk_supplier foreign key (supplierId) references supplier(supplierId) );decimal(10, 2)这个类型在这里是有讲究的商品价格用 double 存储会产生浮点误差比如 19.9 在二进制里表示不精确而 decimal 是定点数能确保金额计算的准确性。这也是这个项目值得学的一个点。库存字段用的是int比较简单直观但它有一个明显的边界问题库存不允许负值这一点在业务代码里需要加校验SQL 层面如果只靠字段类型是管不住负数写入的。3.2 DAO 层的实现套路自己动手写一个商品增删改查项目的 src 目录下cn开头的是主包路径里面基本按dao、model、ui、util分层。这种分层结构在 Java 课设里非常典型model 层放实体类dao 层放数据库操作方法ui 层放 Swing 界面util 层放工具类。下面是我重写的一个简化版商品 DAO和源码里的逻辑一致但更精简方便你理解核心套路public class GoodsDao { private Connection conn; public GoodsDao(Connection conn) { this.conn conn; } // 新增商品 public boolean insertGoods(Goods goods) throws SQLException { String sql insert into goods(goodsName, goodsPrice, goodsStock, supplierId) values(?, ?, ?, ?); PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, goods.getGoodsName()); ps.setBigDecimal(2, goods.getGoodsPrice()); ps.setInt(3, goods.getGoodsStock()); ps.setInt(4, goods.getSupplierId()); int rows ps.executeUpdate(); ps.close(); return rows 0; } // 根据商品名模糊查询 public ListGoods queryGoodsByName(String name) throws SQLException { String sql select * from goods where goodsName like ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, % name %); ResultSet rs ps.executeQuery(); ListGoods list new ArrayList(); while (rs.next()) { Goods g new Goods(); g.setGoodsId(rs.getInt(goodsId)); g.setGoodsName(rs.getString(goodsName)); g.setGoodsPrice(rs.getBigDecimal(goodsPrice)); g.setGoodsStock(rs.getInt(goodsStock)); list.add(g); } rs.close(); ps.close(); return list; } // 更新商品信息 public boolean updateGoods(Goods goods) throws SQLException { String sql update goods set goodsName ?, goodsPrice ?, goodsStock ? where goodsId ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, goods.getGoodsName()); ps.setBigDecimal(2, goods.getGoodsPrice()); ps.setInt(3, goods.getGoodsStock()); ps.setInt(4, goods.getGoodsId()); int rows ps.executeUpdate(); ps.close(); return rows 0; } }上面这段我故意省略了删除方法因为删除方法大同小异就是delete from goods where goodsId ?。关键要理解的是三个点第一PreparedStatement传参用setXxx而不是直接拼字符串这能挡住 SQL 注入风险也是项目里反复强调安全性的体现如果你在源码里看到有人用字符串拼接 SQL那拿到手之后建议立刻改成预编译第二executeUpdate()返回的是受影响行数用rows 0判断操作是否成功这是 JDBC 里最基础的判断方式第三like查询时通配符在 SQL 语法里写Java 代码里不做拼接避免中英文百分号混用导致的查不到数据。3.3 从 DAO 到表格显示把查询结果送到 JTable界面层和 DAO 层的衔接在项目里是通过一个DefaultTableModel对象实现的。点击界面的查询按钮后后台执行 DAO 的查询方法得到ListGoods然后遍历这个列表把每一行数据加入表格模型。这个期间必须注意一个线程问题Swing 的事件分发线程EDT里执行数据库操作时界面会出现短暂卡死。这个项目的规模下数据量不大卡顿不明显但如果未来商品数据量到了上万条这种卡顿就会被放大。常见的优化方案是改用SwingWorker异步执行查询但这个项目源码里大概率没有做这一步因为它的定位是教学演示。DefaultTableModel model new DefaultTableModel(new Object[]{编号, 商品名, 价格, 库存}, 0); GoodsDao dao new GoodsDao(conn); ListGoods goodsList dao.queryGoodsByName(txtName.getText()); for (Goods g : goodsList) { model.addRow(new Object[]{g.getGoodsId(), g.getGoodsName(), g.getGoodsPrice(), g.getGoodsStock()}); } goodsTable.setModel(model);这段代码的意图非常直白查询、循环、加行、刷新表格。这里有个新手容易翻车的地方JTable构造时如果直接传Vector嵌套结构后续修改数据需要强制刷新fireTableDataChanged()而上例用DefaultTableModel就不存在这个问题因为它自带数据变更通知机制。MySQL 的中文数据显示为乱码通常不是这段代码的锅而是要检查 JDBC 连接串里是否加了characterEncodingutf8。4. Swing 界面与业务联动布局管理器、登录认证与事件监听4.1 布局管理器怎么选BorderLayout 打底GridLayout 收尾打开 src 下的ui包主界面的类名一般是MainFrame或者ManagerUI打开后你会发现 Swing 布局的选择基本遵循一个规律外层用 BorderLayout中间面板用 GridLayout 或者 GridBagLayout。BorderLayout 把窗口分成东南西北中五个区域北区放搜索条件南区放按钮中间放JTable的滚动面板左区和右区通常闲置。这种布局的好处是窗口缩放时中间表格区域能自动拉伸按钮和标签的位置不会跳来跳去。GridLayout 则用于按钮面板。比如「新增」「修改」「删除」「刷新」四个按钮放在同一个 JPanel如果这个面板用 FlowLayout按钮会横排靠左不如 GridLayout 分两行两列整齐。源码里按钮面板很可能用的是 JPanel 嵌套——外面一个 JPanel 用 BorderLayout里面加两个子 Panel 分别用 FlowLayout 和 GridLayout。我在课设里也这么干过好处是直观易懂坏处是代码行数偏长后续调整布局时容易改一处坏一处。如果你想优化可以把多个 Panel 合并成一个 GridBagLayout 版。4.2 登录界面背后的认证逻辑不是摆设的安全防线这个项目带用户认证功能登录界面不是形象工程它校验的是 user 表里的用户名和密码。查了下源码里的登录按钮点击事件逻辑分几步接收输入框文本 → 调用 UserDao 按用户名查记录 → 比对密码是否一致 → 一致则打开主窗口不一致则弹提示框。这里有一个值得留意的细节如果源码里是把明文密码和数据库直接比对那安全性比较弱你可以改成先哈希再比对。下面这段是常见的登录校验写法我把它拆开讲String username txtUser.getText().trim(); String password new String(txtPwd.getPassword()); UserDao dao new UserDao(conn); User user dao.findByUsername(username); if (user ! null user.getPassword().equals(password)) { JOptionPane.showMessageDialog(loginFrame, 登录成功); new MainFrame(user).setVisible(true); loginFrame.dispose(); } else { JOptionPane.showMessageDialog(loginFrame, 用户名或密码错误); }注意两个细节密码框用的是getPassword()返回的是char[]而不是String这是 Swing 本身的安全考虑——字符数组用完可以被主动清空而 String 对象在字符串常量池里停留时间更长。另一个细节是trim()方法用户经常在输入用户名时手滑多打一个空格不 trim 就查不到对应记录界面弹一个「登录失败」让人莫名其妙。new String(txtPwd.getPassword())把字符数组转回字符串时如果项目更严谨应该在比对结束后立刻把数组填充为 0char[] pwd txtPwd.getPassword(); // 比对结束后 Arrays.fill(pwd, 0);4.3 商品表格的行内操作与事件监听表格界面的核心交互是选中一行 → 点击修改 → 弹出表单 → 回填数据 → 保存。这里有一个 Swing 开发里非常典型的监听器写法在表格上挂一个MouseListener捕获鼠标点击行的索引然后根据这一行的数据填充表单goodsTable.addMouseListener(new MouseAdapter() { Override public void mouseClicked(MouseEvent e) { int row goodsTable.getSelectedRow(); if (row 0) { int goodsId (int) goodsTable.getValueAt(row, 0); txtName.setText((String) goodsTable.getValueAt(row, 1)); txtPrice.setText(goodsTable.getValueAt(row, 2).toString()); txtStock.setText(goodsTable.getValueAt(row, 3).toString()); // 存住 goodsId方便保存时定位记录 currentEditId goodsId; } } });这个写法的特点是直接在鼠标事件里读取表格模型不需要额外维护选中对象。getValueAt(row, col)返回的是 Object 类型所以取数值字段后必须强转或者调toString()否则在 setText 时会编译报错。这里有个坑如果你在表格上点击时拿到了行号但表格数据后来刷新了getSelectedRow()的行号可能已经失效那是旧行里面存的可能不是你想改的那条。所以我建议在刷新表格之后调用goodsTable.clearSelection()把选中状态清掉防止脏数据残留导致改错商品。5. 避坑与常见问题排查数据库连接、中文乱码与界面卡死的五个典型场景5.1 运行时报错ClassNotFoundException: com.mysql.jdbc.Driver或com.mysql.cj.jdbc.Driver这是最常见的问题现象是程序一启动控制台立刻抛异常提示找不到驱动类。原因几乎可以锁定lib 目录下的 mysql-connector jar 没有成功引入到 Build Path。刚才在第 2 章导入项目时我提到了这个点如果当时漏了现在就得补上。解决方式右键项目 →Build Path→Configure Build Path→ 在Libraries标签页确认是否有 mysql-connector-java 的 jar没有就点Add JARs找到lib目录下的对应文件。如果你用的 MySQL 8.0还要确认 jar 是mysql-connector-java-8.0.x.jar而不是旧的 5.1.x否则驱动类名也得跟着改。5.2 连接 MySQL 8 时抛Unsupported timezone现象是程序启动后控制台报错内容包含Server returns invalid timezone。原因是 MySQL 8 的驱动要求显式指定时区。解决方式是在 JDBC 的 URL 里加参数这与我在第 2 章末尾提到的完全一致jdbc:mysql://localhost:3306/swing_goods?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai如果你在别的项目里看到有人加了useSSLfalse是因为本地开发连接不需要加密传输加上之后还能减少握手时间。这个报错在 MySQL 5.7 上几乎不会出现这也是很多选手第一次升级到 MySQL 8 时觉得「玄学」的原因——其实不是玄学是驱动版本带来的默认行为差异。5.3 新增商品时中文全部变成?问号现象是界面上输入中文商品名点确定后进数据库一看name 字段存的是???。原因有两个层次第一MySQL 表字段的 charset 不是 utf8第二JDBC 连接的 characterEncoding 没有配置。最直接的排查方式是先跑一条 SQL 看当前连接的编码show variables like character_set%%;如果显示 latin1那说明建表时没指定字符集。解决方式如果还能改重建表并声明字符集如果不能重建用下面的语句修改已有表的默认字符集alter table goods convert to character set utf8mb4;注意 utf8mb4 和 utf8 的区别emoji 和一些特殊生僻字在 utf8 下存不了用 utf8mb4 一劳永逸。同时确认连接串里的characterEncodingutf8两个条件同时满足中文问题才能根治。5.4 删除商品时控制台报Cannot delete or update a parent row现象是点删除按钮程序没弹异常对话框但后台日志冒出外键约束失败。原因是在商品表与供应商表之间存在外键约束如果所删除的供应商仍然被商品引用MySQL 会拒绝删除从而保证数据完整性。解决方式有两种如果你确认该商品已经停售可以在业务代码里先删除商品表记录再删除供应商记录顺序颠倒就会报上面的错。实际项目更通用的做法是逻辑删除也就是给商品表加一个isDeleted字段删除时执行update goods set isDeleted 1 where goodsId ?这样既保留了历史数据又避开了外键约束的限制。5.5 界面能打开但表格数据加载不出来进度条转圈现象是主窗口成功弹出商品表格一直是空白过几秒后整个窗口卡死无响应。原因是查询操作在事件分发线程EDT里同步执行数据库没及时响应时整个界面被阻塞。有一半的概率是前一次查询还没结束你又点了第二次查询连接池没有设置超时线程就堵在数据库等待上了。解决方式如果是这个项目自带的单连接写法先确认每次查询后连接有没有关闭。看 DAO 代码里的finally块是否调用了conn.close()如果没有连续操作几次后连接被占满后续查询只能等待。更好的做法是引入HikariCP连接池把等待时间控制在一定范围内但改动较大课设阶段不建议动结构只要保证连接必关即可。真正的长期修复是在查询按钮的事件里套一层SwingWorker让耗时的 SQL 在后台线程执行执行完再切回 EDT 刷新表格。6. 从 CRUD 到事务控制模拟一次完整的商品销售出库流程数据库设计部分提过事务处理拿这个超市项目来说一个最典型的场景是销售出库顾客买走一瓶水系统要做两件事——扣减商品表里该商品的水库存同时往销售记录表里插一条销售明细。这两步必须绑定成一个原子操作如果扣了库存但销售记录没插上账就对不上。Swing 界面里对应的动作是「结算」按钮点击之后的逻辑在SaleService类的saleGoods方法里。我一般建议在这个方法里显式开启事务因为 MySQL 的默认隔离级别是 REPEATABLE READ如果不开启事务但各自执行两条 SQL出现故障时数据就会半新半旧。下面是核心方法public boolean saleGoods(int goodsId, int quantity) { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); String updateStock update goods set goodsStock goodsStock - ? where goodsId ?; PreparedStatement ps1 conn.prepareStatement(updateStock); ps1.setInt(1, quantity); ps1.setInt(2, goodsId); ps1.executeUpdate(); String insertSale insert into sale_record(goodsId, saleQuantity, saleTime) values(?, ?, now()); PreparedStatement ps2 conn.prepareStatement(insertSale); ps2.setInt(1, goodsId); ps2.setInt(2, quantity); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); e.printStackTrace(); return false; } finally { JDBCUtil.closeConnection(conn); } }setAutoCommit(false)的意思是让 JDBC 不再把每一条 SQL 当作独立事务自动提交而是等手动调用commit()时统一提交。也就是说在ps1.executeUpdate()和ps2.executeUpdate()之间只要抛了任何异常程序就会走进 catch 块执行rollback()把已经扣掉的库存归还。这也是源码摘要里提到事务处理的落点你接手项目之后我建议全局搜索setAutoCommit凡是出现多条写操作的地方都检查是否成对出现 commit 和 rollback。只调用setAutoCommit(false)却忘写 commit 的话数据会一直锁在事务里其他连接查询会阻塞这是一类非常隐蔽的 bug。验证这个方法是否正确可以做一个很简单的实验把insertSale那行的 SQL 故意改成insert into sale_record(goodsId) values(?)让它缺少字段而报错然后看商品表的库存是否保持不变。如果事务生效库存应该原封不动如果事务没有生效你会发现库存少了 quantity 但销售记录不存在。这个实验虽然听起来有点「自毁」却是排查事务边界最有效的办法。从那以后我每次接手带数据库操作的 Swing 项目第一件事就是全局搜setAutoCommit第二件事是排查每个 DAO 方法里的连接是否在 finally 里关闭。这两步走完项目的基本数据安全就有底了。这个源码包的定位很清楚它不是生产级系统但作为 Java 学习者理解 Swing 和数据库交互的桥梁结构比大多数网上零散教程要完整得多。希望你能把它跑起来之后再动手改一两个功能学到的东西会比只看不练扎实得多。希望帮到你。本文还有配套的精品资源点击获取