ARTICLE DETAIL

资讯详情

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

Java课程设计实战:JDBC+Swing商品管理系统开发指南

Java课程设计实战:JDBC+Swing商品管理系统开发指南 简介面向Java初学者与大学低年级学生资源是一份完整可运行的商品管理系统项目以Java为核心、通过JDBC操作MySQL数据库、使用Swing构建图形界面涵盖商品增删改查、数据展示等典型功能有助于理解分层结构下的Java数据库开发全流程。压缩包共18个文件以10个java源文件为主按DAO、Service、Controller、UI进行分层组织同时包含6个xml配置文件含Maven或IDE工程配置并附有gitignore与iml工程描述文件整体仅16KB结构简洁、便于快速阅读源码与导入工程。该项目已有1951人学习浏览适合作为课程设计参考或入门练习素材。通过学习可获得数据库连接工具类、SQL操作封装、Swing界面事件处理等关键代码并能结合源码理解JDBC加载驱动、建立连接、处理结果集以及Swing布局与监听器的配合方式是一份能直接运行并二次改造的实用项目。1. 一个 Java 课程设计题目凭什么值得你照着做一遍商品管理系统是 Java 课程设计和期末项目中出现频率最高的题目之一这个「使用 javajdbc 连接数据库和 Java swing 图形化界面完成一个商品管理系统」基本把 Java 后端访问数据库和桌面 GUI 两条主线都占了。你用纯 JDBC 写数据访问层用 Swing 写界面层中间不夹带任何框架一套代码跑下来JDBC 的增删改查、PreparedStatement 防注入、Swing 的事件模型、表格数据绑定这些 Java 基础面试题里的核心考点全都能落在实处。适合两类人一是准备 Java 课程设计、需要能答辩能演示的在校生二是想补 Java 基础、之前只写过 Servlet 和 MyBatis 但没碰过原生 JDBC 的转行者。这个项目最大的价值不是「做出一个系统」而是让你把 Java 基础里最容易囫囵吞枣的几个环节亲手打通。2. 为什么是 JDBC Swing先把技术选型和项目结构立住2.1 Swing 不是过时技术是课程设计和桌面端的最稳选择很多人一听到 Swing 就觉得是老古董实际在课程设计、毕业设计和部分企业内部小工具场景里Swing 依然是存活率最高的桌面方案。JavaFX 虽然界面更现代但它的包结构复杂、FXML 和 Controller 分离的写法对刚做完 Java 基础学习的人来说陡峭不少Electron 要装 Node.js 全家桶一个课设的重量级完全不对等。Swing 是 JDK 自带的javax.swing 包开箱即用不需要额外引入任何依赖这对「拿到题目就得在一周内交出可运行代码」的场景是决定性的优势。Swing 的学习曲线也平缓JFrame 是窗口JPanel 是容器JTable 显示数据JButton 绑定 ActionListener 处理点击事件。它的 MVC 设计虽然老派但非常清晰——JTable 的 TableModel 就是典型的 Model 层业务逻辑完全不需要堆在界面类里。你把这个项目做完再去看网上那些用 JavaFX 写的管理系统会发现界面渲染和事件响应的思路完全相通迁移成本很低。2.2 JDBC 是数据库访问的「原厂件」绕过它等于没学 Java 数据库编程JDBC 是 Java 规范里访问数据库的基础接口MyBatis、Hibernate、Spring Data JPA 这些框架底层全是 JDBC。你在课设里用纯 JDBC 写一遍连接、查询、更新、事务提交后面再用任何 ORM 框架时遇到诡异问题——比如连接池爆了、事务没提交、驱动版本不兼容——你才能根据 JDBC 的报错栈信息快速定位到是连接层还是 SQL 层出了问题。这个项目里 JDBC 承担三件事获取数据库连接、执行 SQL、处理结果集。连接用 DriverManager 从配置文件里读取 URL、用户名、密码来创建执行 SQL 分成 Statement 和 PreparedStatement 两种后者用占位符传参既能避免字符串拼接引发的 SQL 注入又能让数据库预编译 SQL 提升重复执行效率结果集 ResultSet 通过 next() 方法逐行遍历每行按列名或列索引取值转成商品对象放回 List。这套流程就是 MyBatis 框架里 SqlSession 执行 Mapper 语句的底层原形你在这里把它走通面试时被问到「MyBatis 一次查询做了什么」就能答出实质内容。2.3 三层结构把界面、业务、数据分开课设答辩才有东西讲商品管理系统这类题目最容易写成的样子是把所有代码塞进一个 JFrame 类里界面上直接写 SQL、直接操作 ResultSet、直接弹 JOptionPane 提示。这种写法能跑但答辩时老师问一句「如果要把 MySQL 换成 SQL Server你改哪里」就答不上来。更严重的是这种代码后期改一个字段要翻几百行调一次 bug 要动整个界面逻辑。我一般会把项目拆成三个包package对应三层职责。entity 包放商品实体类字段和数据库表的列一一对应dao 包放数据访问类每个方法对应一条 SQL 操作只负责「给一个商品对象插入数据库」或「从数据库查所有商品返回 List」ui 包放 Swing 界面类只负责接收用户输入、调用 dao 方法、把结果显示在表格里。业务规则比如「库存不能为负数」可以放在 dao 层做校验也可以单独拆 service 包课设规模下放在 dao 层够用不要过度设计。这个结构的好处是每一层都能独立测试先写 entity再用一个 main 方法单独测 dao 的增删改查确认数据没问题最后才碰界面。出 bug 时定位也快——界面没反应查事件监听是否绑定SQL 报错只查 dao 层数据对不上查实体类字段映射。答辩时你把三层包结构一摆老师自然知道你理解了分层思想而不是只会拖控件。3. 从建表到写通第一个 DAOJDBC 连接与商品增删改查的最小实现3.1 MySQL 建表字段别贪多够课设演示就行数据库设计这一步最容易翻车不是表建得不够全而是字段类型和长度选得不对。商品管理系统最核心的表就一张商品表 goods字段不要超过六个id、name、price、stock、description、create_time。id 用 INT 自增主键name 用 VARCHAR(50) 并加唯一索引防重复录入price 用 DECIMAL(10,2) 而不是 DOUBLE——浮点数计算价格在 Java 侧用 BigDecimal 接收才不会出现精度丢失stock 用 INTdescription 用 VARCHAR(200)create_time 用 DATETIME 默认值 CURRENT_TIMESTAMP。建表脚本可以直接写进项目根目录的 sql 文件夹里课设报告里也能作为附录展示。我习惯把建表语句写成可重复执行的格式先 DROP TABLE IF EXISTS 再 CREATE TABLE这样反复调试时不会因为表已存在而中断。数据库名用 goods_manager字符集必须指定 utf8mb4否则 Java 侧传入的中文商品名在 MySQL 8.x 默认字符集下可能变成乱码。DROP TABLE IF EXISTS goods; CREATE TABLE goods ( id INT NOT NULL AUTO_INCREMENT COMMENT 商品ID, name VARCHAR(50) NOT NULL COMMENT 商品名称, price DECIMAL(10,2) NOT NULL COMMENT 单价, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, description VARCHAR(200) DEFAULT NULL COMMENT 商品描述, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;这里把 name 设成唯一索引实际课设里可能遇到「同一商品名不同规格」的需求那你把 name 改成 VARCHAR(100) 并去掉唯一索引即可但演示阶段建议保留因为这是答辩时讲「数据完整性约束」的现成素材。create_time 用 DEFAULT CURRENT_TIMESTAMP 可以省掉 Java 侧手动塞时间的代码插入时少一个参数的坑。3.2 JDBC 连接参数url、user、password 别写死在代码里连接数据库最常见的错误写法是把 URL、用户名、密码直接写死在 DAO 类里。这么做代码确实能跑但换一台电脑运行、改数据库账号就得重新编译而且代码里裸奔数据库密码在课设查重和答辩演示时都很尴尬。正确做法是放在 src 目录下的 db.properties 配置文件里用 Properties 类读取。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/goods_manager?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password123456注意 url 里的三个参数useSSLfalse 是避免 MySQL 8.x 默认开启 SSL 时报证书警告serverTimezoneAsia/Shanghai 是解决 JDBC 驱动连接 MySQL 8.x 报时区错误The server time zone value Öйú±ê׼ʱ¼ä 之类的乱码错误的关键characterEncodingutf8 保证中文参数传输不出乱码。driver 写 com.mysql.cj.jdbc.Driver 是 MySQL 8.x 的驱动类如果你用的是 mysql-connector-java 5.1.x则要改成 com.mysql.jdbc.Driver这个差异是 JDBC 面试题里高频考点。写一个 DBUtil 工具类负责加载配置、注册驱动、获取连接。类里用静态代码块在类加载时一次性注册驱动用 getConnection() 方法返回新连接。课设项目不需要连接池用 DriverManager 每次获取连接即可但用完必须关这部分写在 DAO 层的 finally 块里。import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; static { try (InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); Class.forName(props.getProperty(jdbc.driver)); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); } catch (Exception e) { throw new ExceptionInInitializerError(数据库配置加载失败 e.getMessage()); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }getResourceAsStream 从 classpath 加载配置文件这样 DBUtil 在任意目录下运行都能找到 db.properties而不依赖当前工作目录。Class.forName 这行在 MySQL 8.x 驱动下其实可以省略因为 SPI 机制会自动注册驱动但保留它是兼容老版本驱动的稳妥写法。静态块里抛 ExceptionInInitializerError 是故意为之——如果配置加载失败类加载直接失败后续任何数据库操作都会立刻报错避免把配置错误延迟到运行时才暴露。3.3 商品 DAOPreparedStatement 的增删改查四条 SQL 写完核心DAO 层是这个项目的心脏。我习惯先定义一个包含全部数据访问方法的接口再用实现类去写 JDBC 代码虽然课设规模下接口加实现有点啰嗦但答辩时能多讲一层「面向接口编程」的设计思想比甩一个全是静态方法的类体面得多。import java.util.List; public interface GoodsDao { int insert(Goods goods); int update(Goods goods); int deleteById(Integer id); Goods findById(Integer id); ListGoods findAll(); ListGoods findByNameLike(String keyword); }实现类里最核心的是 prepareStatement 的写法。以插入为例SQL 用占位符参数从传入的 Goods 实体对象取值executeUpdate() 返回受影响的行数等于 1 表示插入成功。注意 PreparedStatement 的下标从 1 开始不是 0setBigDecimal 对应数据库的 DECIMAL 字段价格必须传 BigDecimal否则类型不匹配报错。import java.math.BigDecimal; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; public class GoodsDaoImpl implements GoodsDao { Override public int insert(Goods goods) { String sql INSERT INTO goods (name, price, stock, description) VALUES (?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, goods.getName()); ps.setBigDecimal(2, goods.getPrice()); ps.setInt(3, goods.getStock()); ps.setString(4, goods.getDescription()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(插入商品失败 e.getMessage(), e); } } }try-with-resources 写法是 JDK 7 引入的Connection 和 PreparedStatement 都会在 try 块结束时自动关闭比手动在 finally 里写 close() 少三层嵌套也避免忘记关闭连接导致数据库连接数耗尽。这是 java 基础面试题里「try-with-resources 和传统 try-finally 的区别」的原型代码。查询方法里需要把 ResultSet 转成 List 的关键操作我用 findAll 来说明。ResultSet 的 next() 方法每调用一次指针下移一行没有更多行时返回 false。取出字段时getInt(id) 和 getBigDecimal(price) 这类按列名取值的方式可读性好也避免因表结构调整后列索引变化导致的隐性 bug。Override public ListGoods findAll() { String sql SELECT id, name, price, stock, description, create_time FROM goods ORDER BY id; ListGoods list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Goods g new Goods(); g.setId(rs.getInt(id)); g.setName(rs.getString(name)); g.setPrice(rs.getBigDecimal(price)); g.setStock(rs.getInt(stock)); g.setDescription(rs.getString(description)); g.setCreateTime(rs.getTimestamp(create_time)); list.add(g); } } catch (SQLException e) { throw new RuntimeException(查询商品列表失败 e.getMessage(), e); } return list; }executeQuery() 和 executeUpdate() 的区别在这里体现出来查询用 executeQuery返回 ResultSet增删改用 executeUpdate返回 int。很多人初学 JDBC 时把两者混用查询用 executeUpdate 拿不到结果集插入用 executeQuery 返回不了行数都属于对 JDBC 接口规范不熟悉的表现。模糊查询的写法也是 JDBC 面试题常客。SQL 里写 name LIKE ?Java 侧调用 setString 时传入 % keyword %注意百分号里的空格不能多写成 % keyword 会导致匹配失败。Override public ListGoods findByNameLike(String keyword) { String sql SELECT id, name, price, stock, description, create_time FROM goods WHERE name LIKE ? ORDER BY id; ListGoods list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Goods g new Goods(); g.setId(rs.getInt(id)); g.setName(rs.getString(name)); g.setPrice(rs.getBigDecimal(price)); g.setStock(rs.getInt(stock)); g.setDescription(rs.getString(description)); g.setCreateTime(rs.getTimestamp(create_time)); list.add(g); } } } catch (SQLException e) { throw new RuntimeException(搜索商品失败 e.getMessage(), e); } return list; }3.4 数据访问层的验证用一个 main 方法先跑通再进入界面开发写 Swing 界面之前必须先验证 DAO 层没有问题。这个步骤省不得否则后续界面开发时数据库报错和界面代码混在一起排错难度直接翻倍。我在 dao 包里放一个 DaoTest 类里面写 main 方法依次调用 insert、findAll、update、delete、findByNameLike观察控制台输出。import java.math.BigDecimal; public class DaoTest { public static void main(String[] args) { GoodsDao dao new GoodsDaoImpl(); Goods g new Goods(); g.setName(农夫山泉 550ml); g.setPrice(new BigDecimal(2.00)); g.setStock(100); g.setDescription(饮用水); int rows dao.insert(g); System.out.println(插入结果 rows); ListGoods list dao.findAll(); for (Goods item : list) { System.out.println(item.getId() item.getName() item.getPrice() item.getStock()); } } }跑完这个 main 方法你会在控制台看到插入成功和查询回来的数据说明驱动版本、连接参数、SQL 语法、实体类映射都没问题。此时再进入 Swing 开发后续如果界面有 bug就能明确断定是界面层的问题不用再怀疑数据库连通性。这一步也是我多年写 Java 项目的习惯每一层开发完必须独立验证绝不带着未经验证的底层代码向上堆界面。4. Swing 图形化界面从空窗口到商品表格增删改查的完整实现4.1 主窗口布局JFrame JPanel把界面拆成功能区Swing 界面开发最忌讳的就是把整界面写成一个类然后塞几百行代码。正确的拆法是把主窗口拆成三个区域顶部是搜索和操作按钮区中间是商品表格展示区底部是状态栏或提示区。JFrame 默认布局是 BorderLayout正好对应这种上中下结构NORTH 放操作面板CENTER 放表格滚动面板SOUTH 放状态标签。主窗口类 MainFrame 继承 JFrame构造函数里设置窗口标题、大小、关闭行为然后调 initUI() 初始化各区域组件。商品管理系统的窗口标题就叫「商品管理系统」尺寸设 900x600居中显示。关闭行为用 EXIT_ON_CLOSE 保证点关闭按钮时整个程序退出。import javax.swing.*; import java.awt.*; public class MainFrame extends JFrame { private GoodsTableModel tableModel; private JTable table; private JTextField searchField; public MainFrame() { setTitle(商品管理系统); setSize(900, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); initUI(); } private void initUI() { JPanel topPanel new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); searchField new JTextField(15); JButton searchBtn new JButton(搜索); JButton addBtn new JButton(新增); JButton updateBtn new JButton(修改); JButton deleteBtn new JButton(删除); JButton refreshBtn new JButton(刷新); topPanel.add(new JLabel(商品名称)); topPanel.add(searchField); topPanel.add(searchBtn); topPanel.add(addBtn); topPanel.add(updateBtn); topPanel.add(deleteBtn); topPanel.add(refreshBtn); tableModel new GoodsTableModel(dao.findAll()); table new JTable(tableModel); JScrollPane scrollPane new JScrollPane(table); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setRowHeight(28); add(topPanel, BorderLayout.NORTH); add(scrollPane, BorderLayout.CENTER); } }FlowLayout 让按钮从左到右排列遇到窗口拉伸时不会挤压变形。表格放在 JScrollPane 里否则表头不可见且数据量大时滚动不了。SINGLE_SELECTION 限制只能选中一行修改和删除操作都基于当前选中行避免多选时逻辑复杂。这里有个 Swing 关键点add(topPanel, BorderLayout.NORTH) 的 add 方法在 JFrame 层面等同于 setContentPane 后再加组件JDK 5 之后直接往 JFrame 上 add 组件是合法写法不需要手动调 getContentPane()。4.2 表格模型用 DefaultTableModel 还是自定义 TableModel商品表格是界面的核心数据显示和编辑是否顺手取决于 TableModel 怎么写。最省事的方式是用 JTable 自带的 DefaultTableModeladdRow 方法往里塞 Object 数组即可。但 DefaultTableModel 的列类型默认都是 Object价格列无法右对齐库存列无法排序而且表格里的值要转成字符串再存取出来还要自己转型代码写得很脏。我一般会继承 AbstractTableModel 写一个 GoodsTableModel持有 List 数据源getValueAt 方法里根据列索引从 Goods 对象取对应字段。这样表格和实体类之间直接映射列数据的类型也是真实的后续如果要给价格列加右对齐渲染器直接在 TableModel 的 getColumnClass 里返回 BigDecimal.class 即可。import javax.swing.table.AbstractTableModel; import java.math.BigDecimal; import java.util.List; public class GoodsTableModel extends AbstractTableModel { private final String[] columns {ID, 商品名称, 单价, 库存, 描述, 创建时间}; private ListGoods data; public GoodsTableModel(ListGoods data) { this.data data; } public void setData(ListGoods data) { this.data data; fireTableDataChanged(); } public Goods getRow(int rowIndex) { return data.get(rowIndex); } Override public int getRowCount() { return data null ? 0 : data.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 data.get(rowIndex); switch (columnIndex) { case 0: return g.getId(); case 1: return g.getName(); case 2: return g.getPrice(); case 3: return g.getStock(); case 4: return g.getDescription(); case 5: return g.getCreateTime(); default: return ; } } }fireTableDataChanged() 是表格模型的关键方法。每次数据更新后必须调用JTable 才会重绘刷新界面。这个方法是 AbstractTableModel 内部的观察者机制它通知所有注册的 TableModelListener 数据已变JTable 收到通知后重新调用 getValueAt 获取数据。如果你改了 data 引用却不调用 fireTableDataChanged表格界面不会报错但显示的还是旧数据这是 Swing 开发里最容易踩的隐藏坑。4.3 事件监听新增、修改、删除的按钮逻辑与数据刷新按钮的 ActionListener 是 Swing 事件驱动的核心。每个按钮注册一个 ActionListeneractionPerformed 方法里写业务逻辑。新增按钮的逻辑是弹出一个对话框收集商品信息用户点确定后把数据封装成 Goods 对象调用 dao.insert()成功后刷新表格。searchBtn.addActionListener(e - { String keyword searchField.getText().trim(); ListGoods list dao.findByNameLike(keyword); tableModel.setData(list); }); refreshBtn.addActionListener(e - { ListGoods list dao.findAll(); tableModel.setData(list); });搜索和刷新是最简单的两个操作。搜索框拿到的文本先去空格空字符串时调用 findByNameLike 会返回 LIKE %% 的结果等价于全表查询。刷新按钮主动调 findAll 并把新列表塞进 TableModel实现数据同步。这里我把 dao 声明为 MainFrame 的成员变量在构造函数里初始化 GoodsDaoImpl而不是每个方法里 new 一个——道理很简单一个 DAO 实例贯穿整个程序生命周期避免无谓的对象创建开销也让代码结构更清晰。新增和删除按钮的完整逻辑我写在一起说明。新增需要弹出输入对话框Swing 提供 JOptionPane 的 showInputDialog 可以逐字段收集但商品有四个字段逐个弹窗体验很差。常见做法是用 JDialog 自建一个商品表单弹窗放四个 JLabel 和四个 JTextField点确定后获取数据。这里给出表单弹窗的简化写法使用 JOptionPane 的自定义面板参数。addBtn.addActionListener(e - { JTextField nameField new JTextField(); JTextField priceField new JTextField(); JTextField stockField new JTextField(); JTextField descField new JTextField(); JPanel formPanel new JPanel(new GridLayout(4, 2, 5, 5)); formPanel.add(new JLabel(商品名称)); formPanel.add(nameField); formPanel.add(new JLabel(单价)); formPanel.add(priceField); formPanel.add(new JLabel(库存)); formPanel.add(stockField); formPanel.add(new JLabel(描述)); formPanel.add(descField); int result JOptionPane.showConfirmDialog(this, formPanel, 新增商品, JOptionPane.OK_CANCEL_OPTION); if (result JOptionPane.OK_OPTION) { try { Goods g new Goods(); g.setName(nameField.getText().trim()); g.setPrice(new BigDecimal(priceField.getText().trim())); g.setStock(Integer.parseInt(stockField.getText().trim())); g.setDescription(descField.getText().trim()); dao.insert(g); tableModel.setData(dao.findAll()); JOptionPane.showMessageDialog(this, 新增成功, 提示, JOptionPane.INFORMATION_MESSAGE); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(this, 单价和库存必须是数字, 输入错误, JOptionPane.ERROR_MESSAGE); } catch (RuntimeException ex) { JOptionPane.showMessageDialog(this, 新增失败 ex.getMessage(), 错误, JOptionPane.ERROR_MESSAGE); } } });GridLayout(4, 2, 5, 5) 生成 4 行 2 列的网格标签和输入框一一对齐间距 5 像素。showConfirmDialog 返回 OK_CANCEL_OPTION 时表示用户点了确定此时从各 JTextField 取文本并解析。注意 NumberFormatException 和 RuntimeException 分开捕获前者是用户输入格式不对后者是数据库操作失败提示信息区分开便于定位。新增成功后重新调 dao.findAll() 刷新表格而不是手动往 tableModel 的 data 里追加一行——虽然效率低一点但保证表格数据和数据库完全一致在课设场景下正确性优先于性能。删除按钮的逻辑是取表格当前选中行从 TableModel 拿到对应 Goods 对象调 dao.deleteById(id) 后刷新。删除前必须弹确认框这是所有管理系统的基本操作规范防止误删。deleteBtn.addActionListener(e - { int selectedRow table.getSelectedRow(); if (selectedRow 0) { JOptionPane.showMessageDialog(this, 请先选择要删除的商品, 提示, JOptionPane.WARNING_MESSAGE); return; } Goods g tableModel.getRow(selectedRow); int confirm JOptionPane.showConfirmDialog(this, 确定删除商品【 g.getName() 】吗, 删除确认, JOptionPane.YES_NO_OPTION); if (confirm JOptionPane.YES_OPTION) { dao.deleteById(g.getId()); tableModel.setData(dao.findAll()); } });getSelectedRow 返回选中行的索引没有选中时返回 -1所以必须判断。这里用 tableModel.getRow(selectedRow) 取数据而不是直接从 JTable 拿值原因在于 TableModel 里存的是 Goods 对象取出来直接能用 g.getId() 和 g.getName()不需要再从表格单元格的字符串转换回去。删除确认框用 YES_NO_OPTION和新增表单的 OK_CANCEL_OPTION 区分开各自语义清晰。修改按钮的复用性最强。选中一行后弹出表单表单各输入框先填上当前行的值用户改动后点确定调 dao.update()。Swing 里实现方式是把新增的表单弹窗逻辑抽取成一个方法传入一个 Goods 对象用于回显没传入则是新增模式传入了则做更新模式。updateBtn.addActionListener(e - { int selectedRow table.getSelectedRow(); if (selectedRow 0) { JOptionPane.showMessageDialog(this, 请先选择要修改的商品, 提示, JOptionPane.WARNING_MESSAGE); return; } Goods g tableModel.getRow(selectedRow); JTextField nameField new JTextField(g.getName()); JTextField priceField new JTextField(g.getPrice().toString()); JTextField stockField new JTextField(String.valueOf(g.getStock())); JTextField descField new JTextField(g.getDescription()); JPanel formPanel new JPanel(new GridLayout(4, 2, 5, 5)); formPanel.add(new JLabel(商品名称)); formPanel.add(nameField); formPanel.add(new JLabel(单价)); formPanel.add(priceField); formPanel.add(new JLabel(库存)); formPanel.add(stockField); formPanel.add(new JLabel(描述)); formPanel.add(descField); int result JOptionPane.showConfirmDialog(this, formPanel, 修改商品, JOptionPane.OK_CANCEL_OPTION); if (result JOptionPane.OK_OPTION) { g.setName(nameField.getText().trim()); g.setPrice(new BigDecimal(priceField.getText().trim())); g.setStock(Integer.parseInt(stockField.getText().trim())); g.setDescription(descField.getText().trim()); dao.update(g); tableModel.setData(dao.findAll()); } });4.4 启动入口一个 main 方法启动整个程序项目里放一个 Main 类作为启动入口只做一件事在 Swing 事件调度线程上创建主窗口。Swing 组件不是线程安全的必须在事件调度线程EDT上创建和操作这是 Java Swing 面试题里几乎必问的原则。初学者在 main 里直接 new MainFrame() 也能跑但在某些 Linux 环境下会出现界面渲染异常正确姿势是调用 SwingUtilities.invokeLater。import javax.swing.SwingUtilities; public class Main { public static void main(String[] args) { SwingUtilities.invokeLater(() - { MainFrame frame new MainFrame(); frame.setVisible(true); }); } }invokeLater 把创建窗口的任务压入事件调度线程的任务队列保证窗口初始化在 EDT 上串行执行。这行代码的含义值得在答辩时说清楚Swing 采用的是单线程模型所有组件创建、重绘、事件处理都在 EDT 上完成开发者自己开的子线程如果要更新界面同样必须通过 invokeLater 切回 EDT否则会出现界面卡顿、闪烁甚至崩溃。5. 避坑实录JDBC 与 Swing 联调时常见的六个翻车现场5.1 MySQL 8.x 时区报错的玄学the server time zone value 乱码现象运行 DAO 测试时控制台报错错误信息里出现 The server time zone value Öйú±ê׼ʱ¼ä is unrecognized or represents more than one time zone数据库完全连不上。原因MySQL 8.x 的时区默认是系统时区JDBC 驱动连接时拿到的是中文时区名的乱码表示驱动解析失败。这不是连接配置写错了是 JDBC 驱动要求显式指定 serverTimezone 参数。解决在 jdbc.url 末尾追加 serverTimezoneAsia/Shanghai并确保 MySQL 服务端时区与之匹配。如果还报错在 MySQL 命令行执行 SET GLOBAL time_zone 08:00 后再试。这个坑在课程设计高峰期特别常见因为大部分人的 MySQL 装完没改过时区配置。5.2 ClassNotFoundException: com.mysql.cj.jdbc.Driver 不发愁先看 jar 包位置现象启动程序直接报 ClassNotFoundException驱动类找不到。原因mysql-connector-java 的 jar 包没有正确引入到 classpath。很多人以为把 jar 文件拷贝进项目根目录就算引入了实际上 IDEA 里必须右键 jar 包选 Add as Library或者通过 Maven 添加依赖。另一种原因是 MySQL 驱动版本太老5.1.x 的驱动类名是 com.mysql.jdbc.Driver你却在配置里写成 com.mysql.cj.jdbc.Driver。解决确认 jar 包已经出现在 External Libraries 里检查驱动类名和 jar 包版本对应关系。我一般把项目改成 Maven 结构在 pom.xml 里引 mysql-connector-java让 idea 自动下载依赖比手动拷贝 jar 包少踩很多路径问题。IDEA 下载 Maven 依赖失败时可以检查 network 配置或改为本机手工装包到本地仓库这又是另一类常见问题了。5.3 中文乱码的根源URL 参数 表字符集 控制台编码三层叠加现象插入中文商品名后查出来是「???」或者控制台输出乱码但数据库中存的汉字正常。原因乱码问题有三层来源。第一层是连接 URL 没加 characterEncodingutf8Java 侧传的中文按默认编码转成字节流MySQL 按 latin1 存储导致乱码第二层是表创建时没指定 utf8mb4用了数据库默认字符集第三层是 IDEA 控制台输出编码不是 UTF-8数据显示正常但打印出来乱码这种是假乱码。解决URL 加 characterEncodingutf8创建表时明确 CHARSETutf8mb4IDEA 里打开 Help - Edit Custom VM Options 添加 -Dfile.encodingUTF-8 并重启。排查时先查数据库里存的到底对不对用 Navicat 看表数据判断是存储层乱码还是展示层乱码别一上来就改代码。5.4 Swing 界面刷新不及时改完数据表格纹丝不动现象新增商品后数据库里有新数据但 JTable 界面没显示必须点一下刷新按钮才能看到。原因JTable 的显示数据来自 TableModel你调 dao.insert() 只改了数据库TableModel 持有的 List 还是旧数据而且没有通知 JTable 数据已变。这不算 JDBC 问题是 Swing 的 Model-View 机制没搞明白。解决新增、修改、删除操作后必须重新调 findAll() 获取最新数据然后调 tableModel.setData() 方法。setData 里调用了 fireTableDataChanged()JTable 收到事件后重新渲染。这里要特别注意顺序先更新数据库再刷新表格两个步骤缺一不可。有些初学者先刷新表格再操作数据库界面永远显示旧数据就是这个顺序反了。5.5 商品重名插入报错唯一索引的 Duplicate entry 异常没有兜底现象插入一个已存在的商品名程序抛异常界面卡住或者直接退出。原因建表时 name 字段有唯一索引 uk_name数据库层面拒绝了重复数据但 Java 代码没有捕获这个 SQLException异常一路抛到事件监听线程Swing 的单线程模型下 EDT 异常会导致界面无响应甚至程序崩溃。解决在 DAO 层的 insert 方法里捕获 SQLException判断错误码或错误信息是否包含 Duplicate转为更友好的提示。或者在 UI 层捕获 RuntimeException 后弹窗提示「商品名称已存在」。我更推荐在 UI 层统一处理因为 DAO 层保持抛出异常的语义UI 层根据场景决定怎么展示。5.6 JDBC 连接不关闭导致数据库连接数耗尽现象程序运行一段时间后任何数据库操作都卡住控制台报 Connection is not available, request timed out 之类的错误重启程序又恢复正常。原因因为用了 try-with-resources 而不再手动关连接时错误地写成了只在成功路径上关闭或者某些代码路径下 ps.executeUpdate() 抛异常导致连接未释放。重复执行多次后 MySQL 的连接数被占满后续所有请求等待连接超时。解决确保所有 JDBC 操作都使用 try-with-resources 关闭 Connection、PreparedStatement、ResultSet。如果项目里有手动获取连接的旧代码检查 finally 块中是否把 conn.close() 放在了正确位置。最好的验证方式是连续新增删除一百条数据然后运行 SHOW STATUS LIKE Threads_connected 看连接数是否持续增长。6. 进阶一步连接池替换、事务处理和入库参数防注入验证项目跑通后再花半天时间做三件进阶改造课设答辩的价值能提高一档。第一件事是引入数据库连接池。DriverManager 每次 getConnection 都走 TCP 握手和数据库认证高并发场景下开销不小。可以换成 HikariCP在 pom.xml 引入依赖并写一个连接池工具类用它替换 DBUtil 里的 DriverManager。核心改动是把配置文件里的密码字段从明文改为可加密存储用 jasypt 或者自写一个 base64 解码工具类处理。这一改动和热词搜索里「mysql的数据库连接池」「数据库同步工具」这类运维话题也能串起来讲说明理解了连接池对数据库稳定性的价值。第二件事是验证 PreparedStatement 的防注入能力。在搜索框输入 1 OR 11 这类注入串JDBC 会把它当作文本直接传给 LIKE 匹配不会拼进 SQL 语法里。你可以在 DAO 里加一个打印实际 SQL 的日志开关用 log4j 或直接 System.out 输出预编译参数让答辩老师看到参数是安全绑定的。这一条直接对应 jdbc增删改查 和 java面试题 里的高频考点——SQL 注入的防护原理不能只背概念得有亲手验证过的例子。第三件事是把新增和修改操作放进事务里。MySQL 默认 autocommit每个 SQL 立即提交。如果一次业务操作要同时更新商品表和库存流水表中途第二条 SQL 失败会出现数据不一致。我在 dao 层加一个批量入库方法用 conn.setAutoCommit(false) 手动控制两条 SQL 都成功才 commit任何一条失败则 rollback。这个写法和热词里「java怎么保证数据一致性」的面试考点完全对应把事务的 ACID 属性从理论变成代码。public void batchInsertWithTransaction(ListGoods goodsList) { String sql INSERT INTO goods (name, price, stock, description) VALUES (?, ?, ?, ?); try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (Goods g : goodsList) { ps.setString(1, g.getName()); ps.setBigDecimal(2, g.getPrice()); ps.setInt(3, g.getStock()); ps.setString(4, g.getDescription()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw new RuntimeException(批量插入失败已回滚 e.getMessage(), e); } finally { conn.setAutoCommit(true); } } catch (SQLException e) { throw new RuntimeException(事务操作失败 e.getMessage(), e); } }addBatch 和 executeBatch 是 JDBC 的批量操作接口一次网络往返提交多条 SQL性能和一致性都比循环单条插入好。commit 和 rollback 的调用位置必须精确commit 在批量执行成功后rollback 只在捕获 SQLException 时触发。finally 里把 autoCommit 恢复 true 是防止连接归还连接池时残留事务状态虽然这里用的是 DriverManager 直连但写成规范对后面接连接池有好处。做完这三件事你的项目就从「能跑的课设」升级成「能讲出设计深度的课设」连接池讲性能优化事务讲数据一致性PreparedStatement 讲安全编码。答辩时老师问「你这个项目哪里有亮点」你至少有三个角度可以展开。这比临时抱佛脚背 java面试八股文要实在得多因为每句话背后都是你亲手跑过的代码——我至今写的每个项目都会保留这套自查流程先单层验证、再联调、再上事务和连接池顺序反了就会陷入界面和数据库互相甩锅的泥潭。希望帮到你。本文还有配套的精品资源点击获取
返回列表