1. 项目概述与需求拆解
1.1 为什么选择做二手商品管理系统
我最早接到这个题目的时候,第一反应是:这年头谁还用Swing写管理系统?但真正做完之后,反而觉得这个组合挺有意思的。二手商品管理系统解决的是一个非常经典的信息管理问题——有买卖双方,有商品信息,有交易记录,还要考虑商品状态的流转。用Swing来实现,在很多高校课程设计和入门项目里仍然是很主流的选型。
先说说需求端。二手交易场景其实比新商品销售更复杂,因为它多了一个“状态管理”的维度。同一样商品,可能处于“闲置中”“已被预订”“已售出”等不同状态,而且每一笔交易背后还牵扯到卖家、买家、成交价格、成交时间这些关键要素。单纯做成一个CRUD的增删改查系统不难,但如果能把商品生命周期和交易流程串联起来,这个项目的含金量就完全不一样了。
从功能模块上看,一个完整可用的二手商品管理系统至少要覆盖这几块:
- 用户管理:注册、登录、个人信息维护
- 商品管理:发布商品、修改商品信息、下架商品
- 分类管理:商品分类的增删改查,方便用户筛选
- 交易管理:下单购买、订单状态跟踪、交易记录
- 搜索与筛选:按关键词、价格区间、分类等条件查找商品
1.2 目标用户与核心使用场景
这个系统的目标用户很明确,就是校园里的大学生或者社区里的居民。这类用户群体有一个共性:手上有闲置物品想出手,但又不愿意走大平台的繁琐流程。所以系统的操作路径应该尽量短,比如发布商品从点击按钮到信息填完,应该控制在两三分钟内完成。
使用场景可以这样理解:小王有一台用了两年的iPad,想卖给学弟。他打开系统,注册账号,点击“发布商品”,上传几张实拍图,填好型号、成色、期望价格,提交之后就等着买家来联系。另一边,想买iPad的小李通过分类浏览或关键词搜索找到了这台iPad,看到成色描述和价格都合适,直接下单购买。卖家收到订单提醒后,线下完成交付,在系统里把订单状态改为“已完成”。一整个流程下来,系统就像一个线上的跳蚤市场管理员,把商品信息和交易记录都管得清清楚楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Swing技术选型解析
2.1 为什么在这个项目中选Swing
我见过不少人在技术选型上纠结很久,其实对于这类桌面端管理系统,Swing依然是性价比很高的选择。没有前端三件套的割裂感,没有Maven依赖地狱,一个JDK装好就能开干,这对教学和课程设计来说非常友好。
Swing是Java自带的GUI工具包,从JDK 1.2开始就内置了,最大的优势就是跨平台。同一个jar包,在Windows、macOS、Linux上跑起来界面表现基本一致,不需要额外打包适配。对于Windows开发、Windows演示、可能还要在别的机器上答辩的场景,Swing的跨平台特性减少了很多环境冲突。
另一个让我坚持选Swing的原因是:它和Java的配合实在太天然了。业务逻辑用Java写,界面也用Java写,全程不用切换语言上下文。项目里需要操作数据库的话,直接JDBC或者MyBatis接上就行,不需要像Web项目那样搞一堆Controller、Service、Mapper的分层映射,代码量能省下不少。
2.2 Swing与JavaFX、Web方案的对比
很多人会问:既然都是Java生态,为什么不选JavaFX?或者干脆做Web系统?这里我做一个直观的对比:
| 对比维度 | Swing | JavaFX | Web(SSM/Spring Boot) |
|---|---|---|---|
| 学习曲线 | 平缓,组件API简单 | 稍陡,FXML和属性绑定要额外学 | 陡峭,前后端都要掌握 |
| 运行环境 | JDK内置,无需额外依赖 | JDK 8后需要单独引入模块 | 需要部署Tomcat或内嵌容器 |
| 界面表现力 | 一般,偏传统 | 较强,支持CSS美化 | 取决于前端技术栈 |
| 团队协作 | 单人即可完成 | 适合单人 | 需要前后端分工 |
| 部署复杂度 | 打jar包直接运行 | 打jar包需注意模块依赖 | 需要服务器环境 |
| 适合场景 | 课程设计、小型管理系统 | 中型桌面应用 | 互联网级应用 |
从这个表格能看出来,Swing的定位非常清晰:它不是为了做炫酷的界面而生的,但做内部管理系统这种偏“工具属性”的软件,完全够用。尤其是课程设计和中小型项目答辩,评委关注的核心其实是业务逻辑是否完整、数据库设计是否合理、代码结构是否清晰,这些方面Swing完全能扛住。
2.3 技术栈配套方案
选定了Swing之后,配套的技术栈也要仔细考虑。我的建议是这样一套组合:
- JDK版本:JDK 8或JDK 11。别追新用JDK 17以上的版本,因为部分老教程里的Swing代码在新版本上会有一点点细节差异,而且JDK 8在课程设计中认可度最高,环境问题最少。
- 界面布局:纯代码布局,不用IDE的可视化拖拽。可视化布局生成的代码又长又乱,后期改一个组件的尺寸可能牵扯到几十行代码的变动,完全是给自己挖坑。
- 数据库:MySQL 5.7或8.0,通过JDBC连接,也可以封装一层DBUtil工具类,简化连接管理。
- 连接池:如果项目体量不大,直接用DriverManager获取连接就够了。想体现专业性,可以引入Druid或C3P0,但核心逻辑是一样的——获取连接、执行SQL、关闭资源。
这套组合最大的好处就是“清爽”:依赖少、环境简单、代码可控,即使整个项目重构,核心逻辑也不会受到框架绑架。
3. 系统架构与数据库设计
3.1 分层架构设计思路
很多初学者写Swing项目,最喜欢把所有代码一股脑塞进事件监听器里。点击一个按钮,下面跟着二三十行代码:获取文本框内容、拼SQL、查数据库、解析结果、刷新表格。这种写法的痛点非常明显:改一个功能的时候要像考古一样在几百行代码里翻找逻辑,排查问题的成本极高。
我在这套系统里采用了经典的三层架构:
- 视图层(View):只负责界面的展示和事件的触发,包括JFrame、JPanel、JTable这些组件的布局,以及对用户操作的响应入口。
- 控制层(Controller):接收视图层传来的操作指令,调用业务逻辑层的方法,并把结果封装好返回给视图层。在Swing里,控制层通常由事件监听器承担,但我建议把监听器里的逻辑单独抽到Controller类里,而不是全部写在匿名内部类中。
- 业务逻辑层(Service)与数据访问层(DAO):Service负责业务规则的校验和流程控制,DAO负责与数据库交互。这两层可以分开,也可以合并,具体看代码规模。但我强烈建议至少保留一个大致的边界,哪怕不单独建类,也要在注释或者方法命名上区分开。
分层之后,核心收益是:界面和逻辑解耦,以后想从Swing换成JavaFX,或者从桌面端扩展出Web端,底层的Service和DAO可以原样复用。
3.2 数据库表结构设计
数据库设计决定了系统能跑多远。我的建议是至少设计五张表:用户表、商品分类表、商品表、订单表、订单明细表。下面给出每张表的核心字段设计思路。
用户表(t_user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| user_id | INT 主键自增 | 用户ID |
| username | VARCHAR(50) 唯一 | 登录用户名 |
| password | VARCHAR(100) | 登录密码,建议MD5加密 |
| nickname | VARCHAR(50) | 昵称 |
| phone | VARCHAR(20) | 联系电话 |
| create_time | DATETIME | 注册时间 |
密码加密这点特别重要。很多Swing课程设计项目都是明文存密码,这在实际系统中是绝对不能接受的。MD5加密虽然已经被认为不够安全,但作为课程设计和学习项目已经比明文强出几个量级,而且实现成本极低。
商品分类表(t_category)
| 字段名 | 类型 | 说明 |
|---|---|---|
| category_id | INT 主键自增 | 分类ID |
| category_name | VARCHAR(50) | 分类名称,如数码、书籍、生活用品 |
商品表(t_product)
| 字段名 | 类型 | 说明 |
|---|---|---|
| product_id | INT 主键自增 | 商品ID |
| seller_id | INT | 卖家用户ID,外键关联t_user |
| category_id | INT | 分类ID,外键关联t_category |
| title | VARCHAR(100) | 商品标题 |
| description | TEXT | 商品描述 |
| price | DECIMAL(10,2) | 期望价格 |
| original_price | DECIMAL(10,2) | 原价,用于显示折扣力度 |
| status | TINYINT | 商品状态:1在售 2已预订 3已售出 0下架 |
| image_path | VARCHAR(255) | 商品图片存储路径 |
| create_time | DATETIME | 发布时间 |
| update_time | DATETIME | 更新时间 |
status字段是整个系统状态流转的核心。商品能不能被搜到、能不能下单,都依赖这个字段做条件过滤。必须养成好习惯:在代码里定义状态常量,而不是到处散落魔法数字。
订单表(t_order)
| 字段名 | 类型 | 说明 |
|---|---|---|
| order_id | INT 主键自增 | 订单ID |
| order_no | VARCHAR(32) | 订单编号,可用时间戳生成 |
| product_id | INT | 商品ID |
| seller_id | INT | 卖家ID |
| buyer_id | INT | 买家ID |
| price | DECIMAL(10,2) | 成交价格 |
| status | TINYINT | 订单状态:1待确认 2已完成 3已取消 |
| create_time | DATETIME | 下单时间 |
| finish_time | DATETIME | 完成时间 |
为什么订单表里要同时冗余商品名称和卖家信息?因为订单一旦生成,商品后续可能被修改甚至下架。如果只存product_id,查询历史订单时还需要join商品表,一旦商品被删,历史订单数据就悬空了。少量的数据冗余在管理系统中是非常常见的做法,是安全性优先于规范化的体现。
3.3 数据库连接的封装
我自己写这类项目时,一定会先写一个DBUtil工具类,把获取连接和关闭资源的样板代码统一管理。这种做法看起来只是省了几行代码,实际价值是:全项目的数据库连接方式统一,以后改数据库地址、账号密码只需要改一处。
java复制public class DBUtil {
private static final String URL = "jdbc:mysql://localhost:3306/second_hand?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8";
private static final String USER = "root";
private static final String PASSWORD = "123456";
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(ResultSet rs, PreparedStatement ps, Connection conn) {
if (rs != null) {
try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (ps != null) {
try { ps.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
}
}
这里特别注意,JDBC驱动类的写法在新旧版本MySQL驱动中不一样。旧版用com.mysql.jdbc.Driver,8.0以后必须用com.mysql.cj.jdbc.Driver,同时连接串里要加上serverTimezone参数,否则很容易报时区相关的异常。这些坑我后文会专门展开。
4. 核心模块实现与代码解析
4.1 登录与注册模块
登录模块是整个系统的入口,它决定了用户的身份和权限。我在设计时主要做了三个方面的控制:格式校验、身份验证、会话保持。
先看登录界面的核心逻辑。用户在用户名框和密码框输入信息,点击登录按钮后,事件监听器里会做以下几步:
java复制private void loginActionPerformed(ActionEvent e) {
String username = usernameField.getText().trim();
String password = new String(passwordField.getPassword());
if (username.isEmpty() || password.isEmpty()) {
JOptionPane.showMessageDialog(this, "用户名和密码不能为空");
return;
}
if (username.length() < 3 || username.length() > 20) {
JOptionPane.showMessageDialog(this, "用户名长度需在3-20个字符之间");
return;
}
String md5Password = MD5Util.encode(password);
User user = userService.login(username, md5Password);
if (user != null) {
currentUser = user;
dispose();
new MainFrame(currentUser).setVisible(true);
} else {
JOptionPane.showMessageDialog(this, "用户名或密码错误");
}
}
注意密码框我用的是getPassword()而不是getText(),这是Swing的一个安全设计,它返回的是char数组而不是String,能减少密码字符串在内存中滞留的时间。虽然在这个层面提升的安全性是有限的,但作为代码习惯非常加分。
注册模块要注意的则是用户名唯一性检查。在DAO层插入新用户之前,首先执行一条SELECT查询判断用户名是否存在,如果存在就提示用户换一个。这里容易踩的坑是并发情况下的重复注册,但在桌面系统里并发概率很小,先查再插就足够用了。
4.2 商品发布与图片处理
商品发布是二手交易系统的核心操作,它比单纯的增删改查多了几个关键点:图片处理、价格校验、状态初始化。
Swing里接收图片,最通用的方式是用JFileChooser弹出文件选择框,让用户选一张图片,然后把这个图片的路径存到数据库,而不是把图片字节存到数据库里。在界面上展示时,再用ImageIcon加载路径并缩放显示。
java复制private void chooseImageActionPerformed(ActionEvent e) {
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new FileNameExtensionFilter("图片文件", "jpg", "jpeg", "png", "gif"));
int result = chooser.showOpenDialog(this);
if (result == JFileChooser.APPROVE_OPTION) {
File selectedFile = chooser.getSelectedFile();
imagePath = selectedFile.getAbsolutePath();
ImageIcon icon = new ImageIcon(imagePath);
Image scaledImage = icon.getImage().getScaledInstance(80, 80, Image.SCALE_SMOOTH);
imageLabel.setIcon(new ImageIcon(scaledImage));
}
}
图片存储路径有个很讲究的细节:不要直接存getAbsolutePath()这种带盘符的绝对路径,因为换一台机器路径就可能失效。更靠谱的方式是把图片复制到项目下的upload目录里,数据库中只存相对路径。这样项目打包后,图片能跟随项目目录走,可移植性大大增强。
价格校验方面,除了RegularExpression正则判断输入是否为合理的数字,我还会额外判断价格是否大于0以及小数点后不超过两位。别小看这些细节,实际运行中用户什么奇怪的数据都可能输入进去。
商品发布成功之后,注意把新商品的状态初始化为“在售”,同时刷新商品列表表格Model,保证用户能立刻看到自己发布的商品出现在列表中。
4.3 商品列表展示与查询
商品列表是二手商城最核心的浏览入口。这里Swing的JTable是最常用的组件,但它有个让人头疼的地方——默认的TableModel不能直接把对象列表往里塞,需要手动把数据转成二维数组或Vector。
我这里写了一个通用的填充方法,把List
java复制public void refreshTableData(List<Product> products) {
DefaultTableModel model = (DefaultTableModel) productTable.getModel();
model.setRowCount(0);
String[] columnNames = {"商品ID", "标题", "价格", "分类", "卖家", "发布时间"};
model.setColumnIdentifiers(columnNames);
for (Product p : products) {
if (p.getStatus() == 0) {
continue; // 下架商品不显示
}
Object[] row = {
p.getProductId(),
p.getTitle(),
p.getPrice(),
p.getCategoryName(),
p.getSellerName(),
p.getCreateTime()
};
model.addRow(row);
}
}
这个查询逻辑里有个关键点:表格里展示的应该是一个“扁平化”后的商品对象,包含分类名称、卖家昵称这些关联信息,而不是直接放ID。要拿到这些信息,最简单的方案是写一个联表查询的SQL,把product表、category表、user表join起来查。这样一次查询就把表格需要的数据都拿全了,避免了循环里反复查询数据库的N+1问题。
搜索功能也是围绕这个表格做的。我在搜索框的按键事件里加了实时搜索,用户每输入一个字符就刷新一次表格。实现方式是在SQL的WHERE条件里动态拼接。为了防止SQL注入,拼接时不用字符串相加,而是用PreparedStatement的参数占位符。
java复制public List<Product> searchProducts(String keyword, Integer categoryId, Double minPrice, Double maxPrice) {
StringBuilder sql = new StringBuilder("SELECT ... FROM t_product p ");
sql.append("JOIN t_category c ON p.category_id = c.category_id ");
sql.append("JOIN t_user u ON p.seller_id = u.user_id ");
sql.append("WHERE p.status != 0 ");
List<Object> params = new ArrayList<>();
if (keyword != null && !keyword.isEmpty()) {
sql.append("AND (p.title LIKE ? OR p.description LIKE ?) ");
params.add("%" + keyword + "%");
params.add("%" + keyword + "%");
}
if (categoryId != null) {
sql.append("AND p.category_id = ? ");
params.add(categoryId);
}
if (minPrice != null) {
sql.append("AND p.price >= ? ");
params.add(minPrice);
}
if (maxPrice != null) {
sql.append("AND p.price <= ? ");
params.add(maxPrice);
}
sql.append("ORDER BY p.create_time DESC");
// 执行查询并返回结果
}
这样写的好处非常明显:一是参数合法性在Service层校验过后才拼接条件,避免无效条件;二是所有条件都走PreparedStatement参数绑定,彻底避免了拼接SQL注入的风险;三是查询条件按需增加,后续想加“按成色筛选”或者“按发布时间范围筛选”,只需要在这个方法里多增加两个if分支,扩展性很好。
4.4 交易流程与订单状态管理
交易是这个系统里业务逻辑最复杂的环节,也是最容易出bug的地方。我设计的交易流程是这样的:
- 买家在商品列表里选中一个“在售”状态的商品,点击“立即购买”按钮
- 系统弹出确认框,显示商品标题、价格、卖家信息
- 买家确认后,系统执行下单操作:
- 将商品状态从“在售”改为“已预订”
- 生成一条订单记录,状态为“待确认”
- 卖家在“卖出的订单”列表里看到待确认订单,确认无误后点击“确认成交”
- 订单状态改为“已完成”
- 商品状态改为“已售出”
- 如果线下交易没谈拢,卖家或买家可以取消订单,商品回到“在售”状态
这里我踩过一个大坑:下单时更新商品状态和插入订单记录,是两条SQL语句,如果第一条执行成功而第二条失败,就会出现商品显示“已预订”但订单没生成的数据错乱。解决办法也很简单——加上事务控制。
java复制public boolean createOrder(Order order) {
Connection conn = null;
try {
conn = DBUtil.getConnection();
conn.setAutoCommit(false);
OrderDAO orderDAO = new OrderDAO();
ProductDAO productDAO = new ProductDAO();
// 检查商品是否仍在售(乐观锁)
Product product = productDAO.findByIdForUpdate(conn, order.getProductId());
if (product == null || product.getStatus() != 1) {
conn.rollback();
return false;
}
productDAO.updateStatus(conn, order.getProductId(), 2);
orderDAO.insert(conn, order);
conn.commit();
return true;
} catch (Exception 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 ... for update来锁定商品行,这一步是为了防止两个买家同时对同一件商品下单。虽然在桌面系统中并发程度很低,但作为技术上严谨的做法,锁行记录是值得保留的。如果不想用数据库锁,也可以用乐观锁的方式,在商品表增加version字段,通过update ... where version = ?的方式来控制冲突,两者各有优劣,但从简洁性来看数据库行锁更方便。
4.5 个人中心与我的商品
个人中心有两种角色视角:作为买家看“我买到的”,作为卖家看“我卖出的”。这个模块的开发重点其实是面板切换和表格刷新的交互设计——用户点击一个单选框或者Tab标签,表格的数据源就切换成不同的查询结果。
我的做法是做一个简单的主界面布局,顶部放菜单栏,中间用一个JTabbedPane放几个功能面板:商品浏览、我的商品、我的订单、个人信息。每个面板做成独立的JPanel子类,各自封装自己的组件和事件处理逻辑。这种模块化的界面设计让代码结构非常清晰。
“我的商品”面板里,用户可以看到自己发布的商品列表,并能执行三个操作:编辑商品、下架商品、删除商品。下架是一种软删除,只更改status字段;删除则是物理删除。从业务合理性来说,我推荐优先使用下架而不是删除,因为商品如果存在关联订单,物理删除会把历史数据链也切断。所以我在删除操作里加了保护逻辑:只要商品存在关联订单,就不允许物理删除,只能用下架。
5. Swing界面设计与交互体验优化
5.1 主界面布局规范
不少Swing项目的界面让人一看就知道是学生作品:控件随意摆放、大小不一、颜色刺眼、没有对齐。界面体验虽然不是项目的核心考核点,但能让阅卷老师和答辩评委第一时间产生好印象。我建议遵循以下几个基本规范:
- 统一使用布局管理器,不要用绝对定位。BorderLayout作为主框架布局,上下分别是工具栏和状态栏,中间是内容切换区域;内部面板用GridBagLayout或GridLayout做网状排列。
- 字体和字号统一。可以通过UIManager统一设置全局字体,避免不同平台上默认字体差异导致界面错位。
- 颜色克制。主体背景用浅灰或白色,按钮用默认主题,不需要刻意做成彩色。Swing的美化空间本来就有限,做得花哨反而显得不专业。
- 窗口大小固定或者最小尺寸限制。我习惯setMinimumSize(new Dimension(900, 600)),保证在小屏幕下也能正常使用。
5.2 事件处理的代码结构优化
Swing开发里写事件监听器最容易犯的毛病就是代码烂成一坨。我推荐的写法是:每个面板类内部,把事件监听器抽成独立的私有方法或者独立的内部类,一个方法只负责一件事。
java复制public class ProductManagePanel extends JPanel {
private JTable productTable;
private JButton publishButton;
private JButton editButton;
private JButton offlineButton;
public ProductManagePanel(User currentUser) {
this.currentUser = currentUser;
initComponents();
initListeners();
loadData();
}
private void initListeners() {
publishButton.addActionListener(e -> openPublishDialog());
editButton.addActionListener(e -> openEditDialog());
offlineButton.addActionListener(e -> offlineSelectedProduct());
}
private Product getSelectedProduct() {
int row = productTable.getSelectedRow();
if (row < 0) {
JOptionPane.showMessageDialog(this, "请先选择一件商品");
return null;
}
Long productId = (Long) productTable.getValueAt(row, 0);
return productService.getProductById(productId);
}
}
Lambda表达式的写法在JDK 8之后是推荐风格,比匿名内部类简洁得多。getSelectedProduct这个方法看似简单,但实际价值很大——它把“判断表格是否选中一行”这个逻辑收敛到了一个地方,三个按钮事件都会调用它,避免了三份重复代码,而且改动选中逻辑时只需动一处。
5.3 表格交互增强
再分享几个JTable增强的小细节,做完之后整个系统的使用感受会有明显提升。
第一,给表格添加点击选中整行。JTable默认的选中模式是单选列,也就是用户点哪个单元格就选哪个单元格,这在管理系统里很难看。需要设置setRowSelectionAllowed(true),最好再把列选择关掉setColumnSelectionAllowed(false)。
第二,添加双击事件。双击表格行弹出商品详情对话框,这个交互在浏览商品时可以大大减少点击次数。我用的是table.addMouseListener,在mouseClicked里判断getClickCount() == 2。
第三,表格排序。JTable自带的TableRowSorter能让用户点击列头排序,比如想看“价格从低到高”或者“发布时间最新优先”,直接点击表头就行,一行代码就能开启:
java复制TableRowSorter<TableModel> sorter = new TableRowSorter<>(model);
productTable.setRowSorter(sorter);
需要注意一个小坑:开启排序之后,getSelectedRow()返回的行号是视图行号,不是模型行号。如果要拿选中行的数据,必须先把视图行号转换成模型行号:productTable.convertRowIndexToModel(row)。这个问题我当年调试了很久才反应过来,一旦表格排序后点按钮操作数据,经常出现操作错行的情况。
6. 实战中常见的坑与排查技巧
6.1 中文乱码问题
Swing打包成jar之后,在别人电脑上跑,最常出现的问题就是中文乱码。造成这个问题的原因有两种:一是数据库连接串里没有设置characterEncoding=utf8;二是源文件编译编码不对。
先说数据库层面,我一般在JDBC连接串里会写清楚characterEncoding=utf8,并且在建表语句中明确指定DEFAULT CHARSET=utf8mb4。如果连接串没指定编码,而数据库表又是UTF-8,Tomcat和MySQL之间的字符集转换就可能出问题。桌面系统同样如此。
再说编译层面,IDEA里默认是UTF-8编码,但很多Windows机器上系统默认编码是GBK。如果项目里有人在GBK编码下修改了Java文件,重新编译后就会出现部分中文乱码。解决方案是在pom.xml的maven-compiler-plugin里配置UTF-8编码,或者在IDEA的Settings里把Project Encoding和Global Encoding统一设置为UTF-8。
6.2 MySQL驱动加载失败
Swing项目跑起来时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,这个问题十有八九是驱动jar没引入或者版本不匹配。需要注意的点:
- MySQL 5.x对应驱动版本一般是5.1.49及以下,驱动类是com.mysql.jdbc.Driver
- MySQL 8.x必须使用8.0以上的驱动,驱动类是com.mysql.cj.jdbc.Driver
- 如果是在IDEA里运行,确认Project Structure里的Libraries是否导入了驱动jar;如果打包成jar运行,确认MANIFEST.MF文件里的Class-Path是否写对了
我自己遇到比较隐蔽的一种情况是:项目里同时存在两个版本的MySQL驱动jar,导致MySQL 8的驱动类被旧版jar中的同名类覆盖,加载报错。排查方法是打开jar包看看里面是否有com.mysql.cj.jdbc.Driver这个类,没有就把它删掉,只保留对应版本。
6.3 窗口弹不出或按钮无响应
这类问题大多是出在Swing线程模型上。Swing规定所有界面更新操作必须在事件分发线程(EDT)中执行,如果在其他线程中直接修改界面组件,轻则不刷新,重则出现偶发性崩溃。
我自己手动创建过一个“后台导出订单数据”的功能,在导出线程中直接调用tableModel.addRow()往表格里加数据,结果界面上表格迟迟不刷新,等了半天才跳出来。后来才知道必须用SwingUtilities.invokeLater()把刷新操作切回EDT执行。
java复制SwingUtilities.invokeLater(() -> {
refreshTableData(orderList);
statusLabel.setText("导出完成");
});
这里也顺带提醒一个点:Swing是单线程模型,所有界面操作都在EDT上执行,所以不要在监听器里做耗时操作(比如大批量数据库查询),否则界面会卡死。正确做法是把耗时操作放到后台线程,执行完后再切回EDT更新界面。课程设计如果处理大数据量查询,这个优化就是加分项。
6.4 图片不显示
图片显示不出来的情况,十有八九是路径问题。我在系统里做了两层规避:
一是统一用相对路径。前文提到,上传商品图片时把文件复制到项目的upload目录下,数据库中存的是相对于项目根目录的路径,比如upload/iphone12.jpg。加载图片时用user.dir获取当前工作目录,拼上相对路径。
二是做一个默认图片兜底。如果图片路径为空或者图片文件不存在,我直接显示一张系统自带的default.png,而不是显示一个空白的方块。这个细节虽然简单,但能避免不少因为图片路径问题导致的界面残缺。
7. 写在最后的几点建议
从开发到调试再到答辩展示,这套基于Swing的二手商品管理系统让我重新审视了一遍Swing这个“老古董”的价值。它确实不新潮,界面也不够靓丽,但它的简单、稳定、跨平台,恰恰是课程设计和中小型项目最需要的特性。如果你正在被课题折磨,我的建议是不要花太多时间纠结“要不要换JavaFX”“要不要改成Web”,选定Swing之后把精力集中到业务逻辑和数据库设计上,收获反而更大。
我个人实际操作中还有一个体会:答辩现场最容易翻车的就是环境问题。所以打包好项目后,建议在另一台干净的新机器上完整跑一遍,确认JDK版本没问题、MySQL驱动没丢、数据库脚本能一键导入。尤其是数据库,我强烈建议把建表和初始化数据的SQL脚本保存成sql文件,并写好一个初始化说明文档。这样即使演示的机器换了,也能最快速度把环境恢复起来,给评委留下一个“这小伙子做事很稳妥”的印象。
最后再分享一个扩展小方向:如果以后想让这个系统从桌面端“升级”成真正可用的产品,可以把数据访问层直接替换成Spring Boot RESTful API,再把Swing界面换成Vue或React前端,Service层的业务逻辑基本可以原样保留。这样你等于用一次课程的作业,把Java生态里最关键的三块技术栈都摸了一遍底,这笔账怎么算都划算。
