JDBC课设实战:超市管理系统分层设计与避坑指南
发布时间:2026/10/1 4:15:20来源:尧图网络
简介面向Java课程设计的超市管理系统项目包基于Java与MySQL实现商品信息、库存及销售记录等后台管理功能以命令行交互为主聚焦后端逻辑与数据库操作适合高校学生完成课设或练习JDBC开发。压缩包共49个文件含22个Java源码、22个编译后的class文件、1个SQL建表脚本、1个MySQL驱动jar包以及project/classpath等工程配置整体仅783KB结构按control、dao、po、db、view、main等模块划分。目前在CSDN已有1379人学习下载。项目中体现了JDBC连接、SQL增删改查、异常处理、多线程并发、设计模式等核心知识点并附有数据库建表脚本与工程配置可直接导入IDE运行参考其分层设计与模块划分可快速完成同类超市管理后端课设。1. 拿到这个项目先别急着跑一个纯 JDBC 课设包的真实面貌「JAVA课设-超市管理系统」这个压缩包没有 Spring Boot没有 MyBatis甚至没有 Maven——解压后就是 src 下几个包、bin 下编译产物、supermarket.sql 数据库脚本外加 lib 里一个孤零零的 mysql-connector-java-5.1.18-bin.jar。这正是老式 Java 课程设计的典型形态Swing 做界面、JDBC 连 MySQL、DAO 层手写 SQL。它要解决的问题很具体商品信息维护、库存增减、销售记录写入再把这几件事通过一个不太好看但能用的桌面窗口串起来。适合两类人正在做 Java 课设想找参考的学生以及想补 JDBC 基本功、想看分层代码长什么样的初学者。它的价值不在界面多漂亮而在「一个类管一件事」的朴素结构清晰可读。2. 项目结构与数据库脚本先看清分层和表关系再动手拿到手先别急着点运行把目录结构读一遍能省掉后面半数以上的报错排查时间。这个项目的分层方式在课程设计里非常典型control 管业务流程调度dao 管数据库操作po 放实体类view 放 Swing 界面main 是程序入口。没有引入框架每一层都看得见摸得着。2.1 src 目录的五个包谁负责干什么先看整体布局。一个标准 Eclipse 课设项目的 src 下通常长这样src/ ├── control/ # 业务控制层调 dao 完成具体功能 ├── dao/ # 数据访问层封装所有 JDBC 操作 ├── db/ # 数据库连接相关工具类 ├── po/ # 实体类和数据库表字段一一对应 ├── view/ # Swing 界面类 └── main/ # 主程序入口启动应用这六个目录各管一摊。po 包里是商品、用户、销售记录之类的实体类字段跟表列名对应dao 包接收 po 对象把它变成 INSERT、UPDATE、DELETE、SELECT 语句发到 MySQLcontrol 包把 view 传来的用户动作翻译成对 dao 的调用。这么分层的核心好处是换界面不动数据库代码换数据库不动界面代码。课程设计答辩时老师几乎必问「为什么把代码分成这几层」答案就在这个目录结构里。bin 目录是 Eclipse 编译后输出的 .class 文件.project 和 .classpath 是 Eclipse 的工程描述文件。这意味着你下载后可以直接用 Eclipse 导入File → Import → General → Existing Projects into Workspace选中解压后的目录构建路径会自动读取 .classpath 把 lib 里的驱动 jar 加进去。2.2 supermarket.sql 里通常有什么四张表的关系模型数据库脚本是这个项目能不能跑起来的前提。打开 supermarket.sql常见的表设计是四张核心表商品表、库存表、销售记录表、用户表。一个典型的关系模型如下表名主要字段说明goodsid, name, price, category商品基本信息价格通常用 DECIMAL(10,2)stockid, goods_id, quantity库存表通过 goods_id 关联商品salesid, goods_id, quantity, total_price, sale_date销售记录记录每笔交易usersid, username, password登录用户课设里一般明文存储外键关系上stock.goods_id 和 sales.goods_id 都指向 goods.id。销售记录表的 total_price 常见做法是由程序算好再写入不依赖数据库触发器——这样代码逻辑更直观答辩时也好解释。日期字段一般用 DATETIME 类型Java 侧对应 java.util.Date 或 java.sql.Timestamp。如果你的脚本里表名或字段跟上面不完全一样很正常——每个课设的命名习惯不同。关键是在改代码前先把脚本导入 MySQL用 DESC 命令看清楚每张表的字段名和类型再对应到 po 包里的实体类。我一般会先在命令行里把表结构导出来看一眼再决定要不要动代码这样能少走很多弯路。2.3 导入数据库两步踩通 MySQL 初始化把脚本灌进数据库有两种方式。第一种是用命令行最直接mysql -u root -p # 输入密码进入 MySQL 命令行后执行 source /your/path/supermarket.sql;第二种是用可视化工具Navicat 或 MySQL Workbench打开脚本文件直接运行。注意 source 后面要用绝对路径相对路径经常因为当前目录不对而报找不到文件。导入完成后用 SHOW TABLES; 确认四张表都建出来了。接着要做一件事检查数据库用户权限。如果项目里用 root 连接那没问题但很多课设代码里写的是专用账号比如用户名 supermarket、密码 123456。你必须在 MySQL 里确认这个账号存在且有权访问这个库CREATE USER IF NOT EXISTS supermarketlocalhost IDENTIFIED BY 123456; GRANT ALL PRIVILEGES ON supermarket.* TO supermarketlocalhost; FLUSH PRIVILEGES;这段操作要是漏了程序一启动就报 Access denied for user而且是在毫无提示的情况下你会误以为是连接代码写错了。账号权限问题不属于 Java 代码范畴最容易浪费时间。3. JDBC 连接与 DAO 层把商品表的增删改查跑通数据库就绪后真正的重头戏是 JDBC 连接和 DAO 封装。这个项目的技术栈是纯 JDBC没有连接池没有 ORM所有数据库操作都靠 DriverManager 拿连接。看完这一章你能明白为什么很多人说 JDBC 是 MyBatis 的「前任」——它就是一张把 SQL 翻译成 Java 方法调用的朴素实现。3.1 驱动加载与连接参数5.1.18 驱动的配置要点lib 目录里那个 mysql-connector-java-5.1.18-bin.jar 决定了连接参数的写法。5.1.x 版本的驱动加载方式是这样的package db; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String DRIVER com.mysql.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/supermarket?useSSLfalsecharacterEncodingutf8; private static final String USER root; private static final String PASSWORD your_password; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败请检查lib目录); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, java.sql.Statement stmt, java.sql.ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { } } } }这段代码有三个关键点。第一Class.forName(DRIVER) 在 5.1.18 时代是必须的虽然 Java 6 的 SPI 机制能自动加载驱动但课设项目惯用显式加载保留它没有任何坏处。第二URL 里的 useSSLfalse 是给老 MySQL 版本用的——如果 MySQL 服务端版本较新而这里不写 false部分环境会告警但不报错建议保留。第三characterEncodingutf8 很重要不写的话中文写入数据库大概率变问号。USER 和 PASSWORD 要改成你本机的真实账号。很多同学直接跑课设代码发现报 Access denied问题就出在这一行没改。这是这个项目里唯一需要根据你本机环境调整的地方。3.2 DAO 层封装BaseDAO 把重复代码收起来每次数据库操作都要经历「拿连接 → 建语句 → 执行 → 处理结果 → 关资源」这五步如果每个方法都从头写一遍代码会膨胀到没法看。课设里常见的做法是抽一个 BaseDAO把通用动作收拢package dao; import db.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class BaseDAO { protected Connection conn; protected PreparedStatement ps; protected ResultSet rs; protected PreparedStatement getPreparedStatement(String sql, Object... params) throws SQLException { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } return ps; } protected void closeAll() { DBUtil.close(conn, ps, rs); } }这里用了可变参数 Object... params 来接收 SQL 里的占位符值setObject 自动映射 Integer、String、Double 到对应的 JDBC 类型。这么做的好处是子类 DAO 写方法时只需关注 SQL 语句本身连接和资源释放都在父类处理。注意 closeAll() 必须在 finally 里调用否则连接泄漏。课设项目规模小连接泄漏一时半会儿看不出问题但如果你在循环里调 DAO 方法几十次之后就会连不上数据库。3.3 GoodsDAO把商品表的增删改查落到具体 SQL有了 BaseDAO具体模块的实现就很直白了。商品表是最典型的单表 CRUD看一个完整实现package dao; import po.Goods; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class GoodsDAO extends BaseDAO { // 查询全部商品 public ListGoods findAll() { ListGoods list new ArrayList(); try { String sql SELECT id, name, price, category FROM goods ORDER BY id; getPreparedStatement(sql); rs ps.executeQuery(); while (rs.next()) { Goods g new Goods(); g.setId(rs.getInt(id)); g.setName(rs.getString(name)); g.setPrice(rs.getBigDecimal(price)); g.setCategory(rs.getString(category)); list.add(g); } } catch (SQLException e) { e.printStackTrace(); } finally { closeAll(); } return list; } // 新增商品 public int add(Goods g) { int result 0; try { String sql INSERT INTO goods(name, price, category) VALUES(?, ?, ?); getPreparedStatement(sql, g.getName(), g.getPrice(), g.getCategory()); result ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { closeAll(); } return result; } // 按ID删除 public int deleteById(int id) { int result 0; try { String sql DELETE FROM goods WHERE id ?; getPreparedStatement(sql, id); result ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { closeAll(); } return result; } }三个细节值得说明。第一所有 SQL 都用了 PreparedStatement 的 ? 占位符而不是字符串拼接这是防 SQL 注入的基本功也是课设答辩时的高频考点——老师会问「为什么不用 Statement」答案就是防注入。第二executeQuery() 返回结果集executeUpdate() 返回受影响行数这两个方法别搞混SELECT 用前者增删改用后者。第三查询结果通过 rs.next() 逐行读取按列名取值比按下标取值更不容易出错当你修改了 SELECT 的列顺序按列名的代码不受影响。这套模式覆盖了商品模块的核心操作。销售模块的写法类似只是 SQL 换成关联查询比如统计某天销售额String sql SELECT SUM(total_price) FROM sales WHERE DATE(sale_date) ?;项目里 control 层会调用这些 DAO 方法把返回的 List 或 int 结果返回给 view 层去展示。整个调用链是界面按钮 → control 方法 → DAO 方法 → JDBC → MySQL反方向再一层层把结果带回来。4. view 层与 main 入口Swing 界面到按钮事件的完整链路DAO 层通了之后剩下的事情就是把数据展示到窗口上。这个项目的 view 包用的是 Java Swing——不是 JavaFX也不是 Web 前端。Swing 虽然丑但它是 JDK 自带的不需要额外依赖课程设计用它最省事。4.1 JFrame 主窗口一个类对应一个业务模块常见的布局是一个业务模块一个 JFrame。商品管理窗口大概长这样顶部一个 JTable 显示商品列表旁边几个文本框输入名称、价格、分类下方一排按钮「添加」「修改」「删除」「刷新」。每个按钮注册一个 ActionListener点击后调用 control 层的方法。关键代码是 JTable 的数据装载。JTable 本身不直接接受 List 需要先转成二维数组或 Vectorpackage view; import control.GoodsControl; import dao.GoodsDAO; import po.Goods; import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.util.List; public class GoodsFrame extends JFrame { private GoodsControl goodsControl new GoodsControl(); private JTable table; private DefaultTableModel tableModel; public GoodsFrame() { setTitle(商品管理); setSize(600, 400); setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); setLayout(new BorderLayout()); tableModel new DefaultTableModel(new String[]{ID, 名称, 价格, 分类}, 0); table new JTable(tableModel); add(new JScrollPane(table), BorderLayout.CENTER); JButton refreshBtn new JButton(刷新); refreshBtn.addActionListener(e - loadData()); add(refreshBtn, BorderLayout.SOUTH); loadData(); } private void loadData() { tableModel.setRowCount(0); // 清空旧数据 ListGoods list goodsControl.getAllGoods(); for (Goods g : list) { tableModel.addRow(new Object[]{ g.getId(), g.getName(), g.getPrice(), g.getCategory() }); } } }tableModel.setRowCount(0) 是刷新表格的关键——不清空的话每次 loadData 会在旧数据后面追加新行越点刷新数据越多。这是 Swing 开发里最常见的翻车点之一。JScrollPane 包 JTable 是为了表头固定和出滚动条不加的话数据多了窗口放不下。4.2 main 入口登录验证到主窗体的启动流程main 包是整个应用的门面。标准流程是启动登录窗口 → 校验用户名密码 → 成功后再打开主窗体。登录校验走 users 表package main; import control.UserControl; import view.LoginFrame; import view.MainFrame; import javax.swing.*; public class Main { public static void main(String[] args) { // 保证 Swing 界面在事件分发线程中创建 SwingUtilities.invokeLater(() - { LoginFrame login new LoginFrame(); login.setVisible(true); }); } }SwingUtilities.invokeLater 里面跑的是 Runnable。它保证界面创建在 EDT事件分发线程上避免多线程操作 Swing 组件导致的界面卡死或渲染异常。课设里不写这行程序也能跑但写上显得专业而且答辩被问到多线程时你可以拿这个当例子。LoginFrame 里的校验动作本质上是调用 DAO 查 users 表String sql SELECT COUNT(*) FROM users WHERE username ? AND password ?;查出来是 1 就登录成功0 就弹 JOptionPane 提示「用户名或密码错误」。整个控制流从按钮点击开始到 SQL 执行结束再回到界面弹窗。这就是 Swing 应用的完整链路。4.3 事件监听与界面刷新的坑数据改了表格不动的真相Swing 里改数据后界面不刷新是个历史悠久的坑。现象是你在文本框输入新商品名点「添加」数据库里确实多了记录用 Navicat 能看到但 JTable 上就是没反应。原因基本只有一个——添加成功后没有重新加载表格数据。正确写法是在 DAO 方法执行成功后立即调用 loadData() 方法重新查一遍private void addGoods() { String name nameField.getText().trim(); String price priceField.getText().trim(); // 在 control 层完成业务校验和 DAO 调用 boolean ok goodsControl.addGoods(name, price, categoryField.getText().trim()); if (ok) { JOptionPane.showMessageDialog(this, 添加成功); loadData(); // 关键这里必须重新加载 } else { JOptionPane.showMessageDialog(this, 添加失败请检查输入); } }另外一个玄学问题是窗口中文乱码。如果你在 Eclipse 里跑界面上的中文显示成方块去 Run Configurations 里给 VM 参数加上 -Dfile.encodingUTF-8 再试试。这一条在课设时经常能解决一批同学的界面乱码问题。5. 避坑清单五个最容易让人启动失败的问题我把这个项目从解压到跑通的整个过程中遇到过的坑集中写在这里。每一条都是真实发生的翻车记录按照「现象 → 原因 → 解决」的顺序你在自己机器上遇到时可以一条条对。5.1 ClassNotFoundException: com.mysql.jdbc.Driver现象一运行就抛 java.lang.ClassNotFoundException: com.mysql.jdbc.Driver程序在第一行就挂掉。原因lib 目录下的 mysql-connector-java-5.1.18-bin.jar 没有被加载到类路径里。在 Eclipse 里你还得右键项目 → Build Path → Configure Build Path → Libraries → Add JARs手动选中这个 jar。虽然 .classpath 文件里可能已经有记录了但导入工程时机器的 jar 路径变了Eclipse 找不到原路径就会失效。解决重新 Add JARs 一次然后 Project → Clean 强制重新编译。Clean 这个动作很关键Eclipse 经常在 jar 路径变更后不肯主动刷新编译产物导致你改了半天代码还是报同样的错。5.2 Communications link failure 或 Connection refused现象报 java.sql.SQLException: Communications link failure或者 ConnectException: Connection refused。驱动已经加载了但连不上 MySQL。原因MySQL 服务没启动或者端口不是默认的 3306或者 MySQL 只监听了本地 socket 没监听 TCP。连接 URL 里的 localhost:3306 与你真实环境不符。解决分两步查。第一步检查 MySQL 进程Linux/macOS 用 ps aux | grep mysqlWindows 打开服务管理器看 MySQL 服务是否显示「正在运行」。第二步命令行执行 mysql -u root -p 确认能进然后在 MySQL 里执行 SHOW VARIABLES LIKE port; 看端口号如果确实不是 3306把代码里的 URL 改成对应端口。几乎是玄学的高频解决方案其实是改 Host 为 127.0.0.1某些机器上 localhost 会被解析成 ::1IPv6MySQL 只监听 IPv4 就会连不上。5.3 中文乱码数据库表和连接参数双重保险现象插入中文商品名成功但查出来全是 ??。数据库里的数据本身就是问号不是界面显示问题。原因字符集不一致涉及两层。第一层是 JDBC 连接字符串没带 characterEncodingutf8第二层是 MySQL 表本身用的字符集不是 utf8。很多同学的 MySQL 默认 character_set_server 是 latin1建表时不指定就继承了 latin1。解决连接字符串保持 characterEncodingutf8 不变然后在 MySQL 里检查并修改表字符集ALTER TABLE goods CONVERT TO CHARACTER SET utf8mb4;utf8mb4 是 utf8 的超集能覆盖 emoji 和生僻字是当前 MySQL 的最佳实践比单纯 utf8 更稳妥。修改后把旧数据清掉重新插入问题就没了。5.4 Unknown column xxx in field list 但数据库里有这个字段现象SQL 里写了某个列名执行时报 Unknown column但你用 DESC 查表确实有这个列。原因最常见的是数据库脚本和你代码用的表名不是同一张表。比如代码里查的是 stock 表但 supermarket.sql 里建的表叫 inventory或者你导入的是另一份脚本旧库里残留了同名的表但结构不同。这个坑隐蔽在「项目自带脚本」和「你本地实际导入的数据」不是同一份。解决执行 SHOW CREATE TABLE goods; 把建表语句完整输出跟代码里的 SQL 逐列核对。凡是出现列对不上的情况以数据库里的实际结构为准改代码不要反过来改表——因为表里可能已有答辩演示用的数据。我习惯是拿到项目先跑一句 SHOW TABLES; 再跑 SHOW CREATE TABLE确认无误后才动代码。5.5 JTable 刷新后数据重复或空白现象点「刷新」按钮表格里要么数据翻倍要么空白。数据库里数据量没变单纯是界面的问题。原因翻倍是因为 loadData() 里没有先 setRowCount(0)直接在原模型上 addRow空白是因为你 new 了一个新的 DefaultTableModel 替换旧的但 JTable 还引用着老模型没有调用 table.setModel(新模型)。前者是清空没做后者是模型替换了没通知组件。解决用同一个 tableModel 实例刷新时先 setRowCount(0) 再逐行添加。如果你确实要换模型最后必须手动 table.setModel(newModel)。这个坑在几乎所有 Swing 课设的「刷新」按钮上都会出现属于经典中的经典。6. 交差之前验证和升级的正确顺序课设项目「能跑」和「能答辩」是两回事。我在做过十几个类似项目的评审之后给你一个交差之前强制走一遍的验证顺序代码重新编译 → 数据库重新导入 → 核心功能走通 → 异常路径走一遍 → 写 README。这一步做完你的项目才算真正闭环。先验证代码的干净程度。在 Eclipse 里执行 Project → Clean然后 Run As → Java Application。注意观察 Console 有没有红色异常哪怕功能看起来正常后台报过 SQL 异常也必须在答辩前处理掉。课设老师很爱做的一件事就是「删除一条不存在的记录」如果代码里没有对 executeUpdate() 返回值做判断这条操作可能抛异常或者显示成功的假象。让 deleteById 返回影响行数为 0 时在 control 层返回「记录不存在」这个小细节非常加分。再验证数据库的完整性。重新执行一次 supermarket.sql确认脚本可以一键从头建库然后跑一遍核心链路登录 → 添加商品 → 修改价格 → 添加销售记录 → 查询销售额。每一步都要去看数据库里的实际数据不要只盯着界面上的提示。如果这个流程里有任一步报错回到第 5 章的排查清单去对。升级方向上最实用的是把单连接换成连接池。课设里每次操作都 DriverManager.getConnection频繁开关连接在低并发下没感觉但这就是 MyBatis 内置连接池想解决的问题。你可以用 Druid 或 HikariCP 替换 DBUtil改造量不大核心是初始化一个数据源HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/supermarket?useSSLfalsecharacterEncodingutf8); config.setUsername(root); config.setPassword(your_password); config.setMaximumPoolSize(10); HikariDataSource dataSource new HikariDataSource(config);把 DBUtil.getConnection() 改成 dataSource.getConnection()其他 DAO 代码一行都不用动。这个改造在答辩时可以说「引入了连接池避免频繁建连的开销」一句话就能体现出你已经超出了课设要求。第二个值得做的升级是给关键 DAO 方法加事务。比如「销售入库」这个动作——写 sales 记录的同时要减 stock 库存。如果第一步成功、第二步失败数据就乱了。用事务包起来的写法是conn DBUtil.getConnection(); conn.setAutoCommit(false); try { // 执行插入销售记录 SQL // 执行更新库存 SQL conn.commit(); } catch (SQLException e) { conn.rollback(); e.printStackTrace(); }数据一致性这个点本身就是 Java 面试的高频话题你在课设里能主动谈起 setAutoCommit(false)、commit、rollback 这三个词比背八股文有效得多。尤其是多表操作时保证数据一致性这种实践经验在真实项目中直接对应面试题「你怎么保证数据一致性」的现场回答。从那以后我每次拿到一个课设包都强制走一遍「先看表结构 → 再改连接参数 → 再跑核心业务链路 → 最后看后台有没有隐藏异常」这个流程能避免九成以上的翻车。这个项目虽然界面朴素但分层清楚、JDBC 基本功扎实把它吃透比抄一个 Spring Boot 大项目学到的东西更多。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网