ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Swing + MySQL 仓库管理系统源码:从零手搓到避坑指南

Java Swing + MySQL 仓库管理系统源码:从零手搓到避坑指南 简介这份资源是基于Java Swing与MySQL的仓库管理系统设计源码面向学习Java桌面开发、数据库应用或需要完成课程设计、毕业设计的开发者。系统以Swing构建跨平台图形界面涵盖容器、按钮、表格、树等组件后端依托MySQL存储商品、库存、出入库记录与用户权限等数据并实现增删改查、表单校验、权限控制、报表导出与操作日志等业务模块适合作为中小型仓储管理场景的参考实现。压缩包共39个文件约8.45MB其中15个Java源文件承载核心业务逻辑10个JAR包提供依赖支持7个XML配置文件负责数据库连接与框架配置另含属性文件、项目文件与图片资源目录按src、dao、entity、frame、utils等分层组织结构清晰。目前已有117人学习下载可帮助读者快速理解Swing界面搭建、MySQL表结构设计与事务处理思路并在此基础上二次开发或扩展功能。1. 从零手搓一套 Java Swing MySQL 仓库管理系统这套源码到底值不值得你花时间很多做 Java 课程设计或者想练手桌面应用的人都会在搜索框里敲下「仓库管理系统 源码」这几个字然后被一堆需要积分下载、跑起来报错、数据库连不上的压缩包劝退。我当初也是这么过来的后来干脆自己用 Java Swing 加 MySQL 从头搭了一套把入库、出库、库存查询、用户权限这些核心模块跑通。这套东西解决的不是什么高深问题就是让你有一个能编译、能连库、能点按钮看到数据的完整闭环。它适合两类人一类是正在找 Java 课程设计案例源码的学生需要一份结构清晰、注释到位的参考另一类是刚学完 Java 基础、想找个东西把面向对象编程和 JDBC 串起来的自学者。你不需要会 Spring也不需要懂前端框架只要装好 JDK 和 MySQL就能在本地把它跑起来。2. 技术选型与工程结构为什么 Swing MySQL 依然是桌面管理系统的稳妥组合2.1 Swing 做桌面端的现实理由现在一提到 Java 做界面很多人第一反应是 JavaFX 或者直接上 Web。但在课程设计和中小型内部工具这个场景里Swing 依然是翻车率最低的选择。原因很直接JDK 自带不需要额外配依赖javax.swing包从 Java 1.2 活到现在网上的资料和问答足够多你遇到问题基本都能搜到。Swing 的组件模型是典型的 MVC 思路JFrame当窗口容器JPanel做布局分区JTable绑数据模型JButton挂ActionListener。这套东西写熟了改需求就是改监听器里的逻辑不用重新学一套框架。另一个现实因素是 MySQL 的配合。Swing 负责展示MySQL 负责持久化中间用 JDBC 直连整个链路短、可控。你不需要配 ORM不需要管连接池的复杂参数一个DriverManager.getConnection就能把数据拉出来。对于仓库管理系统这种表结构不复杂、并发量几乎为零的场景这种简单直接的方式反而最稳。2.2 工程目录怎么分才不乱我见过不少课程设计源码把所有类堆在一个包下面找起来非常痛苦。比较合理的做法是按职责分包下面是我实际用的结构src/ ├── com/warehouse/ │ ├── entity/ # 实体类对应数据库表 │ │ ├── User.java │ │ ├── Goods.java │ │ └── StockRecord.java │ ├── dao/ # 数据访问层写 SQL │ │ ├── UserDao.java │ │ ├── GoodsDao.java │ │ └── StockDao.java │ ├── util/ # 工具类 │ │ └── DBUtil.java │ ├── view/ # 界面类 │ │ ├── LoginFrame.java │ │ ├── MainFrame.java │ │ └── GoodsPanel.java │ └── App.java # 程序入口 └── lib/ └── mysql-connector-java-8.0.xx.jarentity包放的是和表字段一一对应的 Java 类属性名建议和列名保持一致省得后面映射的时候来回对。dao包是真正干活的地方每个方法对应一条或一组 SQL。util包里只放一个DBUtil负责拿连接和关资源。view包按功能面板拆登录一个类主界面一个类每个业务模块一个JPanel子类。这样拆完你改库存查询的逻辑就只动StockDao和GoodsPanel不会牵一发动全身。2.3 数据库表设计的最小可用集仓库管理系统说到底就是围绕「货」和「人」转。下面这几张表是跑通核心流程必须的表名用途关键字段user登录与权限id,username,password,rolegoods商品档案id,name,category,unit,price,stockstock_record出入库流水id,goods_id,type,quantity,operate_time,operatoruser表里role字段用admin和staff区分权限管理员能删商品普通员工只能做出入库。goods表的stock字段是当前库存每次出入库都要同步更新这个值。stock_record表只追加不修改保证流水可追溯。建表语句里goods_id加外键关联goods.idoperate_time用datetime默认CURRENT_TIMESTAMP。注意MySQL 8 之前datetime默认值不支持CURRENT_TIMESTAMP如果你用的是 5.7 版本需要在插入时手动传时间或者把字段改成timestamp。3. 从建库到连库把 MySQL 和 JDBC 这条线打通3.1 建库建表与初始数据先把数据库和表建好后面写代码才有东西可连。打开 MySQL 命令行或者 Workbench执行下面的 SQLCREATE DATABASE warehouse DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE warehouse; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(20) DEFAULT staff ); CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category VARCHAR(50), unit VARCHAR(20), price DECIMAL(10,2) DEFAULT 0.00, stock INT DEFAULT 0 ); CREATE TABLE stock_record ( id INT PRIMARY KEY AUTO_INCREMENT, goods_id INT NOT NULL, type VARCHAR(10) NOT NULL COMMENT in 或 out, quantity INT NOT NULL, operate_time DATETIME DEFAULT CURRENT_TIMESTAMP, operator VARCHAR(50), FOREIGN KEY (goods_id) REFERENCES goods(id) ); INSERT INTO user (username, password, role) VALUES (admin, 123456, admin); INSERT INTO goods (name, category, unit, price, stock) VALUES (A4纸, 办公用品, 包, 25.00, 100);建表时字符集用utf8mb4避免中文商品名出现乱码。goods表的stock字段设了默认值 0新增商品时如果不传库存就是 0。初始数据里插了一个管理员账号和一条测试商品方便你跑起来就能看到东西。3.2 DBUtil 的写法与连接参数DBUtil是整个系统里唯一直接碰DriverManager的地方把它写稳了后面就省心。下面是我常用的版本package com.warehouse.util; import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/warehouse?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }URL里的serverTimezoneAsia/Shanghai是必须的MySQL 8 的驱动如果不指定时区连接时会直接抛异常。useSSLfalse在本地开发时关掉省得折腾证书。characterEncodingutf8保证中文参数不乱码。Class.forName放在静态块里只执行一次加载驱动类。close方法按ResultSet、Statement、Connection的顺序倒着关这是 JDBC 资源释放的标准姿势。提示如果你用的是 MySQL 5.7驱动类名是com.mysql.jdbc.DriverURL 里不需要serverTimezone参数。驱动 jar 包版本要和数据库版本匹配8.0 的库用 8.0 的驱动。3.3 登录模块的完整实现登录是用户接触的第一个界面也是验证 JDBC 通没通的试金石。先写UserDaopackage com.warehouse.dao; import com.warehouse.entity.User; import com.warehouse.util.DBUtil; import java.sql.*; public class UserDao { public User login(String username, String password) { String sql SELECT * FROM user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setRole(rs.getString(role)); return u; } } catch (SQLException e) { e.printStackTrace(); } return null; } }这里用PreparedStatement而不是Statement一是防 SQL 注入二是参数多的时候不用拼字符串。try-with-resources写法会自动关Connection和PreparedStatement比手动关省事。查询结果用rs.next()判断有没有命中有就封装成User对象返回没有就返回null。界面这边LoginFrame继承JFrame放两个JTextField和一个JPasswordField按钮的监听器里调UserDao.loginpackage com.warehouse.view; import com.warehouse.dao.UserDao; import com.warehouse.entity.User; import javax.swing.*; import java.awt.*; public class LoginFrame extends JFrame { private JTextField usernameField new JTextField(15); private JPasswordField passwordField new JPasswordField(15); public LoginFrame() { setTitle(仓库管理系统 - 登录); setSize(350, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JPanel panel new JPanel(new GridLayout(3, 2, 10, 10)); panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); panel.add(new JLabel(用户名)); panel.add(usernameField); panel.add(new JLabel(密码)); panel.add(passwordField); JButton loginBtn new JButton(登录); loginBtn.addActionListener(e - doLogin()); panel.add(new JLabel()); panel.add(loginBtn); add(panel); setVisible(true); } 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 new UserDao().login(username, password); if (user ! null) { dispose(); new MainFrame(user); } else { JOptionPane.showMessageDialog(this, 用户名或密码错误); } } }setLocationRelativeTo(null)让窗口在屏幕居中不加这行窗口会跑到左上角。GridLayout(3, 2)是三行两列最后一行左边放空JLabel占位右边放按钮。登录成功后dispose()关掉登录窗把User对象传给主界面主界面根据role决定哪些按钮可用。4. 入库出库与库存联动把业务逻辑写对的核心细节4.1 入库操作的完整链路入库不是简单往stock_record插一条记录就完事它要同时更新goods表的stock字段。这两步必须在一个事务里要么都成功要么都回滚。先看StockDao里的入库方法package com.warehouse.dao; import com.warehouse.util.DBUtil; import java.sql.*; public class StockDao { public boolean stockIn(int goodsId, int quantity, String operator) { String insertSql INSERT INTO stock_record (goods_id, type, quantity, operator) VALUES (?, in, ?, ?); String updateSql UPDATE goods SET stock stock ? WHERE id ?; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(insertSql)) { ps1.setInt(1, goodsId); ps1.setInt(2, quantity); ps1.setString(3, operator); ps1.executeUpdate(); } try (PreparedStatement ps2 conn.prepareStatement(updateSql)) { ps2.setInt(1, quantity); ps2.setInt(2, goodsId); ps2.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }conn.setAutoCommit(false)开启事务两条 SQL 都执行完才commit。任何一步抛异常就进catch块执行rollback保证不会出现「流水记了但库存没加」或者反过来「库存加了但流水没记」的情况。finally里把autoCommit恢复成true再关连接避免连接回到池子里还带着事务状态。出库方法结构一样只是type改成outUPDATE语句变成stock stock - ?并且要在更新前检查库存够不够。4.2 库存不足的判断与界面反馈出库时如果库存不够不能直接执行UPDATE否则库存会变成负数。正确做法是先查当前库存再决定走不走public boolean stockOut(int goodsId, int quantity, String operator) { String checkSql SELECT stock FROM goods WHERE id ?; String insertSql INSERT INTO stock_record (goods_id, type, quantity, operator) VALUES (?, out, ?, ?); String updateSql UPDATE goods SET stock stock - ? WHERE id ?; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); int currentStock; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, goodsId); ResultSet rs ps.executeQuery(); if (!rs.next()) return false; currentStock rs.getInt(stock); } if (currentStock quantity) { conn.rollback(); return false; } try (PreparedStatement ps1 conn.prepareStatement(insertSql)) { ps1.setInt(1, goodsId); ps1.setInt(2, quantity); ps1.setString(3, operator); ps1.executeUpdate(); } try (PreparedStatement ps2 conn.prepareStatement(updateSql)) { ps2.setInt(1, quantity); ps2.setInt(2, goodsId); ps2.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }查库存和更新库存都在同一个事务里查的时候用SELECT不加锁因为桌面应用并发极低不会出现两个窗口同时出库同一件商品的情况。如果以后要改成多人同时操作需要在SELECT后面加FOR UPDATE行锁。界面层拿到false返回值后弹窗提示「库存不足当前库存 XX」让操作员知道差多少。4.3 JTable 绑定库存数据的正确姿势主界面里库存列表用JTable展示数据从GoodsDao查出来。很多人直接往JTable里塞Vector改数据的时候非常痛苦。标准做法是自定义一个TableModelpackage com.warehouse.view; import com.warehouse.entity.Goods; import javax.swing.table.AbstractTableModel; import java.util.List; public class GoodsTableModel extends AbstractTableModel { private final String[] columns {ID, 名称, 分类, 单位, 单价, 库存}; private ListGoods goodsList; public GoodsTableModel(ListGoods goodsList) { this.goodsList goodsList; } public void setGoodsList(ListGoods goodsList) { this.goodsList goodsList; fireTableDataChanged(); } Override public int getRowCount() { return goodsList.size(); } Override public int getColumnCount() { return columns.length; } Override public String getColumnName(int column) { return columns[column]; } Override public Object getValueAt(int rowIndex, int columnIndex) { Goods g goodsList.get(rowIndex); switch (columnIndex) { case 0: return g.getId(); case 1: return g.getName(); case 2: return g.getCategory(); case 3: return g.getUnit(); case 4: return g.getPrice(); case 5: return g.getStock(); default: return null; } } }继承AbstractTableModel只需要实现getRowCount、getColumnCount、getValueAt三个方法列名通过getColumnName覆盖。setGoodsList里调fireTableDataChanged()通知表格刷新不用重建JTable对象。GoodsPanel里初始化的时候new JTable(new GoodsTableModel(goodsDao.findAll()))入库出库完成后重新查一遍数据调setGoodsList表格就自动更新了。注意getValueAt返回的price是BigDecimal类型JTable默认右对齐显示如果想让价格列居中需要给该列单独设渲染器。库存列是Integer默认右对齐符合数字阅读习惯。5. 避坑与排查这套源码跑不起来时先看这五条5.1 报错「No suitable driver found」现象运行程序点登录控制台抛java.sql.SQLException: No suitable driver found for jdbc:mysql://...。原因驱动 jar 包没加到 classpath 里或者Class.forName里的类名写错了。MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver不是com.mysql.jdbc.Driver。解决在 IDE 里把mysql-connector-java-8.0.xx.jar加到项目的 Libraries 里。如果用命令行编译运行java -cp .;lib/mysql-connector-java-8.0.xx.jar com.warehouse.AppWindows 用分号Linux 和 Mac 用冒号。5.2 中文商品名存进去变成问号现象新增商品时名称写「A4纸」存到数据库里查出来是「A4?」或者乱码。原因数据库、表、连接三处的字符集不一致。常见情况是数据库建的时候用了latin1或者 JDBC URL 里没指定characterEncoding。解决建库时明确DEFAULT CHARACTER SET utf8mb4JDBC URL 里加上characterEncodingutf8。已经建好的库可以用ALTER DATABASE warehouse CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;改表也要单独改。5.3 时间字段插入报错「Invalid default value」现象建stock_record表时operate_time DATETIME DEFAULT CURRENT_TIMESTAMP报错。原因MySQL 5.6 之前的版本不支持DATETIME类型设CURRENT_TIMESTAMP默认值5.6 之后才支持。如果你用的是更老的版本或者sql_mode里开了严格模式就会报这个错。解决把字段类型改成TIMESTAMP或者升级 MySQL 到 5.7 以上。临时方案是在插入 SQL 里显式传NOW()不依赖默认值。5.4 登录成功但主界面按钮点了没反应现象登录窗口消失主界面出来了但点「入库」「出库」按钮没有任何弹窗或数据变化。原因按钮的ActionListener没挂上或者挂上了但里面抛了异常被吞了。Swing 的事件监听器里如果抛异常默认不会弹窗只在控制台打印堆栈。解决检查addActionListener那行代码有没有执行到。在监听器方法第一行加System.out.println(按钮被点击)确认事件有没有触发。如果触发了但没反应看控制台有没有NullPointerException通常是User对象没传进来或者GoodsDao实例为null。5.5 打包成 jar 后连不上数据库现象在 IDE 里跑得好好的mvn package或者手动打成 jar 之后双击运行报数据库连接失败。原因驱动 jar 没有一起打进去或者DBUtil里的 URL 写的是localhost换台机器跑的时候数据库不在本机。解决用 Maven 的maven-assembly-plugin或者maven-shade-plugin把依赖一起打进 fat jar。如果手动打包需要把mysql-connector-java解压后把com/mysql目录合并到你的 jar 里。URL 里的localhost改成实际数据库 IP或者做成配置文件让用户自己填。6. 进阶技巧用 JDBC 批处理和事务隔离把性能与数据一致性再提一档前面跑通的是单条出入库实际用的时候经常遇到批量导入商品、批量出库的场景。一条一条执行 SQL一千条就是一千次网络往返慢得让人想砸键盘。JDBC 的addBatch和executeBatch能把多条 SQL 攒在一起发给数据库速度提升非常明显。下面是我在GoodsDao里加的批量插入方法public int batchInsert(ListGoods goodsList) { String sql INSERT INTO goods (name, category, unit, price, stock) VALUES (?, ?, ?, ?, ?); Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (Goods g : goodsList) { ps.setString(1, g.getName()); ps.setString(2, g.getCategory()); ps.setString(3, g.getUnit()); ps.setBigDecimal(4, g.getPrice()); ps.setInt(5, g.getStock()); ps.addBatch(); } int[] results ps.executeBatch(); conn.commit(); return results.length; } } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return 0; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }addBatch把每条参数攒进批处理队列executeBatch一次性提交。返回的int[]数组里每个元素是对应 SQL 影响的行数正常情况下都是 1。整个批处理包在一个事务里中途有一条失败就全部回滚不会出现导入一半的尴尬局面。一千条商品数据用批处理大概两三秒逐条插入要半分钟以上差距就是这么直接。另一个值得花时间调的是事务隔离级别。MySQL 默认的REPEATABLE READ在桌面应用里够用但如果你在库存查询界面希望每次刷新都看到最新数据可以把连接的隔离级别设成READ COMMITTEDconn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);这行要放在setAutoCommit(false)之前。READ COMMITTED下每次SELECT都能看到其他事务已提交的最新数据代价是同一个事务里两次同样的查询可能结果不同。对于仓库管理这种读多写少、且读的时候希望看到实时库存的场景这个取舍是划算的。最后说一个我踩过的坑JTable的数据模型更新和数据库事务不要混在一起。我一开始在stockIn方法里直接调tableModel.setGoodsList结果事务还没提交界面就刷新了偶尔出现界面显示库存加了但数据库回滚了的情况。后来改成stockIn返回true之后在界面层重新查一遍数据再刷新表格逻辑就清晰了。界面归界面数据归数据这条线划清楚后面加多少功能都不会乱。这套东西我前后改了三版第一版所有 SQL 拼字符串第二版换成PreparedStatement第三版才把事务和批处理加上。如果你刚开始做建议先把单条出入库跑通再往上叠批处理和权限控制。别一上来就想着做多仓库、多单位换算那些是后面的事。希望帮到你。本文还有配套的精品资源点击获取
返回列表