☰
Java+Swing+MySQL点餐系统开发:数据库设计、事务处理与JAR打包
2026/10/7 10:40:24 网站建设 项目流程

简介:这套基于Java Swing与MySQL的餐厅点餐管理系统,面向Java初、中级学习者及需要课程设计/毕业设计参考的高校学生。项目完整实现管理员与顾客双角色流程,包含注册登录、套餐新增与管理、订单处理、点餐及个人信息修改等核心模块,覆盖Swing界面设计、JDBC数据库操作、分层开发等常见知识点。压缩包共170个文件,以18个java源文件、66个class编译文件为主,附带数据库sql脚本、界面截图(png/jpg)及项目依赖jar包,整体大小2.19MB,解压后可直接导入IDE运行。目前已有1532人学习,适合用来对照练习Swing窗体布局、事件监听与MySQL数据交互。通过阅读源码和界面素材,可快速理解点餐系统从数据表设计到功能实现的全过程,也可在此基础上扩展菜品分类、销量统计等功能。

1. 为什么 2025 年还有人拿 Java+Swing+MySQL 做点餐系统

如果你去翻各大高校的 Java 课程设计题目,点餐管理系统几乎是出场率最高的那个。不是因为它时髦,而是因为它把桌面应用最核心的三件事全占了:界面交互、数据库持久化、业务逻辑落盘。Java 负责业务建模,Swing 把操作界面拖出来,MySQL 把订单和菜品的数据存住——三个技术点都是 Java 桌面开发绕不开的基本功。你要是能把这套系统从零写到能跑,Java 面试题里常问的 JDBC 连接、事务控制、PreparedStatement 防注入、事件监听这些概念,就不再是背出来的,而是真正用过的。

这篇文章不会给你贴一份完整源码让你复制粘贴了事,而是把做这个系统最常见的方案拆开讲清楚:数据库表怎么设计、DAO 层怎么写、Swing 界面怎么跟数据绑定、订单提交时怎么处理事务,以及最后怎么打包成能发给别人的可执行 JAR。每段代码后面我都会说明为什么这样写、哪些参数能调、失败时应该看哪里。新手可以照着敲,熟手可以直接跳到第五章看优化思路。

2. 技术选型:Swing 和 MySQL 在这个项目里谁说了算

2.1 为什么是 Swing 而不是 JavaFX

Java 桌面开发现在有两个官方 UI 框架可选:Swing 和 JavaFX。很多新项目选了 JavaFX,因为界面更现代、支持 CSS 样式,但点餐管理系统这类课设和内部小工具,Swing 反而更合适。原因有三条:

第一,Swing 是 JDK 自带的,不需要额外装运行时。JavaFX 从 JDK 11 开始被剥离出 JDK,要单独引依赖,光这一步就能劝退一批环境还没配好的新手。

第二,Swing 的资料存量极大。你搜"Java+Swing+MySQL 点餐系统",能找到十年前的论坛帖子、GitHub 上的完整项目、CSDN 上的设计文档。遇到问题一搜就有答案,这对课设赶工来说是最实际的效率优势。

第三,Swing 的事件监听模型足够经典。ActionListener、MouseListener、TableModel 这些接口,理解透了以后看 Android 的点击事件、看前端的事件绑定,都是同一个套路。用 Swing 做项目,练的是底层的事件驱动思维,而不是某个框架的 API。

当然你如果做一个要上线商用、对界面颜值有要求的系统,Swing 确实不够看。但如果是课程设计、毕业设计、企业内部工具,Swing 的成熟稳定反而成了优点。

2.2 MySQL 选型和 JDBC 驱动版本怎么定

MySQL 这边,最常见的教学组合是 MySQL 5.7 或 8.0 + mysql-connector-java 8.x 驱动。如果你机器上已经装了 8.0,直接用 8.0 的驱动就行;如果用的是 5.7,驱动也建议用 8.x 版本,因为它向下兼容 5.x 的协议。连接字符串写这一行:

jdbc:mysql://localhost:3306/restaurant?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8

参数说明:

  • useSSL=false:本地开发不需要加密连接,不加这个参数 MySQL 8.0 会警告,虽然不影响运行但日志会很吵
  • serverTimezone=Asia/Shanghai:MySQL 8.0 默认时区是 UTC,不加这个参数你往数据库里存时间,取出来会差 8 小时
  • characterEncoding=utf8:保证中文菜品名和订单备注不乱码

驱动 JAR 包下载后放到项目里的lib目录,用 IDE 导入时选择"Add as Library"。注意不要用System.setProperty("jdbc.drivers", ...)这种写法,直接在DriverManager.getConnection()之前写Class.forName("com.mysql.cj.jdbc.Driver"),虽然 JDBC 4.0 以后这一步可以省略,但写上能让阅卷老师一眼看懂你在加载驱动。

2.3 数据库连接用 JDBC 还是连接池

课设项目里直接用 JDBC 是常规操作,但你要是想在答辩时多拿点分,用一个简单的连接池会让代码质量上一个档次。自己做课设的话别引入 MyBatis 或者 HikariCP 这种重量级框架——没必要,而且答辩时老师会追着问原理。我一般建议自己写一个极简连接池,用LinkedList存连接对象,核心逻辑也就三十行:

public class SimpleConnectionPool { private static LinkedList<Connection> pool = new LinkedList<>(); private static final int INITIAL_SIZE = 5; private static final int MAX_SIZE = 10; private static String url = "jdbc:mysql://localhost:3306/restaurant?..."; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); for (int i = 0; i < INITIAL_SIZE; i++) { pool.add(DriverManager.getConnection(url, "root", "password")); } } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { if (pool.isEmpty()) { synchronized (pool) { if (pool.isEmpty() && pool.size() < MAX_SIZE) { pool.add(DriverManager.getConnection(url, "root", "password")); } } } return pool.removeFirst(); } public static void returnConnection(Connection conn) { if (conn != null) { pool.add(conn); } } }

逻辑说明:连接池的本质就是预先创建几个连接放在池子里,用的时候从池头取,用完还回池尾。synchronized保证多线程同时取连接时不会重复创建超出上限的连接。这里要注意MAX_SIZE不能设太大,MySQL 默认的最大连接数是 151,你池子里放 100 个等于把数据库逼到极限。

3. 数据库表设计与 DAO 落地:把菜单和订单变成可操作的对象

3.1 建表 SQL:五张表怎么设计才不冗余

点餐系统的核心数据是菜品、订单、订单明细,再加上员工和管理员。设计表的时候记住一个原则:一张表只存一类事物的信息,订单和菜品之间用中间表关联。完整的建表脚本是这样的:

CREATE DATABASE IF NOT EXISTS restaurant DEFAULT CHARSET utf8mb4; USE restaurant; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(20) ); CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL UNIQUE, sort_order INT DEFAULT 0 ); CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(50) NOT NULL, price DECIMAL(10, 2) NOT NULL, image_path VARCHAR(100), description TEXT, status TINYINT DEFAULT 1 COMMENT '1上架 0下架', FOREIGN KEY (category_id) REFERENCES category(id) ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, table_no VARCHAR(10) NOT NULL, total_amount DECIMAL(10, 2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0未支付 1已支付 2已出餐', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME ); CREATE TABLE order_detail ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, dish_name VARCHAR(50) NOT NULL, price DECIMAL(10, 2) NOT NULL, quantity INT NOT NULL, subtotal DECIMAL(10, 2) NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (dish_id) REFERENCES dish(id) );

表设计说明:order_detail里同时存了dish_id和dish_name,有人会觉得冗余,但这恰恰是为了防止菜品改名或删除后订单记录对不上。实际做餐饮的系统都是这么干的,这叫"快照冗余",用空间换数据的稳定性。DECIMAL(10,2)而不是DOUBLE是因为浮点数算钱会丢精度,这也是 Java 后端开发里讲小数计算时的标准答案。菜品价格如果只用DOUBLE,一次0.1 + 0.2的结果就能让你在结账时少收几分钱。

3.2 DAO 层的三个必写方法:查询、插入、更新

DAO(Data Access Object)这一层做的是把 SQL 语句封装成 Java 方法。以菜品为例,最基础的是按分类查询菜品、新增菜品、修改菜品状态。查询菜品的代码用 PreparedStatement 防止 SQL 注入:

public class DishDAO { public List<Dish> findByCategoryId(int categoryId) { String sql = "SELECT * FROM dish WHERE category_id = ? AND status = 1 ORDER BY id ASC"; List<Dish> list = new ArrayList<>(); try (Connection conn = SimpleConnectionPool.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, categoryId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Dish dish = new Dish(); dish.setId(rs.getInt("id")); dish.setName(rs.getString("name")); dish.setPrice(rs.getBigDecimal("price")); dish.setImagePath(rs.getString("image_path")); list.add(dish); } } } catch (SQLException e) { e.printStackTrace(); } finally { // 连接归还到连接池 } return list; } }

代码里的关键点:try-with-resources写法能保证ResultSet、PreparedStatement自动关闭,这个写法从 JDK 7 开始就有,Java 面试题里常考;?占位符配合ps.setInt()传入参数,SQL 注入攻击在这里失效,因为PreparedStatement会在数据库端预编译,用户输入不会被当成 SQL 代码拼接执行。

新增菜品的方法类似,但要注意设置返回自增主键。MySQL 的AUTO_INCREMENT字段插入成功后不会自动回填到 Java 对象的 id 属性里,需要额外处理:

public boolean insertDish(Dish dish) { String sql = "INSERT INTO dish (category_id, name, price, description, status) VALUES (?, ?, ?, ?, 1)"; try (Connection conn = SimpleConnectionPool.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, dish.getCategoryId()); ps.setString(2, dish.getName()); ps.setBigDecimal(3, dish.getPrice()); ps.setString(4, dish.getDescription()); int rows = ps.executeUpdate(); if (rows > 0) { try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { dish.setId(keys.getInt(1)); } } return true; } } catch (SQLException e) { e.printStackTrace(); } return false; }

这里的Statement.RETURN_GENERATED_KEYS是个容易忽略的细节。不加这个参数,getGeneratedKeys()返回的结果集就是空的,你拿到的新增记录 id 永远是 0。这个坑我在给项目加购物车功能时实实在在踩过:菜品加入购物车后想按 id 修改数量,结果 id 全是 0,一改就把第一条记录改了。

3.3 订单提交的原子性问题:事务边界怎么画

订单提交是整个系统里唯一涉及多表写入的操作:先往orders表插入订单主表,再往order_detail表插入明细。这两步必须是一个事务——如果订单主表插入成功但明细失败,就会出现一张没有内容的空订单。Java SE 里没有 Spring 的事务注解,事务得手动控制:

public boolean createOrder(Order order, List<OrderDetail> details) { Connection conn = null; try { conn = SimpleConnectionPool.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,手动开启事务 String insertOrder = "INSERT INTO orders (order_no, table_no, total_amount, status) VALUES (?, ?, ?, 0)"; try (PreparedStatement ps = conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, generateOrderNo()); ps.setString(2, order.getTableNo()); ps.setBigDecimal(3, calculateTotal(details)); ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { order.setId(keys.getInt(1)); } } } String insertDetail = "INSERT INTO order_detail (order_id, dish_id, dish_name, price, quantity, subtotal) VALUES (?, ?, ?, ?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(insertDetail)) { for (OrderDetail detail : details) { ps.setInt(1, order.getId()); ps.setInt(2, detail.getDishId()); ps.setString(3, detail.getDishName()); ps.setBigDecimal(4, detail.getPrice()); ps.setInt(5, detail.getQuantity()); ps.setBigDecimal(6, detail.getPrice().multiply(BigDecimal.valueOf(detail.getQuantity()))); ps.addBatch(); } ps.executeBatch(); } conn.commit(); // 全部成功,提交事务 return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); // 任一环节出错,回滚所有操作 } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { if (conn != null) { try { conn.setAutoCommit(true); // 恢复自动提交,把连接还回池子 SimpleConnectionPool.returnConnection(conn); } catch (SQLException e) { e.printStackTrace(); } } } }

这段代码有三处值得复盘的地方。第一,executeBatch()批量执行明细插入,比一条条executeUpdate()快得多,数据量大时差距明显。第二,conn.setAutoCommit(false)之后,如果忘记commit()就关连接,MySQL 会默认回滚,数据静默丢失。第三,连接归还前一定要把autoCommit恢复成true,否则连接池里的其他代码拿到的还是事务态连接,一个新的查询可能读到旧事务里的脏数据。

4. Swing 界面与数据绑定:别把界面代码写成三千行的上帝类

4.1 主窗口布局:JFrame + CardLayout 还是 JTabbedPane

点餐系统的界面通常分两个角色:服务员端要点单、结算,管理员端要维护菜品、看订单统计。最常见的布局是左侧一个导航栏,右侧内容区根据点击切换面板。实现这个效果有两种方式——JTabbedPane或者CardLayout。

JTabbedPane 适合角色功能固定、标签不会动态变化的管理端,代码少,外观自带边框。而服务员点餐界面我更推荐CardLayout,因为你要配合全屏菜单展示,点餐时隐藏导航栏、结账时再切回来,CardLayout 可以直接控制显示哪一块:

JPanel mainPanel = new JPanel(); CardLayout cardLayout = new CardLayout(); mainPanel.setLayout(cardLayout); mainPanel.add(dishPanel, "DISH_PANEL"); mainPanel.add(orderPanel, "ORDER_PANEL"); mainPanel.add(settlePanel, "SETTLE_PANEL"); // 点击"去结算"按钮 cardLayout.show(mainPanel, "SETTLE_PANEL");

CardLayout的show(Container, String)方法的第二个参数是你在add()时指定的约束字符串。这里有个容易犯的错:页面切换后界面上之前输入的数据还留着,因为组件没有重建。解决办法是在切页监听里刷新数据,或者用removeAll()后重新add(),二选一,但别两个都做,否则会出现闪烁。

4.2 JTable 绑定数据的正确姿势:TableModel 与 setValueAt

菜品列表和订单明细都要用JTable展示。新手最容易踩的坑是直接往DefaultTableModel里塞数据,但不实现单元格更新回调,结果界面上改了数量,后台的订单对象完全不知情。正确做法是自定义TableModel,把数据源和界面控件实时同步:

public class DishTableModel extends AbstractTableModel { private String[] columns = {"菜品编号", "菜品名称", "价格", "数量"}; private List<OrderDetail> details; public DishTableModel(List<OrderDetail> details) { this.details = details; } @Override public int getRowCount() { return details.size(); } @Override public int getColumnCount() { return columns.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { OrderDetail d = details.get(rowIndex); switch (columnIndex) { case 0: return d.getDishId(); case 1: return d.getDishName(); case 2: return d.getPrice(); case 3: return d.getQuantity(); default: return null; } } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return columnIndex == 3; // 只允许修改数量列 } @Override public void setValueAt(Object value, int rowIndex, int columnIndex) { if (columnIndex == 3) { int quantity = Integer.parseInt(value.toString()); details.get(rowIndex).setQuantity(quantity); fireTableCellUpdated(rowIndex, columnIndex); } } }

这个 Model 类的价值在于:界面上的修改会直接写回OrderDetail对象,而不只是改了表格显示。fireTableCellUpdated()会通知JTable重绘该单元格,省去手动刷新整个表格。注意quantity的输入校验——解析成int之前要先try-catch,不然用户在单元格里输入"abc"会抛NumberFormatException,直接把整个界面搞崩。我一般会先写一个正则校验,只允许 1 到 99 之间的数字。

4.3 事件监听与耗时操作的坑:EDT 线程上别做数据库查询

Swing 的所有界面操作都在 Event Dispatch Thread(EDT)上执行。你在按钮的ActionListener里直接写数据库查询,点一下按钮界面就卡死——鼠标能动但窗口不刷新,这就是 EDT 被阻塞了。解决方式是用SwingWorker把耗时操作丢到后台线程:

btnSave.addActionListener(e -> { btnSave.setEnabled(false); // 防止重复提交 new SwingWorker<Void, Void>() { @Override protected Void doInBackground() { boolean ok = orderService.createOrder(order, details); if (ok) { SwingUtilities.invokeLater(() -> { JOptionPane.showMessageDialog(frame, "下单成功"); cardLayout.show(mainPanel, "DISH_PANEL"); btnSave.setEnabled(true); }); } return null; } }.execute(); });

doInBackground()里执行的是后台线程,可以放心查数据库;界面上的回调必须用SwingUtilities.invokeLater()切回 EDT 执行。这个写法的好处是用户界面始终流畅,点单时不会出现"未响应"的假死状态。如果不开新线程,MySQL 查询响应慢的瞬间,操作系统就会弹窗提示"Java 程序未响应",这在答辩演示时是相当尴尬的场面。

5. 三层架构拆分与订单编号生成:让代码不只是"能跑"

5.1 一层代码和三层代码的分界线在哪里

很多课设项目的代码是这样组织的:一个MainFrame.java里既有窗口布局、事件监听,又有 JDBC 查询代码。整个文件两千行,跑起来没问题,但你的"系统"也就停留在跑起来这个层面。要让它达到"系统"的水准,得把职责拆开——这就是常说的三层架构:

  • 表现层(UI):JFrame、JPanel、JTable 这些界面组件,只负责展示数据和收集用户操作
  • 业务逻辑层(Service):订单金额计算、库存校验、订单状态流转这些规则
  • 数据访问层(DAO):只在 Service 层被调用,持有 SQL 语句和 Connection 管理

我用一个实际的登录验证来说明这个分层的价值。如果你把SELECT * FROM admin WHERE username=? AND password=?写在按钮的 ActionListener 里,那这个按钮只能干登录这一件事,以后要加"记住密码自动登录"、"验证码校验",就得再复制一遍查询代码。如果拆出AdminService.login(String username, String password)方法:

public class AdminService { private AdminDAO adminDAO = new AdminDAO(); public boolean login(String username, String password) { // 参数校验 if (username == null || username.trim().isEmpty()) { throw new IllegalArgumentException("用户名不能为空"); } // 业务规则:密码加密后对比 String encryptedPwd = encrypt(password); Admin admin = adminDAO.findByUsername(username); return admin != null && admin.getPassword().equals(encryptedPwd); } private String encrypt(String rawPassword) { // MD5 或 SHA-256 哈希 return DigestUtils.md5Hex(rawPassword); } }

UI 层的登录按钮只需要调adminService.login(),根据返回的 boolean 决定跳转。这样将来换数据库、换加密算法、增加验证码,改动的范围都被隔离在 Service 层内部,UI 层一行代码都不用动。这也是 Java 面试题里"高内聚低耦合"的活教材。

5.2 订单编号生成:时间戳加随机数为什么不够

订单编号有两个要求:唯一性和可读性。有人用System.currentTimeMillis()加Random.nextInt(1000),但同一毫秒内两个线程同时下单会撞号,虽然概率低,但撞上一次就够你查半天的。我习惯用日期加序列号的方式:

public synchronized String generateOrderNo() { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); String timePart = sdf.format(new Date()); int sequence = sequenceCounter++; return timePart + String.format("%03d", sequence); }

synchronized保证同一时刻只有一个线程能执行编号生成,String.format("%03d", sequence)把序号补足三位,这样即使同一秒内有多个订单,编号也是"20250618143025001"这种形式,既不会重复,又能在日志里直接看出下单时间。sequenceCounter每次系统重启要重置回 1,但因为有时间戳参与拼接,重启后同一秒发生撞号的概率极低。如果确实需要更严格的唯一性,可以在数据库orders.order_no字段上加唯一索引,插入重复时捕获DuplicateKeyException重新生成。

5.3 多线程环境下的订单并发:一个隐藏的坑

服务员端可能同时开着多个窗口对同一个桌号下单。理论上两个窗口同时向orders表插入数据,不会有问题;问题出在数据校验环节——比如你要实现"每桌同时最多 4 人下单"或者"菜品库存不能为负"这类约束。这些校验如果在事务外执行,就会出现经典的并发问题:两个线程同时读到库存为 1,同时通过校验,同时扣减,最后库存变成 -1。

解法是给涉及库存的表增加乐观锁或悲观锁。Java 面试题里讲 MySQL 锁的分类时经常提到的SELECT ... FOR UPDATE就是悲观锁,在事务内锁定读取的行:

// 事务内执行 String checkStock = "SELECT stock FROM dish WHERE id = ? FOR UPDATE"; try (PreparedStatement ps = conn.prepareStatement(checkStock)) { ps.setInt(1, dishId); ResultSet rs = ps.executeQuery(); if (rs.next() && rs.getInt("stock") < quantity) { throw new BusinessException("库存不足"); } } // 然后执行 UPDATE dish SET stock = stock - ? WHERE id = ?

FOR UPDATE会让其他事务的SELECT和UPDATE等待当前事务提交或回滚。如果你的点餐系统不涉及库存扣减,这个技巧用不上;但你要是在菜单里卖"限量烤鸭",这就是必须掌握的写法。

6. 打包分发与常见问题排查:从你电脑搬到别人电脑不翻车

6.1 可执行 JAR 的导出与内嵌 MySQL 驱动

项目写完后,交付形态是一个可执行的 JAR 包。用 IDEA 的 Artifacts 功能导出时,默认打出来的 JAR 用java -jar运行会报ClassNotFoundException: com.mysql.cj.jdbc.Driver——因为依赖的 MySQL 驱动 JAR 根本没打进去。解决方法是打 fat JAR(也叫 uber JAR),把所有依赖解压后合并进一个 JAR 包。如果你用 Maven 管理项目,直接在pom.xml里加maven-shade-plugin;如果是手工导 JAR,IDEA 里选择"Build Artifacts -> Add -> JAR -> From modules with dependencies",然后在Main-Class里填你带main方法的类全名,比如com.restaurant.MainApp。

导出成功后命令行验证一下:

java -jar restaurant-system.jar

如果提示Error: Could not find or load main class,八成是 MANIFEST.MF 里的Main-Class写错了。注意类名前面不要带.class后缀,包名分隔符用点号。

6.2 五个高频报错与对应的排查方式

报错一:java.sql.SQLException: Access denied for user 'root'@'localhost'。现象是程序启动后连不上数据库。原因一般是密码错误,或者 MySQL 8.0 用的加密插件是caching_sha2_password,而本地驱动版本太旧不支持。解决:确认 MySQL 用户密码;或者在 MySQL 命令行里执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';换回老插件。

报错二:Unknown database 'restaurant'。原因是你忘了建库。MySQL 的CREATE DATABASE不会因为你在 Java 代码里写了一句jdbc:mysql://localhost:3306/restaurant就自动创建。解决:先手动执行第三章里的建表脚本,确保数据库和表都真实存在。

报错三:中文乱码。界面里显示的中文正常,但存进 MySQL 后变成"???"。这通常是三处编码不一致:MySQL 连接字符串的characterEncoding、表字段的字符集、IDE 的默认编码。确认三处都是utf8或utf8mb4,注意表字段单独设utf8而连接用utf8mb4才会出问题。我一般建表时统一用DEFAULT CHARSET=utf8mb4,连接字符串也用utf8mb4,从根上消除编码错位。

报错四:Public Key Retrieval is not allowed。MySQL 8.0 使用caching_sha2_password认证时,客户端第一次连接需要向服务器请求公钥。解决:连接字符串加allowPublicKeyRetrieval=true&useSSL=false。这属于 MySQL 8.0.34 版本之后的常见问题,我用 8.0 跑课设时遇到的第一道坎就是它。

报错五:Communications link failure。现象是程序跑着跑着突然数据库访问失败。原因可能是 MySQL 服务停了,或者连接空闲太久被服务端断开。开发阶段最简单的排查方法:打开命令行执行net start mysql确认服务在运行;如果是空闲断开,代码里查一下wait_timeout变量的值,默认 8 小时,但连接池里的连接可能以为它还活着,实际早已被数据库端杀掉。

6.3 一个加分小技巧:全局异常捕获,双击运行不再是黑屏

JAR 包在别人电脑上双击运行时,如果抛异常,用户只看到一个一闪而过的黑窗或者毫无反应,根本不知道发生了什么。加一个全局异常处理器,把错误信息写入日志文件,是让交付物更专业的细节。Java 的Thread.setDefaultUncaughtExceptionHandler可以拦截所有未捕获的异常:

Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { try { FileWriter writer = new FileWriter("error.log", true); writer.write("时间: " + LocalDateTime.now() + "\n"); writer.write("异常: " + throwable.toString() + "\n"); StackTraceElement[] stack = throwable.getStackTrace(); for (StackTraceElement element : stack) { writer.write("\t" + element.toString() + "\n"); } writer.close(); } catch (IOException e) { e.printStackTrace(); } JOptionPane.showMessageDialog(null, "程序遇到错误,详情请查看 error.log 文件"); });

这段代码放在main()方法里、启动界面的第一时间执行。加上之后,用户双击运行即使出错,也至少能弹出一个提示框,日志文件会记录完整的堆栈信息。我自己做项目交付时,单位领导双击 JAR 包看到"无反应"时心情都不太好,加了异常捕获之后至少能定位问题,不用靠猜。

6.4 数据备份:别让 MySQL 数据跟着重装系统一起消失

开发过程中有件事现在不做,答辩前一周必然会后悔——备份数据库。每次改完表结构、加完测试数据,顺手执行一次导出,成本只有几秒钟:

mysqldump -u root -p restaurant > restaurant_backup.sql

需要还原时执行:

mysql -u root -p -e "source restaurant_backup.sql"

这个命令可以手动跑,也可以写成一个.bat脚本放在项目根目录,双击就能生成带日期后缀的备份文件。如果你用 Windows 自带的任务计划程序,甚至可以做到每天自动备份。这个习惯我是在一次手滑执行了DROP DATABASE之后形成的,从那以后我所有带数据库的项目都会顺手做备份——数据没了,代码写得再好也只是个空壳。

这套系统的完整链路到这里就闭环了:从数据库建模到 DAO 层封装,从 Swing 界面绑定到事务处理,最后打包成可执行文件 —— 每一步踩过的坑基本都是我在实际带项目时遇到过的,希望帮到你。照这个思路做下来,不管答辩还是实际部署,心里都更有底。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询