ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java课设超市管理系统:从JDBC到事务控制的完整实践

2026/9/16 1:46:37 拓冰建站 浏览量
Java课设超市管理系统:从JDBC到事务控制的完整实践 简介面向Java课程设计的超市管理系统项目适合初学Java与数据库整合开发的在校生完成课设或进阶练习。项目基于Java与MySQL通过命令行交互模拟超市后台管理聚合JDBC数据库连接、SQL增删改查、异常处理、多线程并发、文件读写等核心知识点并可按需扩展工厂模式、单例模式、日志记录与JUnit单元测试等工程实践。压缩包共49个文件包括22个java源码及对应class文件、mysql-connector-java-5.1.18-bin.jar驱动包、SQL脚本和Eclipse工程配置整体约783KB源码按control、dao、po、db、view等包分层便于阅读与二次开发。已有1378人学习下载。通过学习该项目可以理清超市商品、库存、销售记录的后端管理流程掌握JDBC连接MySQL的完整操作步骤并借鉴无界面场景下的逻辑分层与数据持久化设计对巩固Java基础和数据库编程能力有切实帮助。1. 纯控制台的超市管理系统反而最能看清后端功底Java课程设计里最容易被低估的就是这类超市管理系统——没有前端页面、没有框架、没有 Spring Boot界面就是黑窗口里的一行行输入提示。但恰恰是这种项目把 Java 基础、JDBC、SQL、事务和分层思想完整串了一遍。它解决的是超市场景里的核心业务闭环商品建档、库存维护、进货入库、收银扣减库存、销售流水记录。对于正在走 Java 学习路线的人来说把它拆透比照着视频敲三个小时业务代码收获更大。这套 supermarket 源码包用的是典型的三层结构view 做交互、dao 做数据访问、control 做流程控制数据库脚本、JDBC 驱动和完整工程文件都齐了。上手前先搞清楚一个问题为什么课设不用 ORM 框架因为裸 JDBC 才能让你看清数据库连接从创建到释放的每一个环节这正是面试八股文里高频追问的部分。2. supermarket 项目拆包control、dao、po、db 四层职责与表结构设计2.1 项目目录结构与分包逻辑解压 supermarket.zip 之后先看目录不要急着运行。工程是 Eclipse 标准结构.classpath和.project文件保证了导入 IDE 后可以直接编译lib目录下放着mysql-connector-java-5.1.18-bin.jar这是 MySQL 5.x 时代的经典驱动包。源码集中在src下包名已经把分层意图写在名字里了包名职责典型类po实体类与数据库表字段一一对应Product、SaleRecorddb数据库连接与资源释放DBUtildao数据访问层封装所有 SQL 操作ProductDao、SaleDaocontrol业务逻辑与流程控制SaleController、StockControllerview控制台交互界面菜单打印与输入接收MainViewmain程序入口Mainbin目录是编译输出如果从 IDE 里运行时遇到类找不到的错误先检查这里是不是旧文件。直接看src下的源码即可。分层的核心逻辑是view 不写 SQLdao 不打印菜单control 负责把两者串起来。很多初学者把 JDBC 代码直接写在 main 方法里项目一复杂就完全没法维护这个课设的包结构本身就是规范示范。2.2 从 supermarket.sql 看核心表设计supermarket.sql是初始化脚本在 MySQL 里执行后会自动建库建表并插入基础测试数据。课程设计级别的表设计一般不会引入外键约束而是通过业务层保证数据一致性原因有两个一是控制台程序对异常处理的演示要求更高外键约束会让插入顺序变得死板二是课设答辩时评委更关注你对 SQL 语义的理解而非数据库端的约束配置。典型表结构如下字段命名风格与代码里的 PO 保持一致CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARSET utf8mb4; USE supermarket; CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, barcode VARCHAR(32) NOT NULL UNIQUE COMMENT 商品条码, name VARCHAR(64) NOT NULL COMMENT 商品名称, spec VARCHAR(64) COMMENT 规格如500ml/瓶, price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sale_record ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL COMMENT 关联product.id, quantity INT NOT NULL COMMENT 销售数量, total_amount DECIMAL(10,2) NOT NULL COMMENT 单笔小计, sale_time DATETIME NOT NULL COMMENT 销售时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;price和total_amount都选DECIMAL(10,2)而不是FLOAT这是对账务类数据的基本尊重。二进制浮点数在累加时会产生精度漂移虽然在课设数据量上看不出来但如果哪天你想把这个项目往简历上写这一处设计就能在面试时讲出深度。如果不执行 sql 脚本而是在 IDE 里手工建表编码务必选utf8mb4否则存生僻字或表情符号会报Incorrect string value错误这是 MySQL 中文乱码最常见的坑。2.3 PO 实体类与表字段的映射关系po包下的类不需要任何继承关系就是纯数据载体。字段类型与表字段对应数据库的INT映射IntegerDECIMAL映射BigDecimalDATETIME映射java.util.Date或LocalDateTime。这里最容易犯的错是用double接收price后面做金额累加时会吃大亏。public class Product { private Integer id; private String barcode; private String name; private String spec; private BigDecimal price; private Integer stock; public Product() { } public Product(String barcode, String name, String spec, BigDecimal price, Integer stock) { this.barcode barcode; this.name name; this.spec spec; this.price price; this.stock stock; } // 每个字段提供 getter/setter此处省略 }实体类必须保留无参构造器因为 DAO 层要用Class.newInstance()或反射做结果集映射时依赖它这也是 Java 基础中反射机制的典型应用场景。有参构造器只是方便业务代码里 new 对象不是必须的但没有无参构造器在很多框架和工具类里会直接报InstantiationException。如果字段很多手写 getter/setter 容易漏Eclipse 里右键选择 Generate Getters and Setters 可以一次性生成课设阶段用 IDE 自动生成即可不强制引入 Lombok 省这几行代码。3. JDBC 连接封装与 DAO 层 CRUD 实现3.1 用 DBUtil 统一管理连接和资源释放数据库连接是昂贵资源每次操作都新建连接、用完不关程序跑几分钟就会报Too many connections。db包里这个类的职责就是三件事加载驱动、获取连接、释放资源。网上很多教程把Class.forName写在每次获取连接的代码里这是没有必要的重复操作驱动只需要加载一次就够了。package db; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8useSSLfalse; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.jdbc.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, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }URL 里的useSSLfalse是 MySQL 5.7 之后必须加的参数不加会看到WARN: Establishing SSL connection without servers identity verification is not recommended的警告虽然课设里不报错但每次连接都多一次 SSL 握手控制台程序会明显变慢。close方法忽略异常而不是抛出因为资源关闭阶段的异常对业务逻辑没有任何影响这个设计思路在 Java 面试中经常被问到属于典型的经验型考点。驱动类com.mysql.jdbc.Driver对应的是 5.x 驱动包如果你换成mysql-connector-java-8.x的 jar这个类名要改成com.mysql.cj.jdbc.Driver同时 URL 里还要追加serverTimezoneAsia/Shanghai。3.2 商品管理的增删改查实现DAO 层是最能体现 SQL 功底的区域。以商品管理为例一个完整的 DAO 至少要提供按条码查询、模糊搜索、插入新品、修改价格、更新库存、删除商品。每个方法内部都是四步走拿连接、预编译 SQL、绑定参数、执行并映射结果。package dao; import po.Product; import db.DBUtil; import java.math.BigDecimal; import java.sql.*; import java.util.ArrayList; import java.util.List; public class ProductDao { public Product findByBarcode(String barcode) { String sql SELECT id, barcode, name, spec, price, stock FROM product WHERE barcode ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, barcode); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return mapRow(rs); } } } catch (SQLException e) { e.printStackTrace(); } return null; } public ListProduct searchByKeyword(String keyword) { String sql SELECT id, barcode, name, spec, price, stock FROM product WHERE name LIKE ?; ListProduct list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } } catch (SQLException e) { e.printStackTrace(); } return list; } public int updatePrice(Integer id, BigDecimal newPrice) { String sql UPDATE product SET price ? WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setBigDecimal(1, newPrice); ps.setInt(2, id); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } private Product mapRow(ResultSet rs) throws SQLException { Product p new Product(); p.setId(rs.getInt(id)); p.setBarcode(rs.getString(barcode)); p.setName(rs.getString(name)); p.setSpec(rs.getString(spec)); p.setPrice(rs.getBigDecimal(price)); p.setStock(rs.getInt(stock)); return p; } }Java 7 的 try-with-resources 语法让连接和语句自动关闭代码里不再出现 finally 块。但要注意如果事务跨越多个 DAO 方法不能用这种写法因为 try-with-resources 会在方法结束时把连接也关掉事务就断了事务场景的写法会在第四章专门展开。mapRow方法把 ResultSet 的行数据映射成 Product 对象抽取出来是为了避免每个方法里都写一遍rs.getString(name)这是 DAO 层最常见的重构点。executeUpdate返回受影响行数调用方可以据此判断更新是否真正生效。3.3 PreparedStatement 与 SQL 注入边界用字符串拼接 SQL 是课设作品里最高频的低级错误。SELECT * FROM product WHERE barcode barcode 这种写法在功能上没问题但一旦 barcode 来自用户输入注入风险就出现了。PreparedStatement 的预编译机制把 SQL 结构和参数分离参数值经过转义后才进入执行计划从根本上杜绝了拼接注入。另一个被忽略的优势是执行计划复用——同一个 PreparedStatement 对象重复执行时MySQL 服务端不需要重新解析 SQL性能优于 Statement。在控制台程序里这个差距不明显但在 DAO 层养成用?占位符的习惯以后写 MyBatis 的#{}时理解会顺畅得多这两个东西在防注入的原理上是完全一致的。JDBC 里还有setObject方法可以替代各种类型的 setter但类型转换的隐式开销和潜在的类型错配会掩盖代码意图显式调用setString、setInt、setBigDecimal更利于答辩时讲清楚每一行参数的行为。4. 销售流程里的事务边界与并发扣库存4.1 一次结账动作应该包住哪些 SQL单笔销售从业务上看只有两步向sale_record插入一条销售记录扣减product表的库存。但这两步不能拆开执行。如果在第一步成功后、第二步执行前程序崩溃就会出现卖了货但库存没减的数据不一致。这就是必须引入事务的典型场景。public void checkout(String barcode, int quantity) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); Product product productDao.findByBarcodeWithConn(conn, barcode); if (product null) { throw new RuntimeException(商品不存在: barcode); } if (product.getStock() quantity) { throw new RuntimeException(库存不足当前库存: product.getStock()); } saleDao.insertSaleRecord(conn, product.getId(), quantity, product.getPrice().multiply(BigDecimal.valueOf(quantity))); productDao.deductStockWithConn(conn, product.getId(), quantity); conn.commit(); } catch (Exception e) { try { if (conn ! null) { conn.rollback(); } } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException(结账失败: e.getMessage(), e); } finally { try { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }注意这里的所有 DAO 方法都多传了一个Connection参数这和第三章里的写法不同。事务必须保证多个 SQL 用同一个连接执行如果每个 DAO 方法内部都自己getConnection()连接各开各的事务就失控了。这是从课设代码通往生产级代码的第一个门槛。commit放在最后rollback放在 catch 里finally 中把连接归还前要恢复setAutoCommit(true)因为连接池复用时如果 autoCommit 状态是 false下一次借出就会出诡异的事务不生效问题。4.2 事务隔离级别与回滚时机MySQL InnoDB 默认的隔离级别是REPEATABLE_READ在这个级别下两个并发事务同时扣库存会面临另一个问题先看代码再分析。上面deductStockWithConn的实现是核心public int deductStockWithConn(Connection conn, Integer productId, Integer quantity) throws SQLException { String sql UPDATE product SET stock stock - ? WHERE id ? AND stock ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, quantity); ps.setInt(2, productId); ps.setInt(3, quantity); return ps.executeUpdate(); } }WHERE stock ?这个条件加上executeUpdate的返回值构成了一个轻量级的乐观锁保护。两个线程同时读库发现库存为 10都执行扣 8 的操作第一个线程的 UPDATE 执行成功后库存变为 2第二个线程执行 UPDATE 时stock 8判定失败返回 0。业务层只需检查返回值等于 0 就提示库存不足。这种做法把并发控制下沉到了 SQL 层而不是靠 Java 的synchronized或Lock因为 JVM 锁只在单机单进程生效如果未来拆成微服务锁就完全失效。库存字段在 UPDATE 语句里被行锁锁定这也是为什么不在 Java 里算好新库存再 UPDATE——stock 新值在并发时会发生丢失更新而stock stock - ?是原子操作。4.3 异常处理捕获什么、抛出什么这个项目里的异常处理策略很典型DAO 层捕获SQLException后打印堆栈并返回默认值control 层抛出业务异常RuntimeException统一处理。很多课设作品喜欢在 DAO 层 catch 之后返回 null 或 0然后在业务层判断 null 再提示操作失败这种写法最大的问题是把 SQL 异常吞掉了线上排查时根本不知道是 SQL 写错还是数据库连不上。正确姿势是SQLException属于受检异常在 DAO 层可以转换成RuntimeException继续向上抛由最外层的 view 或 main 统一捕获并输出用户可读的提示。连接失败、驱动缺失这类环境异常要在第一时间暴露而不是转换成 null 让程序带病运行。日志记录在课设阶段用e.printStackTrace()够用但如果你想用 Log4j 2 替换只需要在db包里加一个静态 Logger把每个 catch 块里的堆栈输出换成logger.error(扣减库存失败, productId{}, productId, e)这是 SLF4J 占位符式日志的标准写法变量直接拼在字符串里是反面教材。4.4 多线程模拟并发结账的验证方式课设一般不要求写并发代码但你可以用这个项目验证自己写的库存保护到底靠不靠谱。写一个简单的多线程测试类开 20 个线程同时抢购同一个商品每个线程购买 1 件初始库存设为 10最后查数据库看库存是否为 0并且sale_record里恰好 10 条记录。如果结果出现库存为负或者记录数对不上说明事务边界或 SQL 写法有漏洞。Java 里模拟并发用ExecutorService加CountDownLatch即可注意线程启动的同时性要用 latch 控制否则线程逐个启动压力根本打不上去。5. 从课设到简历项目排错清单与分层演进5.1 环境与部署高频排错用 Eclipse 导入这个项目时最常遇到的问题有三个。第一个是驱动包没有真正进入 classpath——把 jar 放进lib目录后必须右键项目选择 Build Path、Add Jars把mysql-connector-java-5.1.18-bin.jar加入构建路径只看它在磁盘上存在是不够的。第二个是数据库连接报Access denied for userDBUtil里的用户名密码改成你自己 MySQL 的账号注意 MySQL 8 默认用caching_sha2_password认证老驱动不兼容会报Unable to load authentication plugin解决办法是换 8.x 驱动包或者执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 密码;。第三个是控制台输出中文乱码右键 Run Configurations、Common 面板里把 Console Encoding 改成 UTF-8同时保证数据库连接 URL 里的characterEncodingutf8与实际表编码一致。supermarket.sql文件用 NotePad 打开时确认编码不是 GBK否则注释里的中文导入后全是乱码。5.2 把控制台 view 层升级成图形界面的三个步骤你要是想把这个项目写到简历里给它加一个 Swing 界面是投入产出比最高的升级。第一步在view包里新建LoginFrame和MainFrame把MainView里的菜单逻辑改成按钮事件product表做成JTable的数据源。第二步把 DAO 层返回值适配成TableModel——ProductDao.searchByKeyword返回的ListProduct转成DefaultTableModel这里会用到Vector或二维数组正好复习一下集合和数组转换。第三步把control层暴露成服务接口SaleController.checkout原本接收控制台参数改成接收一个SaleRequest对象参数校验放在 controller 内部view 层只负责传对象这一步做完后你的项目结构就已经接近 Spring Boot 的 Controller-Service-DAO 三层模型了面试时可以直接映射着讲。不到 500 行代码改动就能让项目从黑窗口课设变成桌面管理系统。5.3 连接池与 ORM 的引入时机课程设计里用裸 JDBC 是正确的但如果你决定深入学习下一步自然是把DBUtil.getConnection()换成连接池比如HikariCP或Druid。换的时候注意连接池拿到的是PooledConnection的代理对象conn.close()不再真正关闭物理连接而是归还给池子所以 DAO 层现有的资源释放代码不需要大改。再往后是 MyBatis你需要把 DAO 接口改成Select注解或者 XML 映射文件以前手写的mapRow方法就由 MyBatis 的结果映射替代了。这个演进路径对应着 Java 学习路线上从基础到框架的自然过渡也是面试中请你讲一下 JDBC 到 MyBatis 的演进这类问题的答案素材。最后可以自己写一个Class.forName验证类用DriverManager.getDriver反射打印驱动版本顺便把 Java 动态代理在事务控制上的应用看一遍——很多工厂里优秀开源框架的事务管理器就是基于动态代理实现的。本文还有配套的精品资源点击获取