Java课设超市管理系统实战:JDBC分层架构与MySQL从环境到事务 简介一份面向高校Java课程设计的超市管理系统完整项目整体采用命令行文本交互全面覆盖商品、库存、销售等核心业务的数据维护适合需要利用Java与MySQL完成课程设计并希望深入理解JDBC与分层架构的开发者。资源包共49个文件包含22个Java源码及其编译后的class文件、1个SQL建表脚本、1个MySQL连接驱动的jar包、以及工程配置文件与IDE设置文件整体压缩包约783KB。目前已有1379人学习下载源码按control、dao、po、db、view、main六个包组织清晰区分控制、数据访问、实体、数据库与视图等职责SQL脚本可直接导入MySQL生成数据表配合驱动可快速跑通数据库连接流程。通过阅读和运行这套代码读者能掌握JDBC连接管理、SQL异常处理、分层编程等关键技能也能为独立开发同类管理信息系统提供一套可复用的课设参考。1. 一个 Java 课设超市管理系统到底值不值得动手跑一遍如果你是 Java 初学者手上正好需要一个“能交差、能讲清楚、还能在答辩时说出点门道”的课程设计这类java 超市管理系统 MySQL的控制台项目是最合适的练手样本。它没有 Spring、没有 MyBatis就是纯 Servlet 时代的经典写法JDBC 连库、PO 装数据、DAO 做查询、Control 管逻辑、View 做交互。很多人在网上下到这类压缩包后第一反应是“代码能跑吗”我的经验是八成跑不起来的人不是代码有问题而是 Java 版本、MySQL 版本、驱动 jar 和编码这四座山没搬走。这个项目正是这样一份素材——里面有完整的 src 分层源码、数据库脚本 supermarket.sql、还有配好的驱动包 lib/mysql-connector-java-5.1.18-bin.jar。适合三类人第一次做课设想照葫芦画瓢的新手、想复习 JDBC 和分层设计的求职者、以及需要快速改造成自己的系统拿去答辩的同学。这篇文章我会从压缩包结构开始一步步带你把它跑起来再把最容易踩的坑挨个点名。2. 先看懂压缩包supermarket.zip 的五包结构与运行入口2.1 文件清单速览每个目录是干嘛的拿到supermarket.zip先别急着双击运行。我一般会先解压看一眼目录结构心里有个地图后面改代码时才知道去哪找文件。路径作用supermarket.sqlMySQL 数据库脚本里面是建表语句和初始数据是整个系统的数据地基src/源码根目录五个包全在这src/control/业务逻辑层处理登录校验、商品增删改查、销售等业务规则src/dao/数据访问层所有 JDBC 操作都在这一层src/po/实体类对应数据库表结构比如商品表、用户表src/db/数据库连接管理通常是一个工具类负责拿 Connectionsrc/view/交互层控制台菜单、输入输出都在这里lib/依赖 jar只有 mysql-connector-java-5.1.18-bin.jar说明这是一个纯 JDBC 项目bin/编译后的 class 文件能直接跑的话入口就在这.classpath/.projectEclipse 工程配置文件说明这份代码是用 Eclipse 写的有一个细节值得注意项目里没有 pom.xml也没有 build.gradle所以它不是 Maven/Gradle 工程。想导入 IDE 的话Eclipse 直接 Open Projects from File System 最省事IDEA 里选 Eclipse 导入也可以就是路径配置要手动指一下。2.2 五包职责这份分层对应什么样的架构思想有人看到control、dao、po、db、view这五个包名会问这不是和 MVC 差不多吗确实它就是 MVC 的一种轻量变体只不过把 Controller 叫成了 Control把 Model 拆成了 PO 和 DAO 两层。POPlain Object层最朴素一个类对应一张表类里的私有字段对应表的列。比如商品表有id、name、price、stock四个字段Goods.java里就一定有四样东西四个字段、四个 getter、四个 setter。这个层不掺任何 JDBC 代码它只负责“搬数据”。DAO 层是 JDBC 的核心战场这里写SELECT、INSERT、UPDATE、DELETE语句用PreparedStatement绑定参数把结果集ResultSet转成 PO 对象返回。这层最容易出现代码混乱的地方是很多人喜欢在这里写业务判断比如“库存不足不能卖”——这其实是 Control 层的活放在 DAO 里就会导致 Control 层除了调 DAO 无事可做架构直接塌了。Control 层是业务中枢登录时校验用户名密码、卖货时先查库存再扣库存、进货时更新商品数量这些“先干什么后干什么”的规则都在这一层。DB 层最简单通常就是一个DBUtil或者ConnectionManager类负责加载驱动、返回连接。View 层是控制台交互打印菜单、读取输入、调用 Control 层的方法它不知道 SQL 长什么样也不直接碰数据库。2.3 从包结构和 SQL 反推业务流程如果你拿到的supermarket.sql里有几张核心表业务流就很好推了。通常是这样的管理员表登录用、商品表存商品名、进价、售价、库存、销售记录表存每一次卖出商品的明细。对应的控制台菜单大体是登录 → 主菜单 → 商品管理增删改查→ 进货 → 销售 → 退出。业务操作涉及的表典型 SQL登录管理员表SELECT * FROM user WHERE username? AND password?查看商品列表商品表SELECT * FROM goods新增商品商品表INSERT INTO goods(name,price,stock) VALUES(?,?,?)修改价格商品表UPDATE goods SET price? WHERE id?删除商品商品表DELETE FROM goods WHERE id?销售扣库存商品表 销售表UPDATE goods SET stockstock-? WHERE id?这些 SQL 在 DAO 层里几乎都会出现。我建议你拿到压缩包后先打开supermarket.sql看一遍表结构再对照 src 里的 DAO 类看一遍业务逻辑的骨架基本就清楚了。这一步做完你对整个系统的理解深度会超过大多数直接抄代码的同学。3. 从零跑起来JDK、MySQL 与驱动 jar 的装配顺序3.1 环境准备JDK 版本怎么选这个项目的驱动是 5.1.18对应的是 JDBC 4.0 规范Class.forName 加载驱动那行代码可以保留也可以去掉——JDBC 4.0 之后驱动会自动注册但课设代码里基本都保留着Class.forName(com.mysql.jdbc.Driver)跑起来没影响。JDK 版本我建议用 Java 8。原因很现实这个驱动 jar 和 MySQL 5.x 的组合在 Java 8 下最流畅不会碰到模块化带来的java.sql模块访问问题。如果你机器上装的是 Java 17编译大概率能过但运行时可能会遇到“cannot access class com.mysql.jdbc.Driver in module”之类的幺蛾子没必要给自己加戏。装完 JDK 后在命令行验证一下java -version javac -version两条命令都显示版本号就说明环境变量配好了。如果java -version有输出但javac提示找不到命令那就是JAVA_HOME配了但PATH里没加%JAVA_HOME%\bin这是新手常犯的顺序问题。把JAVA_HOME指到 JDK 安装目录再在 Path 里追加一条%JAVA_HOME%\bin重新打开命令行就生效了。3.2 数据库准备导入 supermarket.sql 的完整操作MySQL 版本方面5.5、5.7 都能直接跑这份脚本。如果你装的是 MySQL 8.0脚本能导入但驱动的兼容问题会在第 5 章细说。导入前先建库因为很多课设脚本里不包含CREATE DATABASE语句直接用source导入会报“No database selected”。mysql -uroot -p登录 MySQL 后依次执行下面三条命令CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4; USE supermarket; SOURCE D:/path/to/supermarket.sql;SOURCE后面要用绝对路径这是最常被忽略的一点。如果你嫌命令行麻烦用 Navicat 或 DataGrip 的“运行 SQL 文件”功能也行但导入完成后务必确认两件事数据库名是不是叫supermarket、表是不是都建出来了。用SHOW TABLES;看一眼如果结果为空说明脚本没导入成功回去查路径和编码。这里有个容易踩的坑脚本文件如果是以 UTF-8 编码保存的但 MySQL 客户端默认用 GBK 读取导入后表里中文注释会变成乱码不影响功能但影响观感。导入前可以先用记事本打开 sql 文件看一眼编码确认是 UTF-8 再操作。3.3 驱动 jar 的正确装配方式lib/mysql-connector-java-5.1.18-bin.jar是整个项目唯一的外部依赖它的位置决定了程序能不能连上数据库。Eclipse 里的操作是右键项目 → Build Path → Configure Build Path → Libraries → Add External JARs选中 lib 目录下的驱动包。IDEA 里操作不同File → Project Structure → Modules → Dependencies → 加号 → JARs or directories选同一个 jar。如果这一小步漏了报错会非常经典java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。另外注意不要把已经导入的 jar 再复制到src目录下那样有时会造成编译混乱。正确的做法只有一个jar 待在 lib 目录被 Build Path 引用编译时它参与运行时它也在 classpath 里。我见过有人把 jar 复制到 src 下面结果报了一堆莫名其妙的重复类错误属于自己给自己挖坑。3.4 连接参数从配置文件到 DBUtil 的补全逻辑这个项目的db包里应该有数据库连接工具类常见的写法是读一个名为db.properties的配置文件。如果压缩包里没有这个文件通常是因为代码里写死了连接参数。我建议你不管原来怎么写统一改成读配置文件的形式答辩时也好说“我用配置文件管理数据库连接参数”。jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8 jdbc.usernameroot jdbc.password123456对应工具类的关键代码大概是这样的public class DBUtil { private static String url; private static String username; private static String password; static { try { // 从 classpath 根目录加载 db.properties Properties props new Properties(); InputStream is DBUtil.class.getClassLoader() .getResourceAsStream(db.properties); props.load(is); Class.forName(props.getProperty(jdbc.driver)); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }这段代码的核心逻辑是类加载时通过静态块读取db.properties注册驱动然后把连接参数保存在三个静态变量里。getConnection()每次调用都通过DriverManager返回一个全新的连接。注意db.properties要放在src根目录下编译后才会出现在 classpath 根路径否则getResourceAsStream会返回 null 然后抛 NullPointerException。配置文件里password改成你自己的数据库密码URL 里的supermarket必须和 3.2 节建的库名完全一致。4. 看懂 JDBC 登录链路从 db 层到 view 层的调用怎么串起来4.1 登录模块的 DAO 实现参数化查询比字符串拼接强在哪打开src/dao/下的用户表 DAO大概率能看到一个findUserByUsername(String username)或者login(String username, String password)方法。这是整个系统里最值得细读的一段代码因为它是 JDBC 六步的完整示范注册驱动、获取连接、创建语句、执行查询、处理结果集、释放资源。驱动注册已经挪到 DBUtil 静态块里了所以 DAO 里实际只剩后五步。下面是常见的实现方式public class UserDao { public User login(String username, String password) { String sql SELECT * FROM user WHERE username ? AND password ?; // 连接和结果集需要在 finally 里释放防止连接泄漏 try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }这段代码里最值得讲的是PreparedStatement的参数绑定。ps.setString(1, username)把第一个问号替换成变量替换过程由 JDBC 驱动处理它会自动对特殊字符做转义所以即使用户输的是 OR 11这种字符串也只被当成普通文本而不是 SQL 代码片段。如果你在原项目里看到的是字符串拼接 SQL比如SELECT * FROM user WHERE username username 那这个系统存在 SQL 注入漏洞答辩时老师一追问就会露馅。我的建议是不管原代码怎么写的登录方法一律改成参数化查询这是课设里性价比最高的安全改造。另外一个细节是返回值和 null 的语义。方法返回User对象查到了就返回带数据的对象查不到返回 null由上层 Control 判断 null 之后输出“用户名或密码错误”。这意味着 SQL 的WHERE条件要把用户名和密码同时带上属于“一次查验证全部凭证”的写法。另一种思路是先查用户名再比对密码多一次查询但能区分“用户不存在”和“密码错误”两种提示那种写法更利于用户体验但课设里两种都常见。4.2 Control 层怎么接住 DAO 的返回值Control 层的职责是拿到 DAO 的结果后做业务判断。登录的 Control 方法通常长这样public class LoginControl { private UserDao userDao new UserDao(); /** * 返回值 1 表示登录成功0 表示用户名或密码错误 */ public int login(String username, String password) { User user userDao.login(username, password); if (user null) { return 0; } return 1; } }Control 层不直接拼 SQL也不自己打开数据库连接。它只是把 View 传来的两个字符串原样转交给 DAO然后对返回的User对象做判断。这里有几个命名习惯值得学习方法名直接叫业务动作login返回int而不是布尔值——这样将来如果有“账号被锁定”的第三种状态只需要把返回值改成 2View 层加一个分支就行。如果你拿到手的项目里这个方法返回的是boolean也不影响功能只是扩展时稍微费点事。4.3 View 层控制台怎么把流程串起来View 层的 main 方法大概是整个项目里最长的类因为它要负责打印菜单、接收键盘输入、调用 Control、再根据返回值打印不同提示。核心结构是while(true)循环加 switch 分支public class MainView { public static void main(String[] args) { Scanner sc new Scanner(System.in); System.out.println( 超市管理系统 ); System.out.print(用户名: ); String username sc.nextLine(); System.out.print(密码: ); String password sc.nextLine(); LoginControl loginControl new LoginControl(); int result loginControl.login(username, password); if (result 0) { System.out.println(用户名或密码错误); return; } while (true) { System.out.println(1.商品管理 2.进货 3.销售 4.退出); int choice sc.nextInt(); // 根据 choice 调用不同的 Control 方法 } } }注意看这段代码里的三个层次sc.nextLine()负责读输入loginControl.login()触发业务逻辑返回值决定页面走向。这正好对应了 view→control→dao→db 的调用方向也是整个项目最重要的理解主线——数据从键盘进一路穿过 view 和 control到达 dao 变成 SQL 参数最后以 ResultSet 的形式原路返回。很多人课设做完不知道自己在写什么就是把这条链路当成了“背代码”实际上只要按这个方向读代码每一步的输入输出都清清楚楚。5. 常见问题与排查五个课设里最容易翻车的点5.1 环境类报错驱动没加载、连接被拒绝现象一控制台抛java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。这是出现频率最高的报错原因只有一个——驱动 jar 没有被加入项目的 Build Path或者加了但加错了地方。注意ClassNotFoundException是运行时才报的代码能编译不代表运行没问题。解决步骤先确认 lib 目录下有那个 jar再重新做一遍 3.3 节的导入操作。如果你是 IDEA 用户导入之后注意看 Dependencies 列表里有没有红叉有红叉说明 jar 路径失效了重新选一次即可。现象二java.sql.SQLException: Access denied for user rootlocalhost。数据库密码不对或者用户名不对。原因通常是 db.properties 里的密码和你 MySQL 实际密码不一致。很多人把数据库密码写到代码里之后忘了自己改过密码排查时第一件事就是去数据库客户端实测一下root/密码能不能登录。解决修改db.properties里的jdbc.password改完必须重启程序因为连接参数只在类加载的静态块里读一次。5.2 数据类报错库名对不上、中文变乱码现象三java.sql.SQLException: Unknown database supermarket。这个报错意思很明确——URL 里写的库名在 MySQL 里不存在。原因一般是只导入了 sql 脚本里的表但没建数据库或者建库时名字不一样。解决按 3.2 节的顺序先执行CREATE DATABASE supermarket再导入。如果你用的是 Navicat 的“运行 SQL 文件”它会默认在某个库下执行表创建语句如果你没选库就会跑到系统库里这时程序连过来自然找不到表。排查方法是进 MySQL 执行SHOW DATABASES;看有没有 supermarket 这个库再USE supermarket; SHOW TABLES;看表在不在这。现象四登录成功后控制台输出全是???或者乱码。数据库里的中文、控制台输出的中文都乱成一锅粥。原因有两个方向数据库连接 URL 里没有指定字符集或者 MySQL 表本身的字符集不是 utf8。解决URL 加上?useUnicodetruecharacterEncodingutf8这是 5.1 驱动时代最标准的写法。如果你用 MySQL 5.7也顺手把default-character-setutf8加进 MySQL 的 my.ini 的[mysql]段再重启服务。Windows 的 cmd 窗口默认用的是 GBK 码页Java 程序输出 UTF-8 字符到 cmd 时也会显示乱码这是显示环境的问题不是程序问题把 cmd 切到 UTF-8执行chcp 65001或者直接改用 IDEA 的控制台运行就能看清楚。5.3 版本类报错驱动 5.1.18 撞上 MySQL 8.0现象五MySQL 8.0 环境下运行时出现Communications link failure或The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。这是驱动 5.1.18 和 MySQL 8.0 的时区特性冲突导致的。5.1 这个驱动年代MySQL 还没有强制校验 serverTimezone到了 8.0 版本连接握手时服务器时区上报给客户端旧驱动解析不了这个中文时区描述就直接拒绝连接。解决有两个方向第一个是把 URL 加上?serverTimezoneAsia/ShanghaiuseSSLfalse5.1.18 驱动认这个参数加了就能连第二个是一劳永逸地换驱动 jar去 Maven 中央仓库下载mysql-connector-java-8.0.xx.jar放到 lib 目录替换旧的然后代码里把驱动类名从com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver。我更推荐后者因为新版驱动对 8.0 的认证插件支持更完整也不会再碰到时区这个玄学问题。6. 课设加分项把销售模块改成事务版的实操技巧6.1 一个连接完成两个写操作课设里的销售功能通常是两步往销售记录表插入一条数据再扣减商品库存。原项目如果分别在 DAO 里打开两次连接、各执行各的就会存在一个数据一致性隐患——插入成功了但扣库存失败或者反过来数据库里留下一笔残缺记录。这在答辩时是个非常好的加分点改造思路也很简单让两个 DAO 方法共用同一个 Connection最后统一提交或回滚。具体做法是给 DAO 方法增加一个带 Connection 参数的版本由 Control 层负责开启事务public class SaleControl { public boolean sale(int goodsId, int count, double totalPrice) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 SaleDao saleDao new SaleDao(); GoodsDao goodsDao new GoodsDao(); saleDao.insertSale(conn, goodsId, count, totalPrice); goodsDao.reduceStock(conn, goodsId, count); conn.commit(); // 两步都成功才提交 return true; } catch (Exception e) { e.printStackTrace(); try { if (conn ! null) { conn.rollback(); // 任何一步失败就回滚 } } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { DBUtil.close(conn, null, null); } } }注意这里 DAO 方法签名换成了insertSale(Connection conn, ...)而不是原来的insertSale(...)这是为了把连接控制权上交给调用方。为什么必须这样做因为事务的作用范围就是“同一个连接从 setAutoCommit(false) 到 commit”之间。如果用两个连接各自自动提交事务就无从谈起。这个改造说明了一个容易被忽略的道理JDBC 事务和数据库连接的绑定关系。6.2 验证方法用一组测试数据跑通全流程改造完成后验证它是否真的可靠做法很简单在事务里故意抛一个异常比如在insertSale之后手动throw new RuntimeException(模拟异常)然后运行销售功能。如果事务生效你会发现库存没有被扣减数据完整回滚。验证完把测试代码删掉恢复正常的销售流程。这是我最常用的自测手段比盯着代码干想快得多。然后跑一遍完整流程用管理员的账号登录进入商品管理新增一件商品价格设 5 元库存设 10 件再去销售模块卖 3 件最后回商品列表查看库存应该变成 7销售记录表里多了一行。如果每一步查到的数据都符合预期说明这套控制台版的超市管理系统已经能正常运作了。这套流程也适合答辩演示时的准备脚本照着走不会卡壳。有一件事我记得很清楚我当年第一次改这类课设时把扣库存和插订单写进了两个连接里测试时发现库存扣了订单却没插进去账目对不上当时还以为是 MySQL 出问题了。从那以后我只要遇到一个业务涉及多个写操作都会强制把连接收拢到同一事务里走一遍这个习惯帮我在好几个项目里避免过数据不一致的尴尬。希望这篇拆解能帮你把这个课设跑通也能让你在答辩时多一个真正能讲透的技术点。本文还有配套的精品资源点击获取