多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java+MySQL超市管理系统课设源码拆解:JDBC分层与改造

Java+MySQL超市管理系统课设源码拆解:JDBC分层与改造 简介这是一份面向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. 从一份超市课设源码说起JavaMySQL 的后端骨架到底长什么样很多同学做 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、Userdao数据访问层封装所有 SQL 操作ProductDao、SaleDaodb数据库连接管理负责加载驱动、拿 ConnectionDBUtil或DBHelpercontrol业务控制层编排 dao 完成一个完整业务ProductControlview控制台菜单与输入输出MainView、LoginViewmain程序入口启动主循环Mainlib目录里放的是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 只管 SQLcontrol 只管业务编排。你改任何一个包只要接口不变其他包不用动。比如要把 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.sqlmysql -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?useUnicodetruecharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456;把USER和PASSWORD改成你本机的 MySQL 账号密码。URL里的库名要和上一步建好的库一致。characterEncodingutf8这个参数别删否则中文商品名会变乱码。第三步在 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?useUnicodetruecharacterEncodingutf8; 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 后面加上serverTimezoneAsia/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-resourcesConnection和PreparedStatement会在代码块结束时自动关闭不需要手动调DBUtil.close()。PreparedStatement用?占位符而不是字符串拼接这是防 SQL 注入的关键——用户输入的商品名里就算带单引号也只会被当成普通字符不会改变 SQL 结构。executeUpdate()返回的是受影响的行数。插入成功返回 1失败返回 0 或抛异常。dao 层把布尔结果返回给 controlcontrol 再返回给 viewview 根据结果打印「添加成功」或「添加失败」。整条链路里每一层只关心自己那一层的返回值不越界处理。4.2 查询、更新、删除的写法差异查询操作和写操作的区别在于要处理ResultSet。以findAll为例public ListProduct findAll() { ListProduct 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 里有useUnicodetruecharacterEncodingutf8。最后确认 IDE 的 console 编码是 UTF-8Eclipse 在 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 而不是空列表或者某一行某个字段在数据库里是 NULLrs.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 是具体类直接 newcontrol 层和 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); ListProduct findAll(); }接口定好之后工厂类里用Class.forName(配置里的实现类名).newInstance()反射创建配置文件里写productDaodao.impl.ProductDaoMySQLImpl。这套写法就是早期 Spring 控制反转的雏形理解了对后面学框架很有帮助。6.2 引入简易连接池把「每次新建连接」的坑填上连接池的原理不复杂程序启动时先建好一批连接放在池子里用的时候借一个用完还回去而不是真的关闭。可以手写一个极简版public class SimplePool { private static final LinkedListConnection 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重新导入一遍保证测试数据是干净的然后再跑上面三步。这样能避免旧数据干扰判断。这份课设源码的价值不在于它本身多完善而在于它给了你一个足够小、足够透明的起点让你能亲手把连接池、事务、接口解耦这些概念一个个加进去看着系统从「能跑」变成「跑得稳」。希望帮到你。本文还有配套的精品资源点击获取
返回列表