简介:这是一份面向Java初学者与课程设计学习者的超市管理系统后端项目源码,基于Java语言与MySQL数据库实现,重点模拟商品信息、库存与销售记录等后台管理逻辑,适合正在完成课设或希望巩固JDBC与数据库编程的开发者参考。压缩包共49个文件,约783KB,包含22个java源文件与22个class编译文件,另有mysql-connector-java驱动jar包、sql建表脚本及Eclipse工程配置文件,目录按control、dao、po、db、view、main等分层组织,结构清晰。项目覆盖Java面向对象基础、JDBC连接与结果集处理、SQL增删改查、异常处理、多线程并发、文件读写、设计模式与日志记录等核心知识点,虽无图形前端,但后端业务逻辑完整,便于读者理解分层架构与数据持久化思路。目前已有1379人学习,适合作为课设模板或数据库编程练习素材。
1. 从一份超市课设源码说起:Java+MySQL 的后端骨架到底长什么样
很多同学做 Java 课程设计时,第一反应是去网上找一份能跑的源码,但真正打开压缩包之后往往一脸懵——目录里一堆control、dao、po、db、view包,不知道从哪看起,更不知道这份代码能不能直接跑起来。我这次拆的是一份典型的JAVA课设-超市管理系统,技术栈就是Java + MySQL,用 JDBC 直连数据库,没有 Spring 那一套框架,也没有前端页面,交互方式以控制台为主。它解决的核心问题是:用最朴素的 Java 分层结构,把商品信息、库存、销售记录这些超市后台数据管起来,让你能完整看到「一个业务请求从控制台输入到 SQL 落库」的全链路。
这份资源适合两类人:一类是正在赶课程设计、需要一份能跑通、能改、能讲清楚原理的参考实现;另一类是想回头补 JDBC 和 DAO 分层基本功的初级开发者。它不适合想直接拿去做生产系统的人——没有事务封装、没有连接池、没有权限体系,这些边界后面会逐条讲清楚。下面我按「结构拆解 → 环境搭建 → 核心链路 → 避坑 → 进阶改造」的顺序,把这份源码从能跑到能改讲透。
2. 拆开 supermarket.zip:包结构与分层职责逐层对齐
2.1 目录清单与每个包的真实职责
先把压缩包解开,顶层能看到src、bin、lib、.settings、.project、.classpath这些 Eclipse 工程标配文件,以及一个supermarket.sql脚本。真正要读的是src下面的六个包,它们构成了这套系统的全部逻辑:
| 包名 | 职责 | 典型类 |
|---|---|---|
po | 持久化对象,和数据库表一一对应 | Product、Sale、User |
dao | 数据访问层,封装所有 SQL 操作 | ProductDao、SaleDao |
db | 数据库连接管理,负责加载驱动、拿 Connection | DBUtil或DBHelper |
control | 业务控制层,编排 dao 完成一个完整业务 | ProductControl |
view | 控制台菜单与输入输出 | MainView、LoginView |
main | 程序入口,启动主循环 | Main |
lib目录里放的是mysql-connector-java-5.1.18-bin.jar,这是整个项目唯一的外部依赖。.classpath文件里会把它登记为库引用,如果你换 IDE 打开,需要重新确认这个 jar 有没有被正确加入 Build Path。
po包里的类通常只有私有字段加 getter/setter,字段名和supermarket.sql里的列名对应。比如商品表如果有id、name、price、stock四列,那Product类里就是这四个属性。这种一一对应的写法虽然笨,但对课设来说最直观——你打开任意一个 po 类,就能反推出数据库表结构。
dao包是重点。每个 dao 类里会有一组方法,命名通常是addXxx、deleteXxx、updateXxx、findAll、findById。方法内部就是标准的 JDBC 四步:拿连接、建 PreparedStatement、执行、处理 ResultSet。db包里的工具类被所有 dao 复用,它一般提供一个静态方法getConnection(),内部用DriverManager.getConnection(url, user, password)返回连接。
control包把多个 dao 调用串成一个业务动作。比如「上架一个新商品」这个操作,control 层会先校验输入,再调ProductDao.add(),最后返回结果给 view 层。view包只负责打印菜单、读Scanner输入、把结果格式化输出,不碰任何 SQL。main包里的入口类只做一件事:new 一个 view,进入 while 循环,直到用户选退出。
2.2 为什么课设要用这种「土」分层
很多人会问:都 2025 年了,为什么不用 Spring Boot + MyBatis?答案很直接——课设的评分点往往就是「你有没有理解分层」和「你能不能手写 JDBC」。用框架当然快,但框架把 JDBC 的细节全藏起来了,答辩时老师问一句「Connection 什么时候关闭」你就答不上来。
这套分层虽然土,但它把三个关键边界划得很清楚:po 只管数据形状,dao 只管 SQL,control 只管业务编排。你改任何一个包,只要接口不变,其他包不用动。比如要把 MySQL 换成 SQL Server,理论上只需要改db包里的连接串和驱动类名,dao 层的 SQL 如果没用到 MySQL 特有语法,几乎不用动。这种可替换性就是分层带来的实际收益,也是课设想训练的核心能力。
另一个现实原因是依赖极简。整个项目只有一个 jar,不需要 Maven 拉一堆依赖,不需要配 application.yml,拷贝到任何装了 JDK 和 MySQL 的机器上,改一下数据库密码就能跑。对于要在机房演示、或者要交给老师检查的课设来说,这种「零配置负担」反而是优势。
提示:如果你打算把这份代码改成 Maven 工程,注意
mysql-connector-java-5.1.18这个版本对应的是旧版驱动类com.mysql.jdbc.Driver,新版驱动类是com.mysql.cj.jdbc.Driver,两者不能混用,否则会报ClassNotFoundException。
3. 把项目跑起来:JDK、MySQL 与驱动版本的三方对齐
3.1 环境准备与数据库导入
先确认本机环境。这份代码用的是 JDBC 4.0 之前的写法,Class.forName("com.mysql.jdbc.Driver")这种显式加载驱动的代码大概率还在db包里。JDK 建议用 8 或 11,不要用 17 以上——旧版驱动在高版本 JDK 上可能因为模块化限制出问题。MySQL 用 5.7 最稳,8.0 也能跑,但要注意时区和认证插件差异。
第一步,导入数据库。打开命令行,用 root 登录 MySQL,执行压缩包里的supermarket.sql:
mysql -u root -p < supermarket.sql这条命令会把脚本里的CREATE DATABASE、CREATE TABLE、INSERT全部执行一遍。执行完之后用SHOW DATABASES;确认库名(通常是supermarket),再USE supermarket; SHOW TABLES;看表有没有建全。如果脚本里没有CREATE DATABASE语句,你需要先手动建库再导入。
第二步,改连接配置。打开db包下的工具类,找到类似下面的代码:
private static final String URL = "jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456";把USER和PASSWORD改成你本机的 MySQL 账号密码。URL里的库名要和上一步建好的库一致。characterEncoding=utf8这个参数别删,否则中文商品名会变乱码。
第三步,在 IDE 里把lib/mysql-connector-java-5.1.18-bin.jar加入 Build Path。Eclipse 里右键项目 → Build Path → Configure Build Path → Libraries → Add JARs,选中这个 jar。如果你用 IDEA,在 Project Structure → Modules → Dependencies 里加。加完之后,db包里Class.forName那一行才不会抛异常。
3.2 驱动加载与连接获取的代码逻辑
db包里的工具类是整条链路的起点,它的典型实现长这样:
public class DBUtil { private static final String DRIVER = "com.mysql.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); // 显式加载驱动,JDBC 4.0 之后可省略,但旧项目保留更稳 } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动未找到,检查 lib 是否加入 Build Path", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); // 每次调用返回新连接 } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 关闭顺序:ResultSet -> Statement -> Connection,每个都判空 try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }static块在类第一次被加载时执行一次,负责把驱动类注册到DriverManager。getConnection()每次调用都新建一个物理连接,用完之后由close()方法释放。这里没有连接池,所以每次数据库操作都会经历「建立 TCP 连接 → 认证 → 执行 → 关闭」的完整过程,性能不高,但课设场景下并发量极低,够用。
close()方法的参数顺序和关闭顺序是反的——传参时习惯按conn, stmt, rs传,关闭时先关rs再关stmt最后关conn。这个顺序不能乱,因为Statement关闭时可能还需要ResultSet的元数据,先关rs更安全。每个close都单独 try-catch,是为了保证一个关闭失败不影响后面的关闭。
注意:如果你在 MySQL 8.0 上跑,
com.mysql.jdbc.Driver会提示 deprecated,但通常还能用。如果直接报ClassNotFoundException,把驱动类改成com.mysql.cj.jdbc.Driver,同时 URL 后面加上&serverTimezone=Asia/Shanghai,否则会报时区错误。
4. 核心链路走查:从控制台输入到 SQL 落库的完整闭环
4.1 以「添加商品」为例走一遍全流程
打开view包里的主菜单类,找到「添加商品」对应的分支。用户输入商品名、价格、库存之后,view 层会把这些数据打包成一个Product对象,然后调用control层的方法。control 层拿到对象后,先做基本校验(比如价格不能为负、名称不能为空),校验通过再调ProductDao.add(product)。
dao 层的add方法是最典型的 JDBC 写操作:
public boolean add(Product product) { String sql = "INSERT INTO product(name, price, stock) VALUES(?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, product.getName()); // 第一个占位符对应商品名 ps.setDouble(2, product.getPrice()); // 第二个对应价格 ps.setInt(3, product.getStock()); // 第三个对应库存 return ps.executeUpdate() > 0; // 影响行数大于 0 表示插入成功 } catch (SQLException e) { e.printStackTrace(); return false; } }这里用了 try-with-resources,Connection和PreparedStatement会在代码块结束时自动关闭,不需要手动调DBUtil.close()。PreparedStatement用?占位符而不是字符串拼接,这是防 SQL 注入的关键——用户输入的商品名里就算带单引号,也只会被当成普通字符,不会改变 SQL 结构。
executeUpdate()返回的是受影响的行数。插入成功返回 1,失败返回 0 或抛异常。dao 层把布尔结果返回给 control,control 再返回给 view,view 根据结果打印「添加成功」或「添加失败」。整条链路里,每一层只关心自己那一层的返回值,不越界处理。
4.2 查询、更新、删除的写法差异
查询操作和写操作的区别在于要处理ResultSet。以findAll为例:
public List<Product> findAll() { List<Product> list = new ArrayList<>(); String sql = "SELECT id, name, price, stock FROM product"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { // 游标逐行下移,每行组装一个 Product Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setPrice(rs.getDouble("price")); p.setStock(rs.getInt("stock")); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } return list; }rs.next()返回 false 时循环结束,说明结果集读完了。取值时用列名而不是列索引,可读性更好,也不怕 SQL 里列顺序调整。getInt、getString、getDouble这些方法要和数据库列类型匹配,如果列是DECIMAL而你用getDouble,一般能自动转换,但精度可能有细微差异。
更新和删除的写法和插入几乎一样,只是 SQL 不同。更新通常是UPDATE product SET name=?, price=?, stock=? WHERE id=?,删除是DELETE FROM product WHERE id=?。注意WHERE条件一定要带,否则会全表更新或全表删除——这是课设里最容易翻车的地方之一。
4.3 控制台菜单的循环与退出逻辑
main包里的入口类通常长这样:
public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); MainView view = new MainView(); while (true) { view.showMenu(); // 打印菜单选项 int choice = scanner.nextInt(); // 读取用户选择 if (choice == 0) { System.out.println("已退出系统"); break; // 选 0 跳出循环,程序结束 } view.handle(choice); // 根据选择分发到对应业务 } scanner.close(); } }这个 while 循环是整个系统的「心跳」,每次循环打印一次菜单、读一次输入、处理一次业务。scanner.nextInt()只读整数,如果用户输入了非数字字符会抛InputMismatchException,导致程序崩溃。健壮一点的做法是在读之前先hasNextInt()判断,或者用nextLine()读字符串再手动解析。
view.handle(choice)内部是一个 switch 或 if-else 链,把不同的数字映射到不同的 control 方法。这种「菜单驱动」的交互方式虽然原始,但逻辑清晰,适合课设演示。如果你要加新功能,只需要在菜单里加一个选项、在 handle 里加一个分支、在 control 里加一个方法,三层各改一处,不会互相干扰。
5. 避坑与排查:这份课设源码最容易翻车的五个地方
5.1 中文乱码:现象是商品名显示问号,原因是字符集没对齐
现象:插入「可乐」之后,查询出来显示成??或者乱码方块。
原因:三个环节的字符集可能不一致——MySQL 建库时用了latin1、JDBC URL 里没写characterEncoding、控制台本身的编码不是 UTF-8。只要有一处不对,中文就会丢。
解决:先确认数据库和表的字符集是utf8mb4,用SHOW CREATE TABLE product;看。然后确认 JDBC URL 里有useUnicode=true&characterEncoding=utf8。最后确认 IDE 的 console 编码是 UTF-8(Eclipse 在 Run Configuration → Common → Encoding 里改)。三处都对齐之后,删掉旧数据重新插入。
5.2 驱动加载失败:现象是启动就抛 ClassNotFoundException
现象:程序一运行就报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。
原因:mysql-connector-java-5.1.18-bin.jar没有被加入 Build Path,或者 jar 文件损坏,或者驱动类名写错了。
解决:在 IDE 里检查 Build Path 的 Libraries 列表里有没有这个 jar。如果没有,重新 Add JARs。如果有但还是报错,展开 jar 看里面有没有com/mysql/jdbc/Driver.class这个文件。如果用的是 MySQL 8.0 且 jar 是 8.x 版本,驱动类名要改成com.mysql.cj.jdbc.Driver。
5.3 连接被拒绝:现象是 CommunicationsException,原因是 MySQL 没启动或端口不对
现象:Communications link failure或者Connection refused。
原因:MySQL 服务没启动、端口不是默认的 3306、或者防火墙拦了本地连接。也有可能是 URL 里的主机名写成了别的机器名。
解决:命令行执行mysql -u root -p看能不能登录。能登录说明服务正常,问题在 Java 侧的 URL 或端口。检查 URL 里的localhost:3306是否和你实际的一致。如果 MySQL 改过端口,URL 里也要改。Windows 下用net start mysql启动服务,Linux 下用systemctl start mysqld。
5.4 空指针异常:现象是查询列表时抛 NPE,原因是 ResultSet 或对象没判空
现象:调用findAll之后遍历列表时抛NullPointerException。
原因:findAll返回了 null 而不是空列表,或者某一行某个字段在数据库里是 NULL,rs.getString返回 null 之后直接调用了它的方法。
解决:dao 层的查询方法统一返回空列表而不是 null,初始化时就new ArrayList<>()。对于可能为 NULL 的列,用rs.wasNull()判断,或者在 po 类里给字段设默认值。view 层遍历之前先判list != null && !list.isEmpty()。
5.5 资源未关闭:现象是跑久了报 Too many connections
现象:反复操作几十次之后,MySQL 报Too many connections,程序卡死。
原因:每次getConnection()都新建连接,但某些分支里忘了关闭,连接泄漏累积到 MySQL 的最大连接数上限。
解决:把所有 JDBC 操作改成 try-with-resources 写法,让编译器保证关闭。如果代码里还有手动DBUtil.getConnection()的地方,逐个检查有没有对应的close()调用。课设场景下把 MySQL 的max_connections调大只能缓解,根治还是要靠正确关闭。
6. 进阶改造:把这份课设变成能写进简历的项目
6.1 用 DAO 接口 + 工厂模式解耦
现在的 dao 是具体类直接 new,control 层和 dao 实现绑死了。想换成 MySQL 之外的存储就得改 control。常见做法是抽一个ProductDao接口,把方法签名定下来,然后写ProductDaoMySQLImpl实现它。再写一个DaoFactory,根据配置文件决定返回哪个实现。这样 control 层只依赖接口,换实现时 control 一行不用改。
public interface ProductDao { boolean add(Product p); boolean update(Product p); boolean delete(int id); Product findById(int id); List<Product> findAll(); }接口定好之后,工厂类里用Class.forName(配置里的实现类名).newInstance()反射创建,配置文件里写productDao=dao.impl.ProductDaoMySQLImpl。这套写法就是早期 Spring 控制反转的雏形,理解了对后面学框架很有帮助。
6.2 引入简易连接池,把「每次新建连接」的坑填上
连接池的原理不复杂:程序启动时先建好一批连接放在池子里,用的时候借一个,用完还回去,而不是真的关闭。可以手写一个极简版:
public class SimplePool { private static final LinkedList<Connection> pool = new LinkedList<>(); private static final int SIZE = 5; static { try { Class.forName("com.mysql.jdbc.Driver"); for (int i = 0; i < SIZE; i++) { pool.add(DriverManager.getConnection(URL, USER, PASSWORD)); } } catch (Exception e) { throw new RuntimeException("连接池初始化失败", e); } } public static synchronized Connection borrow() { while (pool.isEmpty()) { try { SimplePool.class.wait(); } catch (InterruptedException ignored) {} } return pool.removeFirst(); } public static synchronized void giveBack(Connection conn) { pool.addLast(conn); SimplePool.class.notifyAll(); } }borrow()在池子空的时候等待,giveBack()归还后唤醒等待线程。这个实现很粗糙,没有处理连接失效和超时,但足以让你理解连接池的核心思想。生产环境直接用 HikariCP 或 Druid,不要自己写。
6.3 加一层事务控制,保证销售扣库存的原子性
超市系统里「卖出一件商品」通常要同时做两件事:插入一条销售记录、扣减商品库存。这两步必须要么都成功、要么都失败,否则会出现「记录有了但库存没扣」的脏数据。JDBC 默认是自动提交,每条 SQL 独立生效,需要手动改成手动提交:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 saleDao.add(conn, sale); // 传入同一个 conn,保证在同一事务里 productDao.reduceStock(conn, productId, quantity); conn.commit(); // 两步都成功才提交 } catch (SQLException e) { if (conn != null) conn.rollback(); // 任何一步失败就回滚 } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } }关键点是 dao 方法要接收外部传入的Connection,而不是自己内部getConnection()。这样两个 dao 操作才能共用同一个连接、同一个事务。这个改造会让 dao 方法签名变复杂,但这是从「课设」走向「能用的系统」必须迈的一步。
6.4 验证改造是否成功的三个检查点
改完之后怎么确认没改坏?我一般会走这三步。第一,正常流程跑一遍:添加商品 → 查询确认存在 → 卖出 → 查库存减少 → 查销售记录新增。第二,异常流程跑一遍:在扣库存那一步手动抛异常,看销售记录有没有被回滚掉。第三,并发流程跑一遍:开两个线程同时卖同一件库存为 1 的商品,看会不会超卖。第三步需要把连接池和事务都配对,如果还能保证不超卖,说明事务隔离级别和锁用对了。
我自己的习惯是,每次改完 dao 层,都会先把supermarket.sql重新导入一遍,保证测试数据是干净的,然后再跑上面三步。这样能避免旧数据干扰判断。这份课设源码的价值不在于它本身多完善,而在于它给了你一个足够小、足够透明的起点,让你能亲手把连接池、事务、接口解耦这些概念一个个加进去,看着系统从「能跑」变成「跑得稳」。希望帮到你。
本文还有配套的精品资源,点击获取