
简介一套基于Java Swing与MySQL开发的仓库管理系统完整源码适合正在学习Java桌面应用开发、数据库设计或准备课程设计、毕业设计的开发者。系统实现商品入库、出库、库存查询、报表生成、用户权限与操作日志等功能通过表单验证与权限控制保障数据准确安全满足中小仓储场景。界面使用JFrame、JTable、JComboBox等Swing组件后端MySQL以合理表结构与索引提升查询效率并用事务确保写入可靠。压缩包共39个文件约8.45MB包含15个Java源文件、10个JAR依赖包、7个XML配置文件、2个Git忽略文件以及PNG图片、IML项目文件和属性文件等XML配置项覆盖数据库连接等参数工程内按dao、entity、frame、config等分包组织结构清晰配有说明文档便于上手。目前已有118人学习下载适合作为仓库管理项目的实战参考或二次开发基础。通过研读源码可掌握Swing界面与MySQL交互、分层开发、配置管理及日志记录等完整设计思路可直接导入IDE运行和扩展。1. 一个Swing仓库管理系统到底值不值得拆先说我为什么盯上这套源码。前阵子要接一个很急的仓库管理整改需求对方明确要用 Java Swing MySQL想要能跑的代码而不是花里胡哨的技术方案。朋友丢过来这套压缩包我打开一看UI 是十几年前的 JFrame 风格但包结构意外清晰entity、dao、frame、config 分得明明白白不是那种全塞在一个类里的“演示工程”。对正在做课设或者想快速搭一套内部工具的人来说这比自己在 Stack Overflow 上拼一堆 CRUD 靠谱得多。这套系统解决的是仓储管理里最核心的几件事商品信息维护、出入库登记、库存实时扣减与查询、报表输出、用户权限和操作日志。用 Java Swing 做桌面端跨平台不用改代码MySQL 存数据单机部署很轻。适合课程设计、毕业设计二次开发也适合小规模仓库的内部管理工具改造。但要提醒一句它不是一个能直接商用的成品更像是给你打好地基、但装修方案还得自己定的半毛坯房。接下来的内容我会从源码分层、界面搭建、数据库事务、避坑经验这几个角度把它拆开给你看。2. 源码结构与运行准备三个包把界面和数据访问分离清楚再把项目跑起来拿到源码先别急着双击运行先花五分钟看目录能省下后面一整天的排查时间。这套源码比很多“单文件怼到底”的作业规范得多哪怕你只用它做参考分层方式也值得抄。2.1 包结构与职责entity、dao、frame 到底各管什么把src下的目录按职责拆开看其实就是一条标准的 MVC 变体链路包名职责典型内容entity数据实体对应数据库表结构商品、库存、用户、出入库记录实体类dao数据访问层JDBC 操作增删改查 SQL、事务控制frameSwing 界面层主窗口、登录框、入库出库面板config配置管理数据库连接参数、属性文件读取utils工具类日期格式化、字符串校验、导出报表辅助frame层只负责搭界面、收集用户输入把数据交给dao层自己不拼 SQL。比如商品入库窗口你看到JTextField输入商品名、数量之后actionPerformed里就是new StockDao().inStock(productId, qty, operator)查询结果用JTable回显。这就是一个很朴素的 MVC 落法没上 Spring但逻辑是顺的。源码里还有BoxLayoutExample.java和ButtonPanelExample.java两个单独文件很多人忽略这两个 Demo。实际上它们就是给你拆开看的“布局范例”告诉你按钮排成一行、表单输入框纵向排列是怎么用BoxLayout实现的。别删先跑一遍这两个例子再去看真正的界面代码视觉压力小很多。2.2 编译与运行准备JDK 版本、MySQL 驱动和配置文件我不是很建议一上来就追求最新版 JDK。Swing 本身依赖轻但 JDK 8 跑老代码是最稳的JDK 11 或 17 也问题不大。重点在于 MySQL 驱动版本要和数据库对应。项目里带了多个jar包如果用 MySQL 5.7 就选mysql-connector-java-5.x用 MySQL 8.0 则建议用mysql-connector-java-8.x否则会碰到驱动类找不到或认证协议不兼容的问题。导入工程时在 IDEA 里选择File - New - Project from Existing Sources选根目录下的.iml文件IDEA 能顺着把模块识别出来。注意src目录必须标记为 Sources Root。resources目录如果是放配置文件的也要右键标记为 Resources Root否则运行时会找不到 properties。数据库配置一般会写在resources下的属性文件里核心连接参数长这样jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/wms_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456这段url里的参数一个都别省。useUnicodetrue保证中文正常写入characterEncodingutf8和数据库端字符集要一致serverTimezoneAsia/Shanghai是 MySQL 8 驱动必须写的否则时间字段会报错。我看到很多翻车现场就是少了一个serverTimezone启动后抛异常排查半天才发现是时区问题而不是代码问题。2.3 建表语句没给从 DAO 层反向也能推出来这套源码在文件列表里没显式给出structure.sql这很正常。如果你的 MySQL 里还没有库表最快的方法是先看dao包里每个方法的 SQL 字符串把CREATE TABLE的结构逆向写出来。比如某个 DAO 方法里写的是SELECT product_id, product_name, category, spec FROM product WHERE product_id ?你就能推断出product表至少有product_id、product_name、category、spec这四个字段。再用到INSERT INTO stock_log ...就能推出stock_log表的结构。我一般会先把涉及的表列一个清单sys_user、product、stock、stock_log。手工补一个建表脚本后用 Navicat 或命令行执行再把 DAO 里的字段和表字段比对一遍确保没有漏字段。这一步看似麻烦但能帮你快速确认源码质量如果 DAO 里字段名和表结构对不上大概率之前跑不起来就是因为没人做这步。把这个补好项目才算真正到了“能运行”的起点。3. Java Swing 界面模块从 JFrame 搭建到 JTable 刷新的完整链路Swing 的组件很简单难的是组合方式和刷新逻辑。这套系统的界面关键点其实就两个表单输入区和表格展示区。把这两块吃透其他窗口不过是复制粘贴。3.1 组件选型容器的组合方式决定你改代码的心情框架上常见做法是主窗口继承JFrame内容区放JPanel再用布局管理器把面板排布好。源码里大量使用BoxLayout和BorderLayout这俩组合出刀就能砍柴BorderLayout适合做整体骨架比如北边放标题栏中间放表格南边放按钮。BoxLayout适合做表单区域让输入框从上到下纵向排列或者在一行里横向排列多个按钮。GridBagLayout不必强求虽然灵活但学习成本高很多老项目宁可用多个BoxLayout嵌套。组合方式上优先选“组合而不是继承”。什么叫组合你在你的业务窗口里new一个JPanel对象把它放进JFrame的ContentPane里。继承则是你自己的类直接extends JFrame。两者都能实现界面但继承一个只有一个窗口的父类会让后续扩展非常痛苦。这套源码里以组合为主主窗口类里持有多个面板对象切换页面时用CardLayout或者直接移除旧面板刷新耦合度明显低。这个习惯建议直接学走。3.2 用 BoxLayout 把入库表单面板做出来以商品入库面板为例横向一组、纵向一组逻辑很直观。源码里类似的写法我会简化成这样JPanel formPanel new JPanel(); formPanel.setLayout(new BoxLayout(formPanel, BoxLayout.Y_AXIS)); JTextField nameField new JTextField(20); JTextField qtyField new JTextField(10); JComboBoxString categoryBox new JComboBox(new String[]{原料, 半成品, 成品}); JButton submitButton new JButton(确认入库); JPanel row1 new JPanel(new FlowLayout(FlowLayout.LEFT)); row1.add(new JLabel(商品名称)); row1.add(nameField); JPanel row2 new JPanel(new FlowLayout(FlowLayout.LEFT)); row2.add(new JLabel(商品分类)); row2.add(categoryBox); row2.add(new JLabel(数量)); row2.add(qtyField); submitButton.addActionListener(e - { String name nameField.getText().trim(); String qtyText qtyField.getText().trim(); if (name.isEmpty() || !qtyText.matches(\\d)) { JOptionPane.showMessageDialog(formPanel, 请填写商品名称和有效数量); return; } // 这里再调用 stockDao.inStock(...) }); formPanel.add(row1); formPanel.add(row2); formPanel.add(submitButton);逻辑说明BoxLayout.Y_AXIS让row1、row2、按钮依次从上到下排列每一行内部用FlowLayout左对齐避免组件被拉伸得乱七八糟。JComboBox用于固定分类选项防止手工输入同一分类时写出不同名字这是库存统计分组的关键。按钮事件里先做简单校验matches(\\d)确保数量是正整数非法输入直接弹窗不用把脏数据交到 DAO 层去判。参数说明FlowLayout默认的水平间距是 5垂直间距也是 5在密集表单里可能会显得挤可以手动改成new FlowLayout(FlowLayout.LEFT, 10, 10)。3.3 JTable 的“刷新”是个坑先清后填还是更新 TableModel做仓库管理表格是绝对主力。库存查询、出入库记录、报表预览都要用JTable。但很多人会在刷新上翻车直接table.repaint()或者((DefaultTableModel) table.getModel()).setRowCount(0)之后重新addRow结果界面要么闪烁要么显示的数据还是旧的。正确做法是拿到表格的DefaultTableModel把整个数据矩阵一次性塞进去DefaultTableModel model (DefaultTableModel) stockTable.getModel(); model.setDataVector(data, headers); model.fireTableDataChanged();data是一个二维 Object 数组headers是列名数组。逻辑说明setDataVector会先清空旧行再填新行fireTableDataChanged()通知所有监听器刷新视图。这样做的好处是只触发一次数据变更事件避免逐行addRow导致的多次重绘。参数说明data里的每个子数组长度必须和headers一致否则JTable会抛IllegalArgumentException。我建议每次查询后都用同样类型的数据源比如从Vector转成Object[][]保持结构统一。另外列宽是个容易忽视的参数。库存表的“备注”列很长如果不管默认会压缩到看不清。常见做法是为每一列设置首选宽度比如stockTable.getColumnModel().getColumn(2).setPreferredWidth(180);把长文本列设为180px数字列可以压到80px整个表格在JScrollPane里才会读着舒服。4. MySQL 数据库设计与 DAO 层库存表和出入库事务是关键界面做得再漂亮库存数据错了系统就是个摆设。这一章我要重点讲数据库设计和数据访问层的结构性设计这也是这套源码里最值得借鉴的地方。4.1 核心表结构商品表、库存表、出入库记录表仓库管理系统最少得有四张表用户表、商品表、库存表、库存流水表。用户表管权限商品表存基础信息库存表存实时数量流水表存每一次出入库明细。这里给出一个我从 DAO 层反推整理的简化结构CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(16) DEFAULT USER ); CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(64) NOT NULL, category VARCHAR(32), spec VARCHAR(64), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL UNIQUE, quantity DECIMAL(12,2) NOT NULL DEFAULT 0, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_stock_product FOREIGN KEY (product_id) REFERENCES product(id) ); CREATE TABLE stock_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, change_type VARCHAR(4) NOT NULL, quantity DECIMAL(12,2) NOT NULL, operator VARCHAR(32), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_product_time (product_id, create_time) );表结构说明stock.product_id加了唯一约束保证一物一行stock_log里用change_type区分IN和OUT加上操作人和时间后续做审计报表都在这张表上取数。之所以把库存单独拆出来而不是在product表里放quantity字段是为了避免商品基础信息更新时影响库存数据也方便以后做多仓库扩展。索引idx_product_time对“查某商品的出入库历史”非常重要没有它会全表扫描。4.2 JDBC 连接管理驱动、URL 参数、配置文件分离这套源码用的是最原始的 JDBC没有引入 MyBatis 或连接池。好处是容易读懂坏处是每次获取连接都有开销。但作为课程设计和中小型工具直接 getConnection 就够了不要硬上 Spring。连接配置通常集中在 config 层用一个静态工具类读取 propertiespublic class DbUtil { private static String url; private static String user; private static String password; static { try (InputStream in DbUtil.class.getClassLoader() .getResourceAsStream(config.properties)) { Properties props new Properties(); props.load(in); url props.getProperty(jdbc.url); user props.getProperty(jdbc.username); password props.getProperty(jdbc.password); } catch (IOException e) { throw new ExceptionInInitializerError(读取数据库配置失败); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }这段代码的逻辑说明把读配置的动作放到静态代码块中类加载时只执行一次避免每次拿连接都重新加载文件。用getResourceAsStream而不是new FileInputStream是为了让配置文件跟随target/classes走打包 jar 后也能正常读取。ExceptionInInitializerError用来兜底如果配置缺失一启动就报错而不是运行到一半才炸。参数说明props.load()是按 ISO-8859-1 读取 properties 文件的如果里面写了中文密码或中文参数需要转成 Unicode 写入或者改用 XML 配置文件。4.3 出库操作的事务边界两件事必须同时成功这是全源码最有含金量的地方。出库操作涉及两步扣减当前库存写下出库流水。如果先扣库存再加流水失败库存就凭空消失了。如果先加流水再扣库存失败流水就撒谎了。必须用事务包起来。看一个简化的出库 DAO 方法public void outStock(int productId, int qty, String operator) throws Exception { String updateSql UPDATE stock SET quantity quantity - ? WHERE product_id ? AND quantity ?; String insertSql INSERT INTO stock_log(product_id, change_type, quantity, operator) VALUES(?, OUT, ?, ?); try (Connection conn DbUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(updateSql); PreparedStatement ps2 conn.prepareStatement(insertSql)) { ps1.setInt(1, qty); ps1.setInt(2, productId); ps1.setInt(3, qty); int rows ps1.executeUpdate(); if (rows 0) { throw new RuntimeException(库存不足或商品不存在); } ps2.setInt(1, productId); ps2.setInt(2, qty); ps2.setString(3, operator); ps2.executeUpdate(); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } } }逻辑说明WHERE quantity ?是行级保护防止并发下把库存扣成负数。executeUpdate返回影响行数如果为 0 直接抛异常后续的 insert 不会执行。两个PreparedStatement共用一个Connection在同一个事务里。try-with-resources写法确保连接和语句自动关闭不需要手动 finally。参数说明setAutoCommit(false)必须在拿到连接后立刻调用如果放在事务中间某些驱动会隐式提交之前的事务效果完全不同。另外注意RuntimeException要被 catch 到并 rollback如果方法声明的是throws Exception调用窗口层需要 catch 住并弹窗提示否则用户看到的是后端异常堆栈而不是友好提示。5. 避坑与常见问题连接超时、中文乱码、Swing 刷新异常这一章我直接给你列几条我调试这套系统时真实踩过、也看别人踩过的坑每一条都是先现象再原因再解决照着抄就行。5.1 连接数据库报 Communications link failure原因可能是 URL 忘写时区现象点击“登录”按钮后控制台抛出Communications link failure最后一行提示The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.原因MySQL 8 驱动默认要求服务器地址必须带协议类型或时区。如果jdbc.url里没有serverTimezoneAsia/Shanghai驱动会认为客户端和服务器时区不一致连接初始化阶段直接失败。解决在连接 URL 末尾加上serverTimezoneAsia/Shanghai改成jdbc:mysql://localhost:3306/wms_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai如果报错里明确提示Could not create connection to database server也可以换回低版本驱动com.mysql.jdbc.Driver但这不是长久之计。我还遇到过加了时区参数依然连接失败的情况后来发现是防火墙拦了 3306 端口把端口放通或改成 localhost 连接就正常了。5.2 控制台输出中文正常Swing 界面却是问号现象MySQL 表里已经存了中文用 Navicat 看也是中文但 Swing 的JTable或JLabel上显示一堆“???”。更诡异的是从数据库查出来的中文没事手工在界面输入的中文也没事只有两者结合时出乱码。原因这是转码链路中的某个环节用了默认字符集通常是连接 URL 没指定characterEncodingutf8。如果 URL 里没写MySQL 驱动会用平台默认编码Windows 中文环境是 GBK写入时按 GBK 转字节读出时按 UTF-8 解析中文自然变问号。解决统一三条链路。第一条是连接 URL 加characterEncodingutf8第二条是 MySQL 表结构确认是utf8mb4在创建表时加DEFAULT CHARSETutf8mb4第三条是 MySQL 服务器端my.cnf里设置character_set_serverutf8mb4。改完这三处重启应用把之前写脏的数据删掉重试。5.3 JTable 点了刷新还是旧数据连 repaint 也没用现象点击“刷新库存列表”按钮后后台 SQL 查出来的数据已经是最新的但界面上JTable一动不动点表头排序也还是旧数据。原因最常见的两个一是你重写了JTable的getModel()导致拿到一个临时模型二是你只调用了table.repaint()但模型里的数据根本没变。repaint()只负责触发重绘并不会帮你去数据库里拉新数据。解决检查你的刷新按钮事件是否真的调用了 DAO 方法并拿到了新数据然后在事件里对DefaultTableModel执行setDataVector(data, headers)最后调用model.fireTableDataChanged()。如果之前给表格设置了自定义渲染器确认渲染器里没有缓存旧值。代码结构上我习惯把“查询数据并刷新表格”封装成一个独立方法refreshStockTable()按钮、定时器、搜索框都调它保证逻辑只有一份。5.4 打包 jar 运行后找不到图片资源现象在 IDEA 里跑程序一切正常但用mvn package或jar命令打包后双击 jar 运行窗口里的 logo 图或者按钮图标全部消失控制台提示ImageIO.read返回null。原因代码里用了new File(images/logo.png)这样的相对路径。IDEA 运行时工作目录是项目根目录所以能找到images文件夹打包成 jar 后资源被压进 jar 包内部不再是一个磁盘文件File方式自然失效。解决把图片放在src/main/resources里用资源流读取InputStream imgStream getClass().getResourceAsStream(/images/logo.png); BufferedImage img ImageIO.read(imgStream);逻辑说明/images/logo.png以/开头从 classpath 根目录开始找。这样打包进 jar 后也能读取。如果imgStream是null说明资源文件夹没有打进去检查一下resources是否被标记为资源目录或者pom.xml的resources配置是否把png后缀排除掉了。5.5 入库和出库操作后库存数量对不上现象操作记录显示入库 100 件、出库 30 件但库存余额不是 70而是乱七八糟甚至出现负数。原因提到几个可能一是某个操作没有走事务比如先插入流水再更新库存时程序中途异常退出流水留下来了但库存没扣二是并发问题两个出库请求同时读到一个旧的库存量最后更新互相覆盖三是代码里没有quantity ?的条件保护。解决对照outStock的代码确认更新库存和加流水在同一个事务里并且扣减 SQL 加上AND quantity ?。并发问题上事务隔离级别默认是REPEATABLE READ够用。但更稳妥的做法是给stock表的product_id加唯一约束防止同一条商品出现多行库存记录。如果有历史脏数据先跑一次盘点把库存数校正回来再补一个对账 SQL比对product表存量和stock_log出入库累计差值。6. 二次开发前的验证方法走一遍核心流程再动手改造这套源码最大的价值不是能跑而是给你一个可以验证、可以改的底座。但改造前必须先做四步验证否则改到一半发现底层 DAO 不全回头成本很高。第一步验证登录和权限。用系统初始账号登录确认普通用户和管理员看到的窗口不同。如果你需要改权限控制先定位登录成功后的跳转逻辑看frame包里的哪个类负责拉起主窗口。如果源码里只做了按钮隐藏没做后端拦截记住要补真正的权限必须放到 DAO 或服务层判断光隐藏按钮防不了有心人。第二步验证一条完整业务链路新增商品 → 入库 → 查库存 → 出库 → 查流水。这一步能暴露数据库设计的问题。如果入库时商品不存在系统是直接报外键错误还是提示“请先新增商品”如果入库成功但库存表没有自动创建记录说明INSERT ... ON DUPLICATE KEY UPDATE或者先查后改的逻辑没写好需要补上。第三步检查报表导出。源码里提到了报表生成你要确认导出的文件是 Excel、CSV 还是纯文本。如果是 CSV字段分隔符和中文编码要提前定好避免用户用 Excel 打开乱码。常见做法是导出.csv时带 BOM 头String header \uFEFF商品ID,商品名称,库存数量\n; Files.write(Paths.get(stock_report.csv), header.getBytes(UTF-8));第四步写一个小的压力测试。不需要完整自动化测试框架就从窗口里连续点 20 次“出库”看库存是否为负、流水是否漏记。这一步能最快发现事务边界写没写对。从那以后我每次接手这种 Swing 老项目都会强制先走一遍这四步再谈重构。说起来有点玄学但这套检查流程真的帮我避开了不少“能打开界面但业务全崩”的雷。愿你拿到这套源码后也能少踩几个坑希望帮到你。本文还有配套的精品资源点击获取