news 2026/10/4 17:02:02

超市管理系统实战:JDBC连接MySQL与Swing界面避坑全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
超市管理系统实战:JDBC连接MySQL与Swing界面避坑全解析

简介:一套基于 Java Swing 与 MySQL JDBC 开发的超市管理系统完整项目,面向 Java 学习者与课程设计场景。系统围绕商品管理核心需求,实现了商品信息展示、按名称精确查询、添加商品等操作,并配套 JTable、JTextField、JButton 的交互布局与未找到商品的弹窗提示,覆盖窗体设计、事件处理、SQL 查询、JDBC 连接等关键知识点,适合作为课设作业、期末项目或 Swing 入门练手参考。压缩包共 36 个文件、约 979KB,包含 9 个 Java 源文件、21 个 class 编译文件、JDBC 驱动 jar 包、dbgoods.sql 数据库脚本及工程配置文件,源码、依赖与脚本齐全,便于直接导入 IDE 运行或逐行研读。目前已有 3414 人学习下载,整体结构清楚,对想快速搭建超市管理类桌面应用或理解 Swing + MySQL 开发流程的读者尤其有参考价值。

1. 超市管理系统怎么选型:单机时代为什么还用 Swing + MySQL JDBC

“超市管理系统”这类标题在教程仓库里出现频率极高,但真正把它写到能交、能答辩、能放进简历的人并不多。卡住大多数人的不是 Swing 布局拖拽,而是 JDBC 连 MySQL 时那一整排报错:驱动类找不到、SSL 连接失败、中文写进去变问号。反直觉的结论是,哪怕现在 Spring Boot 已经烂大街,对单人练手和课程设计这种规模,Swing + MySQL JDBC 反而是最短的落地路径——不引入框架,把 Java 基础、SQL、面向对象设计一次串完。

这套方案能解决什么?一个在单机或小局域网内跑起来的收银后台:商品维护、库存出入、销售开单、按条件查询报表。适合谁?刚学完 Java SE、想拿一个“看得见窗口”的项目练手的人,以及需要一份能现场讲清楚原理的课程设计或毕设。本文按 JDBC 连接、建库建表、Swing 对接、避坑排查一路写下来,代码和参数都可复制,照着改就能跑。

先明确一点,这个标题的价值不在“系统”有多大,而在 JDBC 这层。你能把驱动加载、连接建立、参数化查询、事务回滚、资源关闭讲明白,界面反而成了次要的东西。下面就从最容易让人翻车的连接层开始。

2. JDBC 连接 MySQL:六个标准步骤与一整套环境参数

2.1 为什么不用 Hibernate/MyBatis:JDBC 才是这个系统的地板

很多新手拿到这个标题第一反应是“能不能用 MyBatis”,我的建议是不要。这个项目的核心操作就是单表 CRUD 和两三张表的关联查询,SQL 都不复杂,引入 ORM 不仅多一个学习成本,还会让答辩变成“讲讲 MyBatis 的原理”而不是“讲讲你写的系统”。你自己用 JDBC 写的每一行 SQL,都是能现场说清来龙去脉的。

JDBC 的访问流程就六步:加载驱动、DriverManager 建立连接、创建 PreparedStatement、执行并拿到 ResultSet(或影响行数)、处理结果、关闭资源。前四步是套路,后两步最容易写漏。先掌握这六步,再谈连接池和框架,这是我认为做这个项目最合理的顺序。

另外一个现实原因:Swing 是胖客户端,每个窗口都可能独立发起查询。这种场景下 JDBC 直连的失败模式是“慢”和“连接被断开”,而如果你把 ORM 引进来,失败模式就变成“配置项不对”“版本冲突”“懒加载报错”,排查链路反而更长。JDBC 的黑匣子最小,适合做地基。

2.2 JDBC 标准六步与连接参数:一个最小可跑的查询代码

先给一个最小可跑的 JDBC 查询。这段代码覆盖了连接、参数化查询、结果集读取和自动关资源四个关键点。

import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; public class JdbcMinDemo { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/supermarket" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai" + "&useSSL=false&allowPublicKeyRetrieval=true"; String user = "root"; String password = "123456"; String sql = "SELECT id, name, price FROM product WHERE id = ?"; try (Connection conn = DriverManager.getConnection(url, user, password); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, 1001); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { System.out.printf("id=%d, name=%s, price=%.2f%n", rs.getInt("id"), rs.getString("name"), rs.getDouble("price")); } } } catch (Exception e) { e.printStackTrace(); } } }

这段代码用了 try-with-resources,它会在 try 块结束时按逆序自动关闭 Connection、PreparedStatement、ResultSet,比在 finally 里手工 close 干净得多,也不会出现关错顺序的问题。注意ps.setInt(1, 1001),这里第一个参数是占位符?的位置,从 1 开始;SQL 里用了参数化查询而不是字符串拼接,这是防注入的基本操作,后面避坑章节会再提。

连接串里的四个参数是这个项目的“血泪经验”:useUnicode=true&characterEncoding=utf8保证中文不乱码;serverTimezone=Asia/Shanghai是 MySQL 8.0 驱动的强制要求,不写会直接报时区错误;useSSL=false关闭 SSL 警告;allowPublicKeyRetrieval=true解决 MySQL 8.0 默认认证插件 caching_sha2_password 下的公钥获取报错。缺一个,你都会在连库这一步卡住。

驱动类名也要注意,MySQL 8.0 对应的是com.mysql.cj.jdbc.Driver,老教程里常见的com.mysql.jdbc.Driver在 8.0 下已不推荐。以下是不同环境组合的选型参考,按你的 JDK 和 MySQL 版本来选:

环境组合驱动 jar 命名规则驱动类名连接串要点
MySQL 5.7 + JDK 8mysql-connector-java-5.1.x.jarcom.mysql.jdbc.Driver可以不写 serverTimezone
MySQL 8.0 + JDK 8mysql-connector-j-8.0.x.jarcom.mysql.cj.jdbc.Driver必须带 serverTimezone 和 allowPublicKeyRetrieval
MySQL 8.0 + JDK 11/17mysql-connector-j-8.0.x.jarcom.mysql.cj.jdbc.Driver同上,javac 编译时需把 jar 加进 classpath

先说环境准备:JDK 必须配置好 JAVA_HOME 和 PATH,MySQL 装 5.7 或 8.0 都可以,这里按 8.0 为主讲。下载 mysql-connector-j 的 jar 后,在 IDE 里选中它右键 Add as Library,命令行编译则用javac -cp ".;mysql-connector-j-8.0.x.jar" YourClass.java。

2.3 连接池要不要上:单机 Swing 项目的取舍

桌面程序不像 Web 服务有高并发,我的习惯是:先直连跑通,再用连接池优化。如果你的系统只是自己用、数据量不超过几千条,DriverManager.getConnection()每次新建连接完全够用,MySQL 连接很轻量,这个规模下性能差距体感为零。但如果你做了多窗口并发查询,或者以后想把业务层迁到 Web 端,建议直接上 HikariCP。

下面是 HikariCP 的一个最小用法,核心就四行配置:

HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/supermarket?useSSL=false&serverTimezone=Asia/Shanghai"); config.setUsername("root"); config.setPassword("123456"); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(3000); HikariDataSource dataSource = new HikariDataSource(config);

maximumPoolSize=10对桌面应用已经很宽裕,minimumIdle=2保持两个热连接,connectionTimeout=3000表示 3 秒内拿不到连接就报错,避免界面无限等待。这里要提醒一个坑:连接池内的线程默认不是守护线程,Swing 窗口关闭后进程可能退不掉,你需要在程序退出时调用dataSource.close(),否则会在控制台看到进程一直挂在那里。

3. 数据库表设计与读写落地:从建库建表到第一个商品查询

3.1 五张核心表怎么拆:商品、分类、供应商、库存、销售

做超市系统最容易犯的错是把所有字段塞进一张大表,比如直接建一个goods表,里面带上分类名、供应商名甚至销售记录。这样做查询确实省事,但改一个分类名要更新所有商品行,销售历史也没法单独统计。我建议按职责拆成六张表:

表名作用关键字段
category商品分类id, name, sort_no
supplier供应商id, name, contact, phone
product商品主表id, barcode, name, category_id, supplier_id, price, stock_qty, warn_qty, status
stock_record库存流水id, product_id, change_type, change_qty, remark, create_time
sale_order销售订单主表id, order_no, total_amount, operator, sale_time
sale_order_item销售订单明细id, sale_order_id, product_id, product_name, price, quantity, subtotal

商品表单独拆出来的理由是它承担了最频繁的查询和更新;库存不直接改product.stock_qty,而是通过stock_record流水来驱动,这样盘点、报损、入库都有据可查;销售订单主表和明细表是一对多关系,一次结账对应多条商品记录。注意product_name在明细表里冗余了一份,这是故意为之——商品改名或下架后,历史销售单仍然能显示当时的商品名,属于反范式但实用的设计。

3.2 建库建表 SQL:字符集与外键索引怎么定

下面这段 SQL 可以直接在 Navicat 或命令行执行。建表时字符集统一用 utf8mb4,避免以后导入数据时遇到“Incorrect string value”错误。

CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE supermarket; CREATE TABLE category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, sort_no INT DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE supplier ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, contact VARCHAR(50), phone VARCHAR(20) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE product ( id INT AUTO_INCREMENT PRIMARY KEY, barcode VARCHAR(30), name VARCHAR(100) NOT NULL, category_id INT, supplier_id INT, price DECIMAL(10,2) DEFAULT 0.00, stock_qty INT DEFAULT 0, warn_qty INT DEFAULT 10, status TINYINT DEFAULT 1, KEY idx_product_name (name), UNIQUE KEY uk_barcode (barcode) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE stock_record ( id INT AUTO_INCREMENT PRIMARY KEY, product_id INT NOT NULL, change_type TINYINT COMMENT '1入库 2出库 3盘点调整', change_qty INT NOT NULL, remark VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE sale_order ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, total_amount DECIMAL(10,2) DEFAULT 0.00, operator VARCHAR(50), sale_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE sale_order_item ( id INT AUTO_INCREMENT PRIMARY KEY, sale_order_id INT NOT NULL, product_id INT, product_name VARCHAR(100), price DECIMAL(10,2), quantity INT, subtotal DECIMAL(10,2), KEY idx_item_order (sale_order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段 DDL 里有几个选择值得说明。第一,不建物理外键,只保留逻辑关联字段,比如product.category_id。原因是 Swing 桌面项目里“删除商品”“删除分类”很频繁,物理外键会限制删除顺序,报错信息也让人一头雾水;应用层用代码保证引用关系,更贴近实际开发习惯。第二,价格用DECIMAL(10,2)不用 FLOAT,避免浮点误差导致对账不平。第三,product表的uk_barcode是唯一索引,扫码枪录入时重复条码直接报错;idx_product_name是普通索引,支撑商品名的模糊查询。

3.3 JDBC 工具类封装:连接管理与事务边界

每次都写一遍DriverManager.getConnection会很啰嗦,我会封装一个 DBUtil,把连接串、关资源、事务提交回滚都收进去。代码如下:

import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/supermarket" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "123456"; private DBUtil() {} public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void commit(Connection conn) { if (conn != null) { try { conn.commit(); } catch (SQLException ignore) {} } } public static void rollback(Connection conn) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignore) {} } } public static void close(Connection conn, java.sql.Statement st, java.sql.ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignore) {} } if (st != null) { try { st.close(); } catch (SQLException ignore) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignore) {} } } }

getConnection()每次新建连接,适合单机项目;如果前面上了连接池,只需要把这里改成从 HikariDataSource 拿连接,业务代码不用动。rollback和commit是给多表事务准备的,比如销售开单要同时插入sale_order和sale_order_item:

Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 插入主表,拿到自增主键,再插入明细 DBUtil.commit(conn); } catch (SQLException e) { DBUtil.rollback(conn); throw e; } finally { DBUtil.close(conn, null, null); }

setAutoCommit(false)之后,所有 SQL 都在同一个事务里,只有 commit 才会真正落库;任何一步抛异常就整体回滚,不会出现“订单主表有了、明细没了”的脏数据。这是 JDBC 事务的最低成本写法,比用连接池的编程式事务好理解得多。

4. 从登录页到商品台账:Swing 界面与 DAO 层如何对接

4.1 Swing 为什么默认“卡死”:EDT 线程与 JDBC 阻塞

新手做完界面后的第一个“翻车现场”是:点查询按钮,窗口直接失去响应,拖都拖不动。原因在于 Swing 是单线程模型,所有 UI 更新都在 Event Dispatch Thread(EDT)上执行,而你在按钮的 ActionListener 里直接执行了 JDBC 查询,数据库一慢,EDT 就被堵死,整个窗口冻结。

常见做法分两种。数据量小、查询在几百毫秒内完成的,直接在监听器里查询,然后用SwingUtilities.invokeLater更新界面;数据量大或不确定耗时的,用 SwingWorker 把 JDBC 扔到后台线程。下面是 SwingWorker 的标准姿势:

JButton searchBtn = new JButton("查询"); searchBtn.addActionListener(e -> { String keyword = keywordField.getText().trim(); new SwingWorker<List<Product>, Void>() { @Override protected List<Product> doInBackground() { return productDao.search(keyword); // 后台线程执行 JDBC } @Override protected void done() { try { List<Product> list = get(); refreshTable(list); // EDT 上更新表格 } catch (Exception ex) { JOptionPane.showMessageDialog(frame, "查询失败:" + ex.getMessage()); } } }.execute(); });

doInBackground()里跑的是耗时的数据库操作,done()里的get()拿结果并刷新表格,这一步已经回到了 EDT,可以直接操作组件。注意按钮事件本身是在 EDT 上被触发的,SwingWorker 相当于开辟了一条旁路,JDBC 的阻塞不会影响 UI 响应。这是 Swing 桌面项目里最值得养成的习惯。

4.2 登录页与主窗口:先让界面能跑起来

登录窗口是用户见到的第一屏,代码不复杂,但有几个细节会影响体验。下面是核心流程:

public class LoginFrame extends JFrame { private JTextField userField; private JPasswordField passField; private UserDao userDao = new UserDao(); public LoginFrame() { setTitle("超市管理系统 - 登录"); setDefaultCloseOperation(EXIT_ON_CLOSE); setSize(360, 210); setLocationRelativeTo(null); // 这里省略 GridBagLayout 布局代码,核心逻辑在 doLogin() loginBtn.addActionListener(e -> doLogin()); passField.addActionListener(e -> doLogin()); // 回车触发登录 } private void doLogin() { String username = userField.getText().trim(); String password = new String(passField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "账号或密码不能为空"); return; } User user = userDao.login(username, password); if (user == null) { JOptionPane.showMessageDialog(this, "账号或密码错误"); passField.setText(""); return; } new MainFrame(user).setVisible(true); dispose(); } }

JPasswordField.getPassword()返回的是char[]而不是 String,这是刻意的设计:String 在 Java 中是不可变的,会停留在常量池里,而 char[] 用完后可以手动覆盖清零。登录按钮绑定了 ActionListener,密码框也绑了同一个逻辑,这样用户输完密码按回车就能登录,不用拿鼠标去点按钮。用户名校验、逻辑校验都在 UI 层先做,减少无意义的数据库请求。

主窗口我更倾向于用JFrame + JTabbedPane,把“商品管理”“库存管理”“销售开单”做成三个 Tab,而不是用多个 JFrame 来回弹。Tab 切换本身不销毁界面,数据状态保持得更好。菜单栏放“系统管理”,里面加退出、修改密码、数据备份,这样整体结构清晰,也方便划分 DAO 的职责边界。

4.3 把 ResultSet 渲染到 JTable:两种不翻车写法

把查询结果显示到 JTable,最常见的错误写法是直接拿 ResultSet 去填表格。ResultSet 是“向前单向游标”,而且依赖连接保持打开,一旦连接关闭,数据就没了。正确做法是 DAO 层先把它变成实体对象列表,UI 层再填充到表格模型。

public class ProductDao { public List<Product> search(String keyword) { List<Product> list = new ArrayList<>(); String sql = "SELECT id, barcode, name, price, stock_qty FROM product " + "WHERE name LIKE ? ESCAPE '\\\\' ORDER BY id DESC"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + escapeLike(keyword) + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setBarcode(rs.getString("barcode")); p.setName(rs.getString("name")); p.setPrice(rs.getDouble("price")); p.setStockQty(rs.getInt("stock_qty")); list.add(p); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }

DAO 返回List<Product>之后,UI 层就只跟实体类打交道,不用关心 ResultSet 的游标状态。填表格时用DefaultTableModel.addRow()一行行加,刷新前先setRowCount(0)清空旧数据:

private void refreshTable(List<Product> products) { DefaultTableModel model = (DefaultTableModel) productTable.getModel(); model.setRowCount(0); // 清空旧数据 Object[] row = new Object[5]; for (Product p : products) { row[0] = p.getId(); row[1] = p.getBarcode(); row[2] = p.getName(); row[3] = p.getPrice(); row[4] = p.getStockQty(); model.addRow(row); } }

代码里的ESCAPE '\\\\'是为了处理用户输入%或_的情况。这两个字符在 LIKE 里是通配符,如果用户搜索“100%纯棉”,不转义就查不到任何结果。DAO 里要做一次escapeLike处理,把用户输入中的%、_、\前都加上反斜杠,再用ESCAPE告诉 MySQL。这个坑很隐蔽,但遇到了就会觉得特别玄学。

5. 避坑与排查:MySQL 8.0、中文乱码、ClassNotFound 的现场记录

5.1 现象:启动就报 ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因几乎只有一个:mysql-connector-j 的 jar 没有进入运行时 classpath。IDE 里写了 import、代码里也没有语法错误,但运行时就找不到类。还有一种情况是从老教程复制了com.mysql.jdbc.Driver,这个类名在 MySQL 8.0 驱动里已经被移除了。

解决分两种路径。Maven 工程检查 pom.xml 里有没有依赖声明;非 Maven 工程去 Project Structure 或 Build Path 里确认 jar 已被 Add as Library。命令行运行时,把 jar 放在项目 lib 目录下,用java -cp "lib/*;classes" com.example.Main启动。排错时可以先用一个最简单的类只打印驱动的版本信息,确认 classpath 没问题再继续写业务代码。

5.2 现象:连接 MySQL 8.0 报 SSL 连接错误 / Public Key Retrieval is not allowed

这个报错的字面意思是“不允许检索公钥”。MySQL 8.0 默认认证插件是 caching_sha2_password,客户端首次连接时需要通过 RSA 公钥交换密码,而 Connector/J 出于安全考虑默认不允许自动获取公钥,两边的默认值正好撞上了。

解决就是在连接串里加两个参数:useSSL=false&allowPublicKeyRetrieval=true。前者关闭 SSL 加密(本地局域网内可以接受),后者放开公钥获取限制。如果你用的是连接池,也要在每个连接串里带上这两个参数,只改一处没用。这个问题在 MySQL 5.7 下不存在,所以从 5.7 迁到 8.0 的项目最容易踩。

5.3 现象:中文写入 MySQL 变成问号

现象很统一:Swing 界面里显示正常,查数据库看到中文全是??。这个问题的“玄学”之处在于,它既不是 Java 的问题,也不是 MySQL 的问题,而是连接层、库表字符集、客户端变量三者没对齐。

按顺序排查三步。第一步,看建库 SQL 是不是用了utf8mb4,如果库是latin1,后面怎么配都白搭;第二步,连接串里必须带useUnicode=true&characterEncoding=utf8,让 JDBC 知道用 UTF-8 编码发出字符串;第三步,执行SHOW VARIABLES LIKE 'character%',确认character_set_client和character_set_results都是 utf8mb4。Java 内部的字符串本身就是 Unicode,所以问题只会出在这三个环节的某一处,逐项对照就能定位。

5.4 现象:点击查询按钮后窗口直接“未响应”

这个我在第 4 章已经说了原因:JDBC 查询阻塞了 EDT。这里再补充一个判断技巧:如果窗口能拖动,但点击按钮没反应,通常是监听器里抛了异常被吞掉;如果窗口完全拖不动,那就是线程阻塞,基本可以断定是 EDT 上跑了耗时操作。

解决的办法是给所有涉及 JDBC 的监听器都套上 SwingWorker,或者退一步用new Thread(() -> { ... }).start()加SwingUtilities.invokeLater手动切回 EDT。前者代码更规整,后者更容易理解。界面卡死是最打击新手信心的问题,我的原则是:凡是有数据库访问的按钮,一律不直接写查询逻辑,先把异步包装写好。

5.5 现象:插入销售单只写了主表,明细全部丢失

销售开单会同时操作sale_order和sale_order_item两张表,新手最容易犯的错是分两次连接执行,第二次失败后第一次已经提交,数据就对不上了。另一个隐蔽问题是获取新订单 ID 时用了SELECT MAX(id),这在单机下勉强能用,但一旦有并发就很会翻车。

正确做法是第 3 章给出的那套setAutoCommit(false)加commit/rollback的模板,并且插入主表后用PreparedStatement.getGeneratedKeys()拿自增主键:

try (PreparedStatement ps = conn.prepareStatement(insertOrderSql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, orderNo); ps.setBigDecimal(2, totalAmount); ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { long orderId = keys.getLong(1); // 再用 orderId 插入明细 } } }

Statement.RETURN_GENERATED_KEYS告诉驱动把生成的主键返回回来,这才是拿自增 ID 的正确姿势。凡是“先查再拼”的取号方式都别用,事务保证不了并发场景下的正确性。

6. 进阶验证:登录加密、分页查询与可交付 jar

6.1 登录密码不能明文存:加盐哈希改造

登录模块现在还是明文比对,交作业前至少改成加盐哈希。简单做法是注册时生成随机盐,存salt和SHA-256(salt + password)两列;登录时取出盐,重算哈希再比对。这样数据库泄露也不会直接暴露密码明文。Swing 项目里不需要引入 BCrypt 这种重量级库,JDK 自带的MessageDigest够用。

MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest((salt + password).getBytes(StandardCharsets.UTF_8));

6.2 分页查询:LIMIT 参数化与简单的上一页/下一页按钮

商品数量过千后,一次性加载全部数据到 JTable 会变卡。给查询加上LIMIT ? OFFSET ?参数化分页,每页 20 条,公式是offset = (page - 1) * pageSize。界面底部放上一页、下一页和“当前第 x 页”标签,翻页时只请求当前页数据。SQL 里记得ORDER BY id DESC,不然翻页时数据顺序不稳定,你会看到“上一页和下一页有重复数据”的怪现象。

6.3 打成可执行 jar:让项目能双击跑起来

最后一步是把项目打成带依赖的 jar。IDE 里用 Artifacts 选 “JAR from modules with dependencies”,主类选LoginFrame;Maven 工程用 maven-shade-plugin 做 fat jar。关键检查点是:mysql-connector-j 的 jar 有没有被包含进去,没包含的话换台机器就报 ClassNotFoundException。打包后先在本地双击运行一次,确认登录窗口能弹出来,再拿去交付。目标机器只装 JDK 即可,MySQL 装好并执行过建库脚本就能用。

我现在做这类项目的习惯是:每写完一个 DAO 方法,先写一个main方法调一遍,再去接界面。界面出了问题第一反应先怀疑 EDT,再怀疑 SQL 条件,最后才怀疑驱动。这套顺序帮我省了无数排查时间,希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 17:00:34

AI日报系统架构:分层流水线设计与工程化实践

1. 项目概述&#xff1a;这不是一份新闻简报&#xff0c;而是一套可复用的AI内容生产流水线“AI 日报 2026-09-29”——光看这个标题&#xff0c;很多人第一反应是某家科技媒体发布的当日AI领域快讯合集。但作为连续三年搭建、迭代、交付过17个不同行业AI内容自动化系统的从业者…

作者头像 李华
网站建设 2026/10/4 17:00:18

物流API高可用架构(进阶篇)——工程师必备知识

本文深入探讨物流API高可用架构&#xff08;进阶篇&#xff09;&#xff0c;涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。在行业案例与方案领域&#xff0c;物流API高可用架构&#xff08;进阶篇&#xff09;是开发者和技术负责人持续关注的核心议题。本…

作者头像 李华
网站建设 2026/10/4 16:54:07

液冷板热阻计算与流阻优化的实战方法

液冷板热阻计算与流阻优化的实战方法 摘要 液冷板是数据中心和新能源汽车热管理系统的核心传热元件&#xff0c;其热阻和流阻特性决定了整个散热系统的性能上限和运行能效比&#xff08;COP&#xff09;。本文从热阻分解模型出发&#xff0c;介绍液冷板热阻的理论计算方法&…

作者头像 李华
网站建设 2026/10/4 16:52:20

口令实验:用真实操作探测系统状态污染

1. 从“口令实验”开始&#xff1a;我们到底在测试什么&#xff1f;“共享状态&#xff0c;隔离问题”——这八个字乍看像一句技术口号&#xff0c;实则是一道精准的手术刀口&#xff0c;切开了现代软件系统里最常被忽视、也最容易出事的底层逻辑。我第一次在团队内部做这个口令…

作者头像 李华
网站建设 2026/10/4 16:51:17

OpenShell 完全指南:还原经典开始菜单与提升 Windows 效率

相信很多折腾过 Windows 界面的朋友&#xff0c;对OpenShell这个名字都不陌生。它其实就是当年大名鼎鼎的 Classic Shell 的社区延续版——一个开源免费的 Windows 外壳增强工具&#xff0c;核心功能是把 Win10/Win11 那个“重新设计的开始菜单”替换成你熟悉的经典样式&#x…

作者头像 李华
网站建设 2026/10/4 16:50:58

MR25H40CDF+PIC24FJ256GA110工业存储方案解析

1. 项目概述&#xff1a;为什么在工业现场非得用 MR25H40CDF 配 PIC24FJ256GA110 做数据存储&#xff1f;在工厂产线调试PLC信号采集模块时&#xff0c;我亲眼见过三台设备因掉电导致关键工艺参数全丢——不是SD卡写入失败&#xff0c;也不是Flash擦写寿命耗尽&#xff0c;而是…

作者头像 李华