简介:基于Java Swing和MySQL的仓库管理系统设计源码,是一套面向开发者、学生和企业技术人员的完整仓储管理系统实现,目标是用系统化方式解决传统人工记账效率低、库存信息不透明的问题。系统覆盖商品入库、出库、库存盘点、报表统计、权限控制与操作日志等功能,源码按dao、entity、frame、utils等包划分,层次清楚,适合二次开发和教学演示。压缩包共39个文件,大小8.45MB,包含15个Java源文件、10个JAR依赖包、7个XML配置文件,以及Git忽略文件、PNG图标、IML项目文件与说明文档,各文件类型分工明确。目前已有118人学习,可用于课程设计、毕业设计或中小型仓储管理项目参考。读者拿到的是完整可运行的源码,既能理解Swing界面构建与MySQL数据交互,也能借鉴其事务处理和权限管理思路,便于在此基础上扩展新功能。
1. 基于Java Swing和MySQL的仓库管理系统,这套源码到底在解决什么问题
基于Java Swing和MySQL的仓库管理系统,在Java课程设计、毕业设计和中小公司内部工具里反复出现。它不新,但很耐做:一套界面、几张业务表、一连串事务,刚好把Java基础的关键知识点串联起来。一家做五金批发的小仓库,每天进出几十单货,老板只想知道某个螺丝还有多少,上Web系统太重,Excel又容易改错,这正是这套组合的典型栖息地。
这套"设计源码"要交付的不只是能跑的界面,而是一套能讲清楚的方案:数据库建模怎么才能不产生脏库存,Swing窗口怎么组织才不至于开十几个窗口乱飞,入库出库怎么保证库存数字不花。适合三类人:做课程设计的学生、想练桌面端CRUD的Java新手、需要给内部库房做工具又不想上Web的工程师。按我实际落地的做法,从建表、写界面、做业务到打包交付,一步步拆开讲。
2. 先建模再写界面:核心表结构与JDBC连接参数怎么定
2.1 为什么表结构比界面更重要:从业务流倒推数据模型
很多新手拿到仓库管理系统这个题目,第一反应是打开Swing拖控件。这个顺序是反的。界面只是壳,仓库系统的灵魂在数据模型。先想清楚业务流:采购入库带来库存增加,销售出库带来库存减少,盘点把账面数修正为实盘数。每一笔变动都要留有单据,否则三个月后想查这批货什么时候进的、谁经手的,根本无从下手——这是仓库管理系统和普通CRUD练习最大的区别。
最常见的翻车设计是只建一张goods表,里面放一个quantity字段,入库就加、出库就减,不做单据、不留流水。这种设计demo跑起来很爽,但库存一旦错了,你根本不知道错在哪一步。所以我一般会按"单据头 + 单据明细 + 库存余量"三层来建模:单据记录业务事实,库存只是可变的计算结果。这个思路直接决定了后面所有mysql事务处理的写法——库存是结果,单据是事实,两者必须一起提交或一起回滚。
2.2 建库建表SQL:字段类型、唯一键和索引的取舍
建库用utf8mb4字符集,引擎统一InnoDB。MyISAM不是不能用,但它不支持事务和行级锁,而仓库系统最怕的恰恰就是操作到一半失败,库存却加了一半。
CREATE DATABASE IF NOT EXISTS warehouse_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE warehouse_system; -- 用户表 CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT 'MD5密文', real_name VARCHAR(50) COMMENT '真实姓名', role VARCHAR(20) NOT NULL DEFAULT 'staff' COMMENT 'admin/staff', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户'; -- 仓库表 CREATE TABLE warehouse ( id INT AUTO_INCREMENT PRIMARY KEY, warehouse_name VARCHAR(50) NOT NULL, address VARCHAR(200), status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='仓库'; -- 商品表 CREATE TABLE goods ( id INT AUTO_INCREMENT PRIMARY KEY, goods_code VARCHAR(32) NOT NULL UNIQUE COMMENT '商品编码', goods_name VARCHAR(100) NOT NULL, category_id INT COMMENT '商品分类,可关联category表', spec VARCHAR(100) COMMENT '规格型号', unit VARCHAR(20) COMMENT '单位:件/箱/千克', safety_stock INT NOT NULL DEFAULT 0 COMMENT '安全库存', status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品'; -- 库存表:一个商品在一个仓库只有一条记录 CREATE TABLE stock ( id INT AUTO_INCREMENT PRIMARY KEY, goods_id INT NOT NULL, warehouse_id INT NOT NULL, quantity INT NOT NULL DEFAULT 0, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_goods_warehouse (goods_id, warehouse_id), CONSTRAINT fk_stock_goods FOREIGN KEY (goods_id) REFERENCES goods (id), CONSTRAINT fk_stock_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouse (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存'; -- 入库单头 CREATE TABLE stock_in_order ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '单号', warehouse_id INT NOT NULL, supplier_name VARCHAR(100) COMMENT '供应商,简单场景直接存名字', operator_id INT NOT NULL COMMENT '操作人', remark VARCHAR(255), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_warehouse_time (warehouse_id, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入库单'; -- 入库明细 CREATE TABLE stock_in_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, goods_id INT NOT NULL, quantity INT NOT NULL COMMENT '入库数量', price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '单价', KEY idx_order (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入库明细';出库的两张表和入库对称,结构一样,只把order_no前缀改成OUT、语义从入库变成出库,这里不重复贴。表字段的取舍有几个点值得说透。
第一,金额一律用DECIMAL(10,2),不要用DOUBLE或FLOAT。浮点数在二进制里表示不精确,0.1加0.2这种账目误差在财务数据里是事故,DECIMAL是十进制字符串存储,算钱才安心。第二,stock表上的联合唯一键(goods_id, warehouse_id)是整套设计的基石,它保证同一个商品在同一个仓库里只有一行库存记录,后面的upsert才能用。第三,(warehouse_id, created_at)是给按时间范围查某仓库流水准备的,仓库系统查单基本都是这个维度,比单字段索引更贴合查询路径。
提示:MySQL 5.7 和 8.0 的驱动类名不一样,5.7 用 com.mysql.jdbc.Driver,8.0 用 com.mysql.cj.jdbc.Driver。项目里如果两个 jar 同时存在,会互相干扰。
核心表汇总一下,方便你建表时对照:
| 表名 | 用途 | 关键字段 | 设计要点 |
|---|---|---|---|
| user | 登录与权限 | username, password, role | 密码存MD5或bcrypt,不存明文 |
| warehouse | 仓库主数据 | warehouse_name, status | 多仓是库存唯一键的一部分 |
| goods | 商品主数据 | goods_code, goods_name, spec | 编码唯一,分类可独立成表 |
| stock | 库存余量 | goods_id, warehouse_id, quantity | 联合唯一键,upsert的基础 |
| stock_in_order / stock_in_item | 入库单据 | order_no, goods_id, quantity, price | 一单头对多明细 |
| stock_out_order / stock_out_item | 出库单据 | 与入库对称 | 出库时校验库存 |
| inventory_check_order / item | 盘点单据 | goods_id, diff_quantity | 差异调整留痕 |
排序字段多说一句:所有流水单据的列表查询,默认按created_at倒序,SQL里写ORDER BY created_at DESC,别让用户翻到最后一页才能看到刚录的单。这种看似不起眼的Mysql排序细节,恰恰是演示时最容易被老师或老板点出来的体验问题。
2.3 JDBC连接:驱动版本、URL参数和数据库配置
表建好了,接下来是Java这边怎么连MySQL。现在主流是MySQL 8.0,驱动包用mysql-connector-j,驱动类名是com.mysql.cj.jdbc.Driver。如果你还在用5.7,驱动类名是com.mysql.jdbc.Driver——很多ClassNotFoundException就是这么来的,驱动版本和类名对不上。
Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/warehouse_system" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; Connection conn = DriverManager.getConnection(url, "root", "123456");URL上几个参数是血泪经验换来的。useSSL=false关掉SSL握手,本机开发能少一堆警告和偶发超时;serverTimezone=Asia/Shanghai解决MySQL 8.0的时区报错,不设置的话连接时直接抛"The server time zone value"异常,新手第一次看到会以为驱动坏了;characterEncoding=utf8配合建库时的utf8mb4,保证中文从界面到数据库一路不乱码。
一个常见做法是把连接参数抽到db.properties里,用一个JdbcUtil集中读取,而不是在每个DAO里硬编码。理由很现实:课程设计答辩或小团队交付时,数据库密码、IP大概率要现场改,硬编码意味着重新编译打包,抽到配置文件则改一行就能跑。
| URL参数 | 推荐值 | 解决什么问题 |
|---|---|---|
| useSSL | false | 本地/内网免SSL握手,少告警 |
| serverTimezone | Asia/Shanghai | 报时区异常、日期偏差 |
| characterEncoding | utf8 | 中文乱码 |
| allowPublicKeyRetrieval | true | MySQL 8的caching_sha2_password认证需要 |
连接池这块,纯Swing桌面应用、并发用户十几个以内,直接用DriverManager每次开连接就行,不必上连接池。真到了几十人同时用的规模,桌面端本身就不合适了,应该考虑Web化。别在课程设计里为了炫技引入三四个框架,老师问起来反而难自圆其说。
3. Swing界面组织:登录窗口、主框架与面板切换的写法
3.1 登录窗口:组件布局和回车触发登录
Swing的界面代码很多人写得又臭又长,问题多半出在没用布局管理器,而是setBounds硬拼坐标。硬拼坐标在窗口一拉伸、换台高分屏就乱成一团。我一般用GridLayout或GridBagLayout,配合BorderFactory.createEmptyBorder留边距,界面在不同分辨率下都能保持整洁。
public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; public LoginFrame() { super("仓库管理系统-登录"); setSize(380, 240); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); setResizable(false); JPanel panel = new JPanel(new GridLayout(3, 2, 8, 8)); panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); panel.add(new JLabel("用户名:")); usernameField = new JTextField(16); panel.add(usernameField); panel.add(new JLabel("密码:")); passwordField = new JPasswordField(16); panel.add(passwordField); JButton loginBtn = new JButton("登录"); JButton exitBtn = new JButton("退出"); panel.add(loginBtn); panel.add(exitBtn); add(panel); loginBtn.addActionListener(e -> doLogin()); passwordField.addActionListener(e -> doLogin()); exitBtn.addActionListener(e -> System.exit(0)); } }几个细节说清楚。setLocationRelativeTo(null)把窗口居中,演示时不会出现在屏幕角落显得业余。JPasswordField要单独绑定回车事件,因为用户习惯输完密码直接按回车,只绑登录按钮的话每次还得去点鼠标。密码用getPassword()拿char[]而不是getText()拿String,这是Swing的官方建议——String在常量池里不可控,char[]用完可以清掉。课程设计里没人在意这个,但面试官可能会问。
3.2 登录校验:密码加盐哈希而不是明文入库
登录逻辑的核心是查不到就留在登录页,查到才放行。普通做法是用户名加密码一起查,返回null就弹错误提示。
private void doLogin() { String username = usernameField.getText().trim(); String password = new String(passwordField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "用户名和密码不能为空"); return; } User user = userDao.findByUsernameAndPassword(username, Md5Util.encode(password)); if (user == null) { JOptionPane.showMessageDialog(this, "用户名或密码错误"); passwordField.setText(""); return; } dispose(); new MainFrame(user).setVisible(true); }关于密码存储,简单项目用MD5就够了,但至少要做加盐:在原始密码后面拼一段固定盐值再哈希,避免两个同密码用户产生相同的密文。Maven里加个commons-codec,或者手写一个MessageDigest工具类都行。如果这是要长期上线的系统,建议直接上bcrypt,代价只是多一个依赖,安全性完全不在一个量级。这个点在答辩时提出来很加分,属于"我知道有风险且知道怎么补"。
3.3 主框架:JMenuBar导航加CardLayout容器切换
很多人的课程设计每个功能开一个JFrame,点入库登记弹一个窗口,点库存查询又弹一个,几分钟屏幕上就叠了五六个窗口。这种风格有两个错误:一是Alt+Tab切换混乱,二是关闭主窗口后子窗口还在,进程退不干净。正确的组织方式是一个主JFrame,中间区域放一个CardLayout容器,用菜单切换不同的业务面板。
public class MainFrame extends JFrame { private CardLayout cardLayout; private JPanel contentPanel; public MainFrame(User user) { super("仓库管理系统 - " + user.getRealName()); setSize(1000, 640); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); cardLayout = new CardLayout(); contentPanel = new JPanel(cardLayout); add(createMenuBar(user), BorderLayout.NORTH); add(contentPanel, BorderLayout.CENTER); add(createStatusBar(user), BorderLayout.SOUTH); contentPanel.add(new StockInPanel(), "stockIn"); contentPanel.add(new StockOutPanel(), "stockOut"); contentPanel.add(new StockQueryPanel(), "stockQuery"); contentPanel.add(new InventoryPanel(), "inventory"); } }菜单栏的写法是把JMenuBar、JMenu、JMenuItem逐层组装,每个JMenuItem绑定actionCommand或直接lambda,在统一的事件方法里调cardLayout.show(contentPanel, name)。面板常驻、状态不丢是这种结构最大的好处:用户在入库界面填了一半切去查库存,切回来内容还在,体验比反复重建窗口好得多。
角色权限在这里就能做了。admin登录后菜单栏多一项用户管理,staff看不到。实现很简单:登录时把User对象传进MainFrame构造函数,createMenuBar里根据user.getRole()决定是否add子菜单。多角色系统再往上走才是行级权限的活,桌面端一般不需要,知道边界就行。商品分类做成"swing带勾选框的tree"那种勾选树属于锦上添花,核心流程跑通之前不必碰。
3.4 查询面板:JTable配合DefaultTableModel的刷新套路
库存查询是所有仓库系统使用频率最高的面板,JTable的刷新方式直接影响体验。
public class StockQueryPanel extends JPanel { private JTable table; private DefaultTableModel model; public StockQueryPanel() { String[] columns = {"商品编码", "商品名称", "规格", "仓库", "库存数量"}; model = new DefaultTableModel(columns, 0) { @Override public boolean isCellEditable(int row, int column) { return false; } }; table = new JTable(model); table.setAutoCreateRowSorter(true); // 点击表头排序 add(new JScrollPane(table), BorderLayout.CENTER); } public void setRows(List<StockView> rows) { model.setRowCount(0); // 清空旧数据 for (StockView row : rows) { model.addRow(new Object[]{ row.getGoodsCode(), row.getGoodsName(), row.getSpec(), row.getWarehouseName(), row.getQuantity() }); } } }刷新表格的标准套路是setRowCount(0)加addRow循环,而不是每次都new一个DefaultTableModel再setModel。后者的副作用是会把表头的排序状态、列宽设置全部重置,用户刚调好的窗口布局一下就打回原形。isCellEditable必须重写成返回false,否则用户双击表格里的数字就能改显示内容——虽然不影响数据库,但演示时被点到很尴尬。setAutoCreateRowSorter(true)一行代码让表头支持点击排序,这是把"mysql排序"需求搬到界面上最便宜的方式。
4. 入库出库与盘点:用事务和行锁保证库存不花
4.1 入库登记:单据头、明细、库存三步同一事务
入库业务拆开是三件事:写一张入库单头、写若干条明细、给对应商品的库存加数量。三件事要么全成功,要么全失败。只写了单据忘了加库存,账面和实物对不上;加了库存单据却丢了,连对账的依据都没有。所以这三步必须包在同一个事务里。
public class StockService { public void stockIn(StockInOrder order, List<StockInItem> items) throws Exception { Connection conn = null; try { conn = JdbcUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,事务开始 int orderId = insertOrder(conn, order); // 1. 入库单头 for (StockInItem item : items) { item.setOrderId(orderId); insertItem(conn, item); // 2. 入库明细 } for (StockInItem item : items) { mergeStock(conn, order.getWarehouseId(), item.getGoodsId(), item.getQuantity()); } // 3. 库存累加 conn.commit(); // 全部成功才提交 } catch (Exception e) { if (conn != null) conn.rollback(); // 任一步失败全部回滚 throw new BusinessException("入库失败,已回滚:" + e.getMessage()); } finally { if (conn != null) conn.close(); } } }事务的开关是setAutoCommit(false)。很多人忘记这一行,导致每个SQL执行完立刻提交,后面rollback根本没用——这是数据库里多了半截单据最常见的根源。commit放在最后,前面任何一步抛异常,rollback()把已执行的写操作全部撤销。
库存累加用一个INSERT...ON DUPLICATE KEY UPDATE,一次SQL完成"没有就插入、有就累加":
INSERT INTO stock (goods_id, warehouse_id, quantity) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity);这条语句依赖第二章里那个(goods_id, warehouse_id)联合唯一键。第一次入库某商品时stock表里没有对应行,走INSERT;以后再入库,唯一键冲突,走UPDATE把数量累加。比起先SELECT再判断INSERT还是UPDATE,这条SQL是单次往返,天然原子,不用额外加锁。
4.2 出库登记:先锁行再判断,库存不能是负数
出库和入库最大的不同是要先校验库存够不够。校验和扣减是两件事,如果不做并发控制,两个收银员同时卖最后一个商品,两个事务都读到"库存还有1",都判断"够",都执行扣减,最后库存变成-1。这就是经典的超卖,也是Java面试题里数据库并发必问的点。
解决办法是让"校验加扣减"变成原子操作,用SELECT...FOR UPDATE把库存行锁住:
public void stockOut(StockOutOrder order, List<StockOutItem> items) throws Exception { Connection conn = null; try { conn = JdbcUtil.getConnection(); conn.setAutoCommit(false); int orderId = insertOutOrder(conn, order); for (StockOutItem item : items) { insertOutItem(conn, item); Integer qty = queryStockForUpdate(conn, order.getWarehouseId(), item.getGoodsId()); if (qty == null || qty < item.getQuantity()) { throw new BusinessException("库存不足:商品ID=" + item.getGoodsId() + " 当前=" + qty + " 需要=" + item.getQuantity()); } decreaseStock(conn, order.getWarehouseId(), item.getGoodsId(), item.getQuantity()); } conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); throw e; } finally { if (conn != null) conn.close(); } } private Integer queryStockForUpdate(Connection conn, int warehouseId, int goodsId) throws SQLException { String sql = "SELECT quantity FROM stock " + "WHERE goods_id = ? AND warehouse_id = ? FOR UPDATE"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, goodsId); ps.setInt(2, warehouseId); try (ResultSet rs = ps.executeQuery()) { return rs.next() ? rs.getInt(1) : null; } } }FOR UPDATE的含义是:这条SELECT命中并锁住库存行,直到当前事务commit或rollback才释放。第二个事务如果也想对同一个商品出库,会被阻塞在SELECT处,等第一个事务结束才能读到最新库存。这样两个并发出库变成串行执行,第二个事务会发现库存不够,抛出库存不足。单用户课程设计里永远测不出这个bug,但演示时现场多开两个客户端同时下单,有FOR UPDATE和没有是两种完全不同的结果。
注意:FOR UPDATE 必须配合事务使用,setAutoCommit(false) 之后才能锁住行直到 commit/rollback。开着自动提交去执行 FOR UPDATE,锁会立刻释放,等于白写。
还有一个容易被忽视的点:出库明细的插入要放在扣库存之前,并和库存校验交错进行。这样如果出库单有5条明细、第3条库存不够,前两条已经扣的库存会随rollback一起撤销,不会出现"单据没录全但库存少了一块"的中间态。
4.3 盘点业务:差异调整单留痕,不直接改库存
盘点是最容易做歪的功能。很多初稿把盘点做成一个直接编辑库存数字的窗口,管理员手一抖改成多少就是多少,没有任何记录。这个设计在业务上不能接受:盘点结果需要复核,差异需要追溯是谁盘出来的、依据是什么。
我一般这样设计盘点流程:
- 盘点员选择仓库和盘点范围,系统生成一张盘点单,把当前账面库存快照进盘点明细;
- 盘点员在界面上逐条填写实盘数量;
- 系统自动计算差异:实盘减账面,diff_quantity可能是正数盘盈、负数盘亏;
- 确认盘点单后,用和入库出库相同的mergeStock逻辑,按diff_quantity逐条调整库存;
- 盘点单状态从盘点中改成已确认,不可再改。
CREATE TABLE inventory_check_item ( id INT AUTO_INCREMENT PRIMARY KEY, check_order_id INT NOT NULL, goods_id INT NOT NULL, account_quantity INT NOT NULL COMMENT '账面数', actual_quantity INT NOT NULL COMMENT '实盘数', diff_quantity INT NOT NULL COMMENT '差异=实盘-账面', remark VARCHAR(200), KEY idx_check_order (check_order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='盘点明细';确认盘点时,diff_quantity为正就按入库处理,为负就按出库处理,本质上是复用同一个mergeStock方法。这样做的好处是库存变动永远有单据来源:要么来自入库单,要么来自出库单,要么来自盘点单。导数据、对账、答辩讲"库存可追溯"时,这一条就是最有力的证据。
一个实际经验:盘点界面上,账面数量列要设为不可编辑,实盘数量列才可编辑。这是在界面层防呆,防止盘点员把账面数也改了导致差异计算失去意义。类似的防呆还有实盘数量输入负数、填字母时直接拦截——数据库层虽然可以加CHECK约束,但Swing里用InputVerifier或DocumentFilter提前挡住,用户体验好得多。
5. Swing+MySQL联调避坑:5个现场、原因和处理方法
Swing加MySQL联调,坑的密度比想象中高。下面这5个现场我没少遇到,也帮人排过,原因基本都是环境、并发和生命周期的问题,排查清楚了没一个真玄学。
5.1 报错一:ClassNotFoundException,驱动包看似在却找不到
现象:程序一启动就抛ClassNotFoundException: com.mysql.cj.jdbc.Driver。
原因通常有两个。一是mysql-connector的jar根本没进classpath,IDE里导了但没勾选Add to Build Path,或者用javac手动编译时没带-cp参数。二是驱动类名写错:MySQL 8.0的驱动类是com.mysql.cj.jdbc.Driver,5.x的才是com.mysql.jdbc.Driver,版本和类名必须对应。
解决:确认mysql-connector-j-8.x.jar在项目的lib目录并加入Build Path,或者用Maven在pom.xml里声明依赖,让Maven替你管classpath。
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency>5.2 报错二:点击查询后窗口转圈,像死机一样
现象:界面点查询后整个窗口无响应,过几秒甚至几十秒才恢复,小数据集也卡。
原因:把耗时的JDBC查询直接写在了Swing的事件回调里。Swing的界面刷新跑在事件分发线程EDT上,DB查询占用着这个线程,界面重绘、按钮点击全部排队,看起来就是卡死。数据量一大,这个假死会变成真死。
解决:查询放到SwingWorker的doInBackground里,拿到结果后在done里更新JTable。SwingWorker是Swing官方给的异步方案,比手开Thread更安全,因为它保证结果回传还是回到EDT上更新界面。
SwingWorker<List<Goods>, Void> worker = new SwingWorker<List<Goods>, Void>() { @Override protected List<Goods> doInBackground() throws Exception { return goodsDao.searchByKeyword(keyword); // 耗时查询在后台 } @Override protected void done() { try { List<Goods> result = get(); tableModel.setRows(result); // 结果回到EDT,刷新表格 } catch (Exception e) { JOptionPane.showMessageDialog(MainFrame.this, "查询失败:" + e.getMessage()); } } }; worker.execute();5.3 报错三:中文乱码,界面、数据库两边不一致
现象:界面上输入中文保存后,数据库里是问号或者变成????,反过来数据库里的中文在JTable里显示成乱码。
原因:三个环节的字符集不一致。Java源文件不是UTF-8编码,JDBC URL没带characterEncoding=utf8,或者建库建表时用了latin1默认字符集。只要坏一环,中文就花。
解决:三处统一。IDE把项目编码设为UTF-8,MySQL建库时用utf8mb4,JDBC URL加上characterEncoding=utf8。改完之后重启程序,再插入数据验证一遍。还有一个容易被忽略的坑:MySQL连接参数如果写成characterEncoding=utf8mb4,驱动会报Unsupported character encoding,注意这里要写utf8不是utf8mb4。
5.4 报错四:库存被扣成负数,并发下超卖
现象:两个人同时给同一个商品出库,库存明明只剩1,两张单都成功了,库存变成-1。
原因:先查库存、判断够不够、再更新库存,三步不是原子的。两个事务都读到库存1,都认为可以出库,然后各自扣减。没有把读和写锁在一起,校验就形同虚设。
解决:用第四章的SELECT...FOR UPDATE把库存行锁住,配合事务让校验和扣减串行。如果不想写原生SQL锁,可以把库存更新改成带条件的UPDATE:UPDATE stock SET quantity = quantity - ? WHERE goods_id=? AND warehouse_id=? AND quantity >= ?,受影响行数为0就说明库存不足,抛异常。这个写法也能防超卖,而且少一次SELECT。
5.5 报错五:IDE里能跑,双击JAR就报数据库连不上
现象:Maven打包后拿到另一台机器双击运行,立刻报Connection refused或ClassNotFoundException。
原因:三件事没做好。数据库驱动jar没有打进最终的JAR,用的是默认的瘦JAR;数据库地址写死在代码里,换机器就没法改;目标机器上MySQL服务没启动或者账号权限不对。
解决:用maven-assembly-plugin或shade插件打胖JAR,把mysql-connector等依赖一并打包;数据库连接参数外置到db.properties,运行目录放一份配置文件,改IP、密码不用重新编译;交付前在一台干净机器上做一次完整验证,包括MySQL安装、导入SQL脚本、双击JAR运行三步。这条流程自己先跑一遍,交付时才不会在客户面前翻车。
6. 交付前最后一公里:打包、外置配置与库存对账验证
打包这一步直接决定源码能不能变成能用的工具。Maven项目在pom.xml里配maven-assembly-plugin,mvn clean package之后得到带依赖的可执行JAR。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.6.0</version> <configuration> <archive> <manifest> <mainClass>com.whms.MainApp</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals><goal>single</goal></goals> </execution> </executions> </plugin>瘦JAR只装自己的class,依赖的mysql-connector没打进去,换台机器必报ClassNotFoundException。胖JAR把所有依赖合并进同一个jar,体积大一点,但对桌面交付来说是靠谱的做法。数据库连接外置我习惯放在JAR同级的config目录,JdbcUtil启动时先找外部文件、找不到再用内置默认值。配置集中在外面,小JAR配不同的数据库就能适应不同部署环境,这是桌面交付里性价比最高的一项改造。
交付前做一轮库存对账,四步验证:录一笔入库,查库存是否同步增加;录一笔出库,查库存是否减少;故意把出库数量改成大于库存,确认系统拦截;再跑一次盘点,确认差异调整生成一正一负两条库存变动。最后用一条SQL扫一遍负库存:
mysql -uroot -p -e "SELECT goods_id, warehouse_id, quantity FROM stock WHERE quantity < 0;"返回空结果,基本盘就是稳的。
我做这类项目有个习惯:交付时把建表SQL、初始化账号、运行说明三样东西一并给到对方,而不是只丢一个jar。因为对方十有八九不会看代码,但一定会按说明去装MySQL、导数据、双击运行。这套流程我栽过跟头:有一次只给了jar,对方装的是MySQL 5.7,驱动类名对不上,折腾了半小时才发现是环境版本问题。从那以后,我每个Swing项目都附一份环境版本对照说明,写明"开发验证环境:MySQL 8.0.33 + JDK 8以上,5.7请改用旧驱动类名"。希望帮到你。
本文还有配套的精品资源,点击获取