☰
Java Swing+MySQL超市管理系统:CRUD到事务控制实战解析
2026/10/18 11:58:41 网站建设 项目流程

简介:一款基于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 执行都行,我这里说命令行方式,因为最不容易出幺蛾子。打开终端,登录 MySQL:

mysql -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?useUnicode=true&characterEncoding=utf8"; String user = "root"; String password = "123456"; }

如果使用的是 MySQL 8.0,驱动类要改成com.mysql.cj.jdbc.Driver,url 中建议追加两个参数:

jdbc:mysql://localhost:3306/swing_goods?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false

不追加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 List<Goods> 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(); List<Goods> 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 的查询方法,得到List<Goods>,然后遍历这个列表,把每一行数据加入表格模型。这个期间必须注意一个线程问题:Swing 的事件分发线程(EDT)里执行数据库操作时,界面会出现短暂卡死。这个项目的规模下,数据量不大,卡顿不明显,但如果未来商品数据量到了上万条,这种卡顿就会被放大。常见的优化方案是改用SwingWorker异步执行查询,但这个项目源码里大概率没有做这一步,因为它的定位是教学演示。

DefaultTableModel model = new DefaultTableModel(new Object[]{"编号", "商品名", "价格", "库存"}, 0); GoodsDao dao = new GoodsDao(conn); List<Goods> 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 连接串里是否加了characterEncoding=utf8。

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())把字符数组转回字符串时,如果项目更严谨,应该在比对结束后立刻把数组填充为 0:

char[] 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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

如果你在别的项目里看到有人加了useSSL=false,是因为本地开发连接不需要加密传输,加上之后还能减少握手时间。这个报错在 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 一劳永逸。同时确认连接串里的characterEncoding=utf8,两个条件同时满足,中文问题才能根治。

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 和数据库交互的桥梁,结构比大多数网上零散教程要完整得多。希望你能把它跑起来之后,再动手改一两个功能,学到的东西会比只看不练扎实得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询