简介:一套面向高校课程设计与期末大作业的Java Swing物资信息管理系统项目,整合MySQL数据库,定位为可直接运行、可二次扩展的完整示例。资源共34个文件,包含27个Java源码(覆盖登录、主界面、物资出入库、供应商管理、统计查询等模块)、3个SQL脚本(含建库表、触发器、存储过程)、数据库设计文档及README说明,整体压缩包仅4.74MB,结构清晰便于按需查阅。目前已有133人学习下载,属于高分完成度项目(评审达98分)。下载后可获得经过本地编译验证的完整工程、数据库初始化语句与设计文档,能支撑从环境搭建、表结构设计到Swing界面与业务逻辑联调的全过程,适合希望快速理解分层开发或直接改造用于自身课设的初学者。
1. 为什么“高分”的反而是老实巴交的Java+Swing+MySQL?先把项目定位看清楚
单看技术栈,Java Swing + MySQL 在AI和微服务满天飞的今天显得像上个时代的产物。但真正落到课程设计、毕业设计和实训验收上,这类组合反而最容易出“高分”:评分老师看重的从来不是框架多新,而是业务闭环完整度。物资信息管理系统本质上就是一个标准的 CRUD + 出入库流程 + 统计报表训练场,Swing 负责把数据库里的数据变成肉眼可见的表格、树和对话框,MySQL 负责把每一笔账记明白。它适合三类人:需要课程设计交付物的在校生、想用桌面程序练手 JDBC 与事务的 Java 初学者、以及要给单位做内部小工具却不想上 Web 项目的后端工程师。这个标题真正的价值不在“界面好看”,而在“从登录到报表一个环节不缺”。
2. 用Swing+MySQL把最小骨架跑通:环境选型与JDBC连接池
2.1 环境选型:JDK 8 + MySQL 5.7/8.0,为什么不用Spring Boot
先说结论:这套项目最佳环境就是 JDK 8 + MySQL 5.7 或 8.0,IDE 用 Eclipse 或 IntelliJ IDEA 都行。不引入 Spring Boot 的原因很现实:课设和实训的验收环境往往是一台没有外网、没有 Maven 私服的教学机,你能拿到的只有 JDK 和一个 MySQL 服务端。Swing 是 JDK 自带的,双击就能编译运行;JDBC 驱动打个 jar 包扔进 lib 目录就能用。这不是技术倒退,这是交付稳定性优先的务实选择。
我一般建议 MySQL 用 5.7.44 或 8.0 系,注意安装时把字符集选成 utf8mb4,排序规则选 utf8mb4_general_ci。如果装的是 MySQL 8.0,连接驱动的配置会和 5.x 有差异,这事后面避坑章专门讲。JDK 版本别追新,JDK 8 对 Swing 的渲染最稳定,高版本 JDK 在高分屏上偶尔会出现模糊或布局错位的老毛病。
2.2 先立规矩:建库建表与初始数据脚本
动手写界面之前,先建数据库。很多新手一上来就写界面,写到后面发现字段对不上,返工成本极高。正确的顺序是:先设计表结构,再写 DAO,最后布局界面。下面是这套系统最核心的四张表,我通常把它们写在一个 init.sql 里,用 Navicat 或命令行直接执行。
CREATE DATABASE IF NOT EXISTS material_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE material_db; -- 用户表:登录模块的唯一数据来源 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) ) ENGINE=InnoDB; -- 物资分类表:parent_id 为 0 表示一级分类,支持无限层级 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, parent_id INT DEFAULT 0, name VARCHAR(100) NOT NULL ) ENGINE=InnoDB; -- 物资信息表:stock 是实时库存,safety_stock 是低库存预警线 CREATE TABLE t_material ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(50) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, category_id INT, spec VARCHAR(100), unit VARCHAR(20), stock INT DEFAULT 0, safety_stock INT DEFAULT 0, remark VARCHAR(255) ) ENGINE=InnoDB; -- 出入库流水表:table_type 用 1 表示入库、2 表示出库 CREATE TABLE t_stock_io ( id INT PRIMARY KEY AUTO_INCREMENT, material_id INT NOT NULL, io_type TINYINT NOT NULL COMMENT '1=入库 2=出库', quantity INT NOT NULL, operator VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;逻辑说明:t_user 用于登录,密码我建议存 MD5 摘要而不是明文;t_category 用 parent_id 实现树形结构,这是 Swing 里 JTree 的数据基础;t_material 的 stock 字段是冗余设计的,虽然违反第三范式,但换来了查询速度——你不需要每次 SUM 流水表才知道库存;t_stock_io 是流水账,任何出入库操作都往这里插一条记录,作为审计依据。
参数说明:数量字段全部用 INT,物资管理场景几乎不会出现小数。如果要支持按箱、按包入库,建议加一个 conversion 单位换算字段,但这属于进阶,基础版先不做。create_time 用 DATETIME 而不是 TIMESTAMP,避免 2038 年问题。
2.3 写一个不翻车的DBUtil:Properties配置 + 静态连接获取
Swing 项目里最怕的不是界面复杂,而是数据库连接管理混乱。常见做法是写一个 DBUtil 工具类,把 URL、用户名、密码全部放进 db.properties 配置文件,避免硬编码。这个类是整个系统的黑匣子,所有 DAO 都从它身上拿 Connection。
package com.ermu.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.util.Properties; public class DBUtil { private static String url; private static String user; private static String password; // 静态代码块只在类加载时执行一次 static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); // 8.x 驱动必须用 com.mysql.cj.jdbc.Driver Class.forName(props.getProperty("driver")); url = props.getProperty("url"); user = props.getProperty("user"); password = props.getProperty("password"); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(url, user, password); } }对应的 db.properties 文件:
driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/material_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 user=root password=123456逻辑说明:静态代码块保证驱动只注册一次,getConnection 每次调用都是全新的物理连接。对于课设级别的并发量(几十人同时用)够用了,没必要上连接池。如果日后要接入 HikariCP,只需要改这个方法内部实现,调用方不用动。
参数说明:useSSL=false 一定要加,MySQL 8.0 默认开启 SSL 验证,不加会直接报 SSL 连接错误;serverTimezone=Asia/Shanghai 用于解决时区差 8 小时的问题。只要这两项没配齐,程序跑起来就是玄学报错,而且是随机报错。
2.4 用JFrame+JTable拼出主窗体:从代码看到界面
数据库就绪后,先做一个最小主窗体验证整条链路:窗体加载时从 t_material 读数据,显示在 JTable 里。这个步骤跑通了,后面的业务功能都是在同一套模式下往里面加。
public class MainFrame extends JFrame { private JTable table; private DefaultTableModel model; public MainFrame() { setTitle("物资信息管理系统"); setSize(900, 600); setDefaultCloseOperation(EXIT_ON_CLOSE); // 表格列名 String[] columns = {"编号", "物资编码", "物资名称", "规格", "单位", "库存"}; model = new DefaultTableModel(columns, 0); table = new JTable(model); add(new JScrollPane(table), BorderLayout.CENTER); loadData(); // 从数据库加载 setLocationRelativeTo(null); setVisible(true); } private void loadData() { try (Connection conn = DBUtil.getConnection(); Statement st = conn.createStatement(); ResultSet rs = st.executeQuery( "SELECT id, code, name, spec, unit, stock FROM t_material")) { while (rs.next()) { model.addRow(new Object[]{ rs.getInt("id"), rs.getString("code"), rs.getString("name"), rs.getString("spec"), rs.getString("unit"), rs.getInt("stock") }); } } catch (Exception e) { JOptionPane.showMessageDialog(this, "加载失败: " + e.getMessage()); } } }逻辑说明:这里用 DefaultTableModel 而非自定义 TableModel,是为了省事。addRow 每次调用都会触发表格重绘,数据量在几千条以内完全无感。窗口布局用 BorderLayout,表格放在 CENTER 区域,未来左侧放分类树的 JTree、顶部放工具栏就齐活了。
参数说明:new DefaultTableModel(columns, 0) 第二个参数是初始行数,传 0 表示空表格然后逐行 addRow。setSize(900, 600) 是常规桌面分辨率下限,低于这个在 1366×768 的笔记本上会显得拥挤。这一步做完你应该能看到一张能显示数据库内容的表格,看不到就说明 JDBC 链路有问题,回头检查 2.3 的配置。
3. 物资管理的核心业务:分类树、资料维护、出入库与库存账
3.1 用带勾选框的JTree做物资分类:勾选联动与节点展开的坑
物资分类用树形结构展示是最直观的,JTree 本身支持默认节点,但要实现“勾选框勾选分类,下方表格只显示该分类下物资”的效果,得动点手脚。热词里提到的“swing 带勾选框的 tree”正是这个功能的搜索入口,这里给出一个可复现的做法。
class CheckBoxTreeNode extends DefaultMutableTreeNode { private boolean selected = false; public CheckBoxTreeNode(Object userObject) { super(userObject); } public boolean isSelected() { return selected; } public void setSelected(boolean selected) { this.selected = selected; // 联动子节点:勾选父节点时,子节点全部跟随 for (int i = 0; i < getChildCount(); i++) { Object child = getChildAt(i); if (child instanceof CheckBoxTreeNode) { ((CheckBoxTreeNode) child).setSelected(selected); } } } // 关键:重写 children(),让 JTree 正确识别勾选状态 @Override public Enumeration<?> children() { return super.children(); } }逻辑说明:这个自定义节点的核心是 setSelected 方法里的递归遍历——勾选一级分类时,它的所有子分类自动同步勾选状态,这是树形勾选最常见的交互预期。重写 children() 是为了确保 JTree 的展开和折叠逻辑不被自定义字段干扰,很多 Swing 开发者在这里翻车:不重写时节点表现为“能勾选但展开后子节点状态错乱”。
渲染器部分需要把默认的文本渲染器换成 JCheckBox 与文本的组合,核心是把 TreeCellRenderer 的 getTreeCellRendererComponent 返回一个带勾选框的面板。参数上注意:树节点数据加载时用 t_category 表的 parent_id 递归构建,一级节点的 parent_id 为 0。勾选状态一般不用持久化到数据库——它是查询条件,不是业务数据,关掉软件重新打开后清空是正常行为。
3.2 物资资料维护:TableModel封装与增删改查
物资资料的增删改查是整个系统最枯燥也最核心的部分。这里建议抛掉 DefaultTableModel,自己继承 AbstractTableModel,把对表的修改操作收敛到一个类里,界面代码会干净很多。
public class MaterialTableModel extends AbstractTableModel { private String[] columns = {"编码", "名称", "分类", "规格", "单位", "库存", "预警线"}; private List<Material> list = new ArrayList<>(); @Override public int getRowCount() { return list.size(); } @Override public int getColumnCount() { return columns.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Material m = list.get(rowIndex); switch (columnIndex) { case 0: return m.getCode(); case 1: return m.getName(); case 2: return m.getCategoryName(); case 3: return m.getSpec(); case 4: return m.getUnit(); case 5: return m.getStock(); case 6: return m.getSafetyStock(); default: return null; } } @Override public String getColumnName(int column) { return columns[column]; } public void setData(List<Material> data) { this.list = data; fireTableDataChanged(); } public Material getRow(int row) { return list.get(row); } }逻辑说明:这个模型把表格列和 Material 实体类字段做了映射。删除操作先 getRow 拿到当前选中行对象,再拿它的 id 执行 DELETE SQL,最后调用 setData 刷新。编辑操作建议单独弹一个 JDialog 表单,不要在表格里直接改值——物资编码和名称这类字段在表格里直接编辑时,用户很容易误触,而且校验逻辑写起来更麻烦。
参数说明:getColumnName 必须重写,否则表头显示“A、B、C”而不是中文。fireTableDataChanged 是通知 JTable 重绘的关键,漏掉它表格不会刷新。名归类字段需要连表查询拿分类名称,SQL 用 LEFT JOIN t_category,而不是单纯存一个 categoryId 显示数字——这是很多初稿被老师批“不友好”的重灾区。
3.3 入库与出库:事务、行锁与流水表一次写完
入库和出库是本系统的核心操作,也是最容易出数据不一致的地方。常见错误写法是:先 UPDATE 库存,再 INSERT 流水,两步中间没加事务。一旦第二步抛异常,库存变了流水没记,账目就对不上。正确做法是把两步包在一个事务里,同时对库存行加锁。
public void inStock(int materialId, int quantity, String operator) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 锁定库存行,防止并发重复入库 String sqlLock = "SELECT stock FROM t_material WHERE id = ? FOR UPDATE"; try (PreparedStatement ps = conn.prepareStatement(sqlLock)) { ps.setInt(1, materialId); ps.executeQuery(); } // 2. 更新库存 String sqlUpdate = "UPDATE t_material SET stock = stock + ? WHERE id = ?"; try (PreparedStatement ps = conn.prepareStatement(sqlUpdate)) { ps.setInt(1, quantity); ps.setInt(2, materialId); ps.executeUpdate(); } // 3. 写入流水 String sqlInsert = "INSERT INTO t_stock_io(material_id, io_type, quantity, operator) VALUES(?, 1, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(sqlInsert)) { ps.setInt(1, materialId); ps.setInt(2, quantity); ps.setString(3, operator); ps.executeUpdate(); } conn.commit(); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (Exception ex) { ex.printStackTrace(); } } throw new RuntimeException("入库失败: " + e.getMessage(), e); } finally { if (conn != null) { try { conn.close(); } catch (Exception e) { } } } }逻辑说明:SELECT ... FOR UPDATE 是悲观锁,在事务提交前锁住 t_material 表对应行。出库操作的代码和入库几乎一样,只是 UPDATE 时改成 stock - quantity,并在执行前校验一下当前库存是否足够。这里用事务把所有 SQL 包起来,任何一步失败都会整体回滚,库存和流水永远保持一致。
参数说明:conn.setAutoCommit(false) 之后,commit 和 rollback 必须成对出现。finally 块里关闭连接时要小心——connection.close() 会自动回滚未提交的事务,所以在关闭前一定先走完 commit 或 rollback。operator 参数是当前登录用户的 real_name,这为实现“谁领的、谁入的账”提供审计线索。
3.4 库存查询与低库存预警:GROUP BY、HAVING 与排序的实战
库存查询页面通常要支持按名称模糊搜索、按分类过滤、按库存量排序,并高亮显示低于预警线的物资。这节的 SQL 是整系统里最需要思考的部分,尤其是排序和聚合的组合使用。
SELECT m.id, m.code, m.name, m.spec, m.unit, m.stock, m.safety_stock, c.name AS category_name FROM t_material m LEFT JOIN t_category c ON m.category_id = c.id WHERE m.name LIKE CONCAT('%', ?, '%') AND (? = 0 OR m.category_id = ?) ORDER BY m.stock ASC;逻辑说明:排序用 ORDER BY m.stock ASC,把库存最少的物资排在前面,配合 JTable 行渲染器把 stock 小于 safety_stock 的行背景标红,低库存预警就完成了。参数里 ? = 0 OR m.category_id = ? 这个写法是 MyBatis 动态 SQL 的半成品替代方案——当分类参数传 0 时,条件自动失效,查询全部分类。
参数说明:LIKE 拼接用 CONCAT 而不是直接塞参数,可以防止 SQL 注入。ORDER BY 方向建议用户点击表头切换,这需要给 JTable 加 TableRowSorter,Swing 自带的排序器足够应付单列排序。如果要做“按月末库存排序”,就需要在列表页拿子查询结果,这里不做展开。
4. 把报表讲清楚:SQL统计 + 图表的落地写法
4.1 月度出入库统计:三条SQL解决老板问题
物资系统做到最后,使用者最关心的是“这个月入了什么、出了什么、还剩什么”。SQL 统计报表不需要引入任何报表引擎,三条带 DATE_FORMAT 的聚合查询足够覆盖绝大多数需求。下面这条统计每个月入库总量的 SQL 是最常用的。
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, SUM(CASE WHEN io_type = 1 THEN quantity ELSE 0 END) AS total_in, SUM(CASE WHEN io_type = 2 THEN quantity ELSE 0 END) AS total_out FROM t_stock_io WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month DESC;逻辑说明:CASE WHEN 把入库和出库数量摊到两个列上,一行就是一个月的出入汇总。DATE_FORMAT 在 GROUP BY 里直接对格式化后的字符串分组,这是 MySQL 支持的做法。结果集三列数据直接灌进 JTable 的 TableModel,一个统计面板就完成了。
参数说明:WHERE 条件限定了最近 6 个月,避免统计表无限膨胀。ORDER BY month DESC 是让最新月份显示在最上面,对应热词里的 mysql 排序需求。DATE_SUB(CURDATE(), INTERVAL 6 MONTH) 是 MySQL 的日期计算函数,跨年场景会自动进位,不用自己拼月份字符串。
4.2 用JFreeChart把月度数据画成柱状图:坐标轴与数据集的配置
表格式报表终究不够直观,把月度出入库画成柱状图是“高分”项目的加分项。Swing 生态里用 JFreeChart 最顺手,引入 jfreechart jar 包后,核心是把上面的查询结果构造成数据集。
// 构造柱状图数据集 DefaultCategoryDataset dataset = new DefaultCategoryDataset(); while (rs.next()) { String month = rs.getString("month"); int totalIn = rs.getInt("total_in"); int totalOut = rs.getInt("total_out"); dataset.addValue(totalIn, "入库", month); dataset.addValue(totalOut, "出库", month); } JFreeChart chart = ChartFactory.createBarChart( "近6个月出入库统计", // 主标题 "月份", // X轴标签 "数量", // Y轴标签 dataset, PlotOrientation.VERTICAL, true, // 显示图例 true, // 生成工具提示 false // 不生成URL ); // 将图表面板嵌入 Swing 窗体 ChartPanel chartPanel = new ChartPanel(chart); chartPanel.setPreferredSize(new Dimension(600, 300));逻辑说明:DefaultCategoryDataset 的 addValue 三个参数分别是数值、系列名、分类名,JFreeChart 用系列名生成图例里的柱状颜色区分。ChartFactory.createBarChart 返回 JFreeChart 对象,ChartPanel 是 Swing 组件,直接 add 到 JPanel 即可。如果不想引入第三方 jar,也可以用 JPanel 的 paintComponent 自绘柱状图,但坐标轴刻度、抗锯齿这些都要自己处理,投入产出比太低。
参数说明:PlotOrientation.VERTICAL 决定柱子是竖直还是水平。createBarChart 最后一个参数传 false,会跳过一个 URL 生成器,这个功能在桌面端完全没有意义。图表标题不要写死“统计报表”,用当前年份拼接更专业。
4.3 导出CSV报表:用 UTF-8 BOM 解决 Excel 中文乱码
报表做出来后,总有人要求“导出来发给我”。最轻量可靠的方案是导出 CSV,但有个坑:直接用 FileWriter 写 UTF-8 编码的 CSV,用 Excel 打开时中文会乱码。原因在于 Excel 需要文件头有 BOM 标记才能正确识别 UTF-8,解决方法是写文件前先写入 BOM 字节。
try (FileOutputStream fos = new FileOutputStream("库存报表.csv"); OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) { // 写入 UTF-8 BOM,否则 Excel 打开中文乱码 fos.write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}); osw.write("物资编码,物资名称,库存,预警线\n"); for (Material m : list) { osw.write(m.getCode() + "," + m.getName() + "," + m.getStock() + "," + m.getSafetyStock() + "\n"); } } catch (IOException e) { JOptionPane.showMessageDialog(this, "导出失败: " + e.getMessage()); }逻辑说明:注意必须先写 BOM 再写入内容,顺序反了 Excel 依然乱码。CSV 转义规则很简单,字段含逗号时用双引号包裹,字段含双引号时把双引号换成两个双引号,这套基本够用。导出按钮触发时用 SwingWorker 包一层,因为文件写入在数据量大时还是有点延迟的。
参数说明:文件名用“库存报表.csv”是硬编码,实际开发中拼接当前日期更合理,比如 String.format("库存报表_%s.csv", LocalDate.now())。标准库的 OutputStreamWriter 负责字符编码转换,不要用 FileWriter——它只按平台默认编码写,无法强制 UTF-8。
5. 物资信息管理系统避坑指南:五个我亲手踩过的坑
5.1 MySQL 8.0 连接报 SSL 连接错误,程序时好时坏
现象:连接代码第一次运行正常,第二次启动直接抛”SSL connection error”,或者提示”Public Key Retrieval is not allowed”。 原因:MySQL 8.0 默认开启 SSL 与 caching_sha2_password 认证,JDBC 驱动在第一次握手时需要获取 RSA 公钥,但配置里没放开权限。 解决:在连接 URL 尾部加上 useSSL=false 和 allowPublicKeyRetrieval=true,完整写法是 jdbc:mysql://localhost:3306/material_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true。这个坑在 2.3 的配置里出现过一次,但很多同学改完 URL 忘了重启 MySQL 服务,报错照旧。
5.2 驱动类名加载失败:NoClassDefFoundError
现象:程序编译通过,运行到 Class.forName 那行抛 java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。 原因:mysql-connector-java 5.x 版本的驱动类是 com.mysql.jdbc.Driver,8.x 版本改成了 com.mysql.cj.jdbc.Driver。如果拿 5.x 驱动去跑 8.x 的 MySQL,或者反过来,都会加载失败。 解决:统一使用 MySQL Connector/J 8.0.33,驱动类名固定写 com.mysql.cj.jdbc.Driver。另外确认 jar 包真的在构建路径里——IDEA 里双击 lib 目录下的 jar,看模块依赖是否勾选,这是个非常隐蔽的翻车点。
5.3 Swing 界面卡成白板:数据库操作把事件线程堵死了
现象:点击“查询”按钮后,整个窗体无法拖动,鼠标变成转圈状态,几秒到几十秒后才恢复。 原因:JTable 的数据加载、SQL 查询这些耗时操作直接写在了按钮的事件回调里,也就是 EDT(事件分发线程)上。EDT 被堵住,界面重绘、鼠标响应全部排队,看起来就像死机。 解决:耗时操作丢进 SwingWorker。doInBackground 里执行 JDBC 查询,done 方法里更新 JTable。习惯是任何查询按钮的事件处理器都只做一件事:new LoadDataTask().execute(),不做任何数据库操作。
5.4 出入库数量对不上账:事务忘写了,或者SELECT之后忘了锁
现象:入库流程测试时单次操作正常,连续快速录入多笔时,库存数字和流水累计对不上,偶尔还会出现库存变成负数。 原因:两步 SQL 之间没有事务包裹,UPDATE 成功、INSERT 失败时数据就丢了;更隐蔽的是并发场景下两个窗口同时入库,两个事务都读到旧库存,后提交的覆盖先提交的。 解决:按 3.3 的模板,完整事务 + SELECT ... FOR UPDATE 行锁。自己测试时开两个窗口同时提交,确认不会互相覆盖。这里没有玄学,只有事务边界和锁的范围。
5.5 中文乱码与打包后连不上库:编码和配置文件路径
现象:自己电脑运行正常,打包成 jar 发给同学,他打开后要么数据库中文全是问号,要么直接报空指针找不到 db.properties。 原因:打包时 db.properties 没有被打进 jar 的 classpath,或者编译时源文件编码不是 UTF-8。中文乱码通常是因为 JVM 默认字符集与文件编码不一致。 解决:IDEA 里把云编码统一设置为 UTF-8,包括源文件、properties 文件和控制台。db.properties 放 src/main/resources 目录,打包确认 jar 里能看到它。读取用 DBUtil 里的 getResourceAsStream 方式,不要依赖绝对路径——别人电脑上没有你那个 D:\project 路径。最后在代码启动第一行输出一个 System.out.println("编码验证:" + Charset.defaultCharset()),查看运行环境是否按预期用 UTF-8。
6. 把“高分”再往上顶一档:打包交付与文档说明里的加分细节
项目代码写完只完成了一半,另一半是交付形态。这份源码能跑还不算高分,能让人快速跑起来才是。打包时我用 maven-shade 插件打一个 fat-jar,把 MySQL 驱动 jar 合并进去,这样对方不用手动建 lib 目录,双击 java -jar material.jar 就启动,JDK 8 环境零依赖。如果对方机器连 JDK 都没有,再用 exe4j 或 jpackage 生成 exe,这一步能规避掉九成环境问题。
文档说明里最值得花时间的不是操作手册,而是一张 ER 图——用 draw.io 画 t_user、t_category、t_material、t_stock_io 四张表的关系,主键外键标清楚。第二重要的是数据流图:登录后进入主界面,左侧分类树约束右侧物资表格,点击入库按钮弹表单、填数量、提交事务,最后流水表多一条记录。把这两张图放进去,再配核心代码的注释截图,评分老师一眼就知道你把系统架构想明白了。最后一条个人习惯:交付前我会把数据库导出成 init.sql,让验收者一键重建环境,而不是发给他一个已经运行过的数据文件——新环境跑不起来时,这招是后悔药。
回看这个项目,技术难点不在复杂的架构设计,而在于每一条数据流都要闭环:登录要能退、入库要能查、出库要能追、报表要能导。我自己带过的团队里,凡是认真把这一套做完的人,后面对 JDBC 事务、Swing 事件模型、MySQL 聚合查询的理解都相当扎实。这套组合的边界也清楚:适合内部小范围使用,不适合高并发生产环境,真要上线再考虑搬 Web 框架。希望这份拆解能帮你少踩几个坑,把力气花在业务细节上。
本文还有配套的精品资源,点击获取