Java Swing图书管理系统:MVC分层、事务与连接池完整实践 简介这是一套基于Java GUI实现的图书管理系统完整项目资料适用于高校软件工程、计算机相关专业进行课程设计、毕业设计或Java桌面应用开发练习。系统覆盖系统管理、进书登记、图书入库编目、多条件查询以及借书/还书管理等核心业务模块并区分普通读者与管理员权限能够帮助学习者理解图书管理业务流程与Java Swing界面编程。压缩包共263个文件含93个Java源程序、109个class编译文件以及26个jar依赖库并附带SQL数据库脚本、doc课设报告和界面截图等整体大小约16.87MB代码结构与打包文件层次清楚。已有5589人学习下载。通过源码可快速掌握角色权限控制、图书信息增删改查、借阅超期判断等关键实现思路同时数据库文件和报告文档为二次开发与文档撰写提供了直接参考。1. 图书管理系统(Java GUI)为什么这个“老项目”仍然值得做如果你在搜索引擎里敲下这个标题多半是带着两个诉求之一来的课程设计需要交一个能演示的完整项目或者小团队/社团想内部搭一个“能录入书目、能借还登记”的桌面工具。坦白说Java GUI 技术在 Web 时代已经不算主流但“图书管理系统”这个场景恰恰是桌面程序最适合的舞台——数据量小、操作人员固定、交互集中在几张表单和表格上用 Swing 这类自带控件去实现比网页方案更直接也更贴近“从零到一”的学习路径。我见过很多人在这个项目上翻车翻车的点不在 Java 语法而在三个地方界面代码和业务逻辑揉成一团导致后期改不动、数据库连接随用随开导致窗口卡到像死机、打包发布时别人机器上跑不起来。这篇文章不是带你抄一个不可维护的“大作业”而是按一个能真正交付给使用者的标准去拆解它——先讲选型和架构上的理由再给每个核心功能的可运行代码与参数调整方法最后把这些年踩过的坑按“现象→原因→解决”摊开。读完你拿到的不是一个交差用的 Demo而是一套知道边界在哪、坏了自己能修的方案。2. 技术选型与架构设计先想清楚 GUI 库和分层再动手2.1 Swing 还是 JavaFX这个场景下我为什么选 Swing到了 2024 年再谈 Java GUI绕不开 Swing 和 JavaFX 的选择。JavaFX 有更现代的样式表CSS 控制外观、内置动画和更流畅的渲染管线但它的学习曲线更陡需要额外配置 JavaFX 运行时而 Swing 自 JDK 1.2 起就内置在标准库中任何一台装了 JDK 的机器都能直接跑不需要额外引入模块。对于“图书管理系统”这种以表格、表单、按钮为核心交互的管理类应用Swing 的 JTable、JTextField、JComboBox 已经覆盖了绝大部分需求而且资料数量比 JavaFX 多一个量级——遇到问题搜出来的答案基本都能直接解决。我的建议很直接如果这是课程设计或内部工具选 Swing如果目标是把界面做成现代化外观、打算长期迭代成一个商业味更浓的产品选 JavaFX。别在选型上花太多时间纠结这个项目的工作量大头在业务逻辑和数据一致性上不在界面渲染。另外说一个常见误区有人为了“显得高级”去用第三方美化包或者自己写绘制代码结果在跨平台换字体、高分屏缩放上折腾到崩溃。图书管理系统追求的是“稳定、清晰、够用”默认的 Metal 观感或者简单调整过的 Windows 观感完全足够。2.2 MVC 分层让界面改动不再牵一发动全身所谓“图书管理系统”本质是三个东西操作界面、业务规则、数据存储。如果不用任何架构思想最常见的写法是在按钮的 ActionListener 里直接写 JDBC 代码先拼 SQL、再执行、再刷表格。这个小系统在一千行代码内还能跑但一旦加入“借阅超期计算”“图书状态联动”这类规则界面事件和业务逻辑互相纠缠改一处坏三处最后只能推倒重来。我一般会按 MVC 的基本思路把项目拆成三层但不需要引入 Spring 之类的容器纯手工分层就够清晰Model 层数据实体Book、User、BorrowRecord对应数据库表结构。View 层所有 Swing 界面类只负责显示和收集输入不写任何 SQL。Controller 层处理界面事件调用 Service 对象Service 里放业务规则再调用 DAOData Access Object做数据库操作。一个可参考的包结构是这样的com.library ├── model // Book.java, User.java, BorrowRecord.java ├── view // LoginFrame.java, MainFrame.java, BookManagePanel.java ├── controller // LoginController.java, BookController.java ├── service // BookService.java, BorrowService.java ├── dao // BookDao.java, BorrowDao.java └── util // DBUtil.java, DateUtil.java2.3 界面结构主窗口用什么布局承载多个功能页图书管理系统的主界面一般有两种做法单窗口用 JTabbedPane 切换功能页或者用 JFrame 多个 JDialog。我推荐前者理由很简单——工作人员在登记借阅时经常要回头查书目Tab 页切换比打开新窗口更顺手也天然限制了“窗口越开越多却找不到在哪”的混乱。主窗口通常是一个 JFrame顶部放菜单栏或工具栏中间是 JTabbedPane包含“图书管理”“读者管理”“借阅归还”“借阅记录查询”四个页签。布局上有个细节值得注意Swing 的布局管理器初学者很容易用错。JFrame 默认是 BorderLayout如果直接往它上面加多个组件后加的会覆盖先加的。常用的做法是给 JFrame 设一个 JPanel这个 Panel 用 BorderLayout 分成 NORTH工具按钮区和 CENTER表格区子功能页里再用 JPanel 配合 GridBagLayout 或 MigLayout 来做表单排列。如果你是新手先避开手工 setBounds 布局改用布局管理器不然窗口一拉大界面就乱成一团。3. 从登录到借还核心功能的最小可运行实现3.1 登录验证用表和界面对齐的代码来理解 MVC登录窗口是系统的入口逻辑简单但很适合用来展示分层到底怎么分。View 层只收集用户名和密码点击登录按钮后把数据交给 ControllerController 调 ServiceService 调 DAO 查库最终返回一个布尔值和用户角色。先看 View 层的关键代码// 位于 view/LoginFrame.java public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; private JButton loginButton; private LoginController controller; public LoginFrame() { controller new LoginController(); initUI(); } private void initUI() { setTitle(图书管理系统 - 登录); setSize(360, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JPanel panel new JPanel(new GridBagLayout()); // 此处省略 GridBagConstraints 详细配置 panel.add(new JLabel(用户名), constrains(0, 0)); panel.add(usernameField new JTextField(15), constrains(1, 0)); panel.add(new JLabel(密码), constrains(0, 1)); panel.add(passwordField new JPasswordField(15), constrains(1, 1)); panel.add(loginButton new JButton(登录), constrains(1, 2)); loginButton.addActionListener(e - doLogin()); add(panel); } private void doLogin() { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, 用户名和密码不能为空); return; } boolean success controller.login(username, password); if (success) { // 登录成功关闭登录窗口打开主窗口 dispose(); new MainFrame().setVisible(true); } else { JOptionPane.showMessageDialog(this, 用户名或密码错误); } } }这里有个易错的点JPasswordField.getPassword()返回的是char[]而不是String。官方建议用字符数组处理密码用完立即覆盖避免密码以字符串形式驻留在内存中。但我见过的绝大多数项目——包括我自己早期写的——都是直接new String(...)转出来用图省事。考虑到这是内部管理系统威胁模型有限这种做法可以接受但要意识到它不是最佳实践。再来看 Controller 和 Service 的响应逻辑// 位于 controller/LoginController.java public class LoginController { private UserService userService; public LoginController() { userService new UserService(); } public boolean login(String username, String password) { // 调用 Service 层不直接接触数据库 return userService.checkLogin(username, password); } }UserService.checkLogin内部会调UserDao.findByUsername拿到用户对象后比对密码。这里有一个微妙的取舍比对密码这件事应该放在 Service 层而不是 SQL 层。有人喜欢写SELECT * FROM user WHERE username ? AND password ?看起来简洁但导致两个问题一是无法针对“用户不存在”和“密码错误”给出不同提示二是如果以后改密码存储方式比如加盐哈希SQL 和业务代码要一起改。正确的做法是 SQL 只按用户名查密码比对用 Java 代码完成。3.2 图书录入与列表用 JTable 把数据库记录搬到界面图书管理是整个系统的核心涉及“增删改查”四类操作。列表展示是最常见的需求做法是查询数据库得到一个ListBook转换成DefaultTableModel后塞给 JTable。但很多人会在这里犯一个性能错误直接在查询结果的循环里调用model.addRow数据量大了之后界面明显变慢。看一个规范的处理方式// 位于 view/BookManagePanel.java public class BookManagePanel extends JPanel { private JTable table; private DefaultTableModel tableModel; private BookController controller; public BookManagePanel() { controller new BookController(); String[] columns {编号, 书名, 作者, 出版社, 馆藏总量, 可借数量}; tableModel new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { return false; // 表格数据只读禁止直接双击修改 } }; table new JTable(tableModel); // JTable 放入 JScrollPane否则列头不会显示 add(new JScrollPane(table), BorderLayout.CENTER); loadBookData(); } public void loadBookData() { ListBook bookList controller.getAllBooks(); // 批量刷新先清空再一次性写入避免逐行触发重绘 tableModel.setRowCount(0); for (Book book : bookList) { tableModel.addRow(new Object[]{ book.getId(), book.getTitle(), book.getAuthor(), book.getPublisher(), book.getTotalCopies(), book.getAvailableCopies() }); } } }关键参数解释DefaultTableModel(columns, 0)第二个参数 0 表示初始行数为空重写isCellEditable返回 false 是为了防止用户在界面上直接改表格内容却不知道该数据要去数据库同步——这种“能改但存不进去”的状态最容易让人误以为程序坏了。JTable放入JScrollPane几乎是强制性的否则表头不显示且数据超出可视区域时无法滚动。3.3 借书与还书事务边界决定数据一致性借书和还书是这个系统最需要“动脑子”的地方因为每次操作涉及两处数据变更借阅记录表和图书可借数量。如果只更新用户要执行的借阅记录忘记扣减库存就会出现“书借出去了但馆藏数没减”的错账。借书的核心逻辑是这样一组的三个动作// 位于 service/BorrowService.java public boolean borrowBook(int userId, int bookId) { Connection conn null; try { conn DBUtil.getConnection(); // 开启手动事务三行操作要么全成功要么全回滚 conn.setAutoCommit(false); // 1. 检查库存是否够 int available getAvailableCopies(conn, bookId); if (available 0) { conn.rollback(); return false; } // 2. 插入借阅记录 insertBorrowRecord(conn, userId, bookId); // 3. 扣减可借数量 decreaseAvailable(conn, bookId); conn.commit(); return true; } catch (SQLException e) { // 异常时回滚避免脏数据入库 rollbackQuietly(conn); logger.error(借书失败, e); return false; } finally { DBUtil.closeConnection(conn); } }这里的事务边界最容易被忽略。很多初版项目会在三个步骤之间分别获取和释放连接结果第一步和第二步之间程序崩溃借阅记录和库存数据就出现永久性不一致。用同一个 Connection 加手动事务是这个功能唯一的可靠写法。注意conn.rollback()和conn.close()都要包在 try-catch 里因为这两个方法本身也会抛 SQLException。还书逻辑同理只不过方向相反删除或标记借阅记录为已还、同时把可借数量加回去。3.4 查询与条件筛选拼接 SQL 时守住底线图书列表通常需要按书名、作者、分类做模糊查询。实现方式是在界面放几个文本框和查询按钮点击后执行带 WHERE 条件的 SQL。大部分资料会教你用字符串拼接来构造动态条件String sql SELECT * FROM book WHERE 11; if (!keyword.isEmpty()) { sql AND title LIKE % keyword %; }这种写法在自己机器上演示一般没问题但它有两个致命缺陷第一keyword里如果包含单引号SQL 语句会直接语法错误如果把单引号换成精心构造的字符串就成了注入攻击——虽然图书管理系统多数是本地使用但“本地工具”同样可能被同事或同学通过界面输入攻击。第二每次拼接都产生新的 SQL 字符串数据库无法复用执行计划性能上也有损耗。正确写法是用PreparedStatement的占位符// 位于 dao/BookDao.java public ListBook searchBooks(String titleKeyword, String authorKeyword) { // 用 StringBuilder 动态拼 SQL但值的部分一律用 ? 占位 StringBuilder sql new StringBuilder(SELECT * FROM book WHERE 11); ListObject params new ArrayList(); if (titleKeyword ! null !titleKeyword.isEmpty()) { sql.append( AND title LIKE ?); params.add(% titleKeyword %); } if (authorKeyword ! null !authorKeyword.isEmpty()) { sql.append( AND author LIKE ?); params.add(% authorKeyword %); } return executeQuery(sql.toString(), params); }这样既保留了动态筛选的灵活性又避免了注入风险。很多人觉得“只要我不存重要数据SQL 注入无所谓”但这个习惯一旦养成写到别的高价值系统里就是事故。在图书管理系统上用正确写法练成肌肉记忆是对自己负责。4. 数据库设计与连接从建表到连接池的几个关键决策4.1 三张表的结构设计字段类型和约束怎么设图书管理系统至少需要三张表用户表、图书表、借阅记录表。表结构设计的好坏直接影响后续功能的复杂程度我见过不少项目把“借阅日期”“归还日期”直接设计成 String 类型然后在 Java 代码里反复解析格式化给自己挖坑。下面是经过验证的建表 SQL可以直接复用CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT USER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), isbn VARCHAR(20), total_copies INT NOT NULL DEFAULT 1, available_copies INT NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE NULL, status VARCHAR(20) NOT NULL DEFAULT BORROWED, CONSTRAINT fk_borrow_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;有几个需要说明的细节。第一所有文本字段统一使用utf8mb4字符集这不是“加个 charset 保险”而是因为 MySQL 的utf8实际只支持基本多语言平面遇到生僻字或某些特殊符号会直接报错或变成问号。第二available_copies和total_copies都放在 book 表里而不是每次临时去统计存量减借出量。这种冗余字段需要靠事务保证一致参见 3.3但查询时一次就能拿到不需要复杂 JOIN。第三borrow_record保留已还记录用status字段区分 BORROWED / RETURNED而不是把还掉的记录直接删除——这样才能统计借阅历史。第四外键约束必须建。有人觉得“业务代码已经控制一致性外键多余”但在删除一本书时如果忘记检查是否仍有未还记录外键能兜底拦截防止产生孤儿数据。4.2 JDBC 连接参数那些让程序“莫名其妙慢”的配置数据库连接是 Java GUI 程序最容易出现性能假象的地方。最基础的写法是每次操作前DriverManager.getConnection用完close。这在数据量小的时候毫无感觉但一旦连接池没配置好或者 MySQL 服务端的wait_timeout参数把空闲连接断掉程序就会在某个操作时报“Connection has been closed”的错误。我的建议是至少用一个轻量级的连接池。HikariCP 是常见选择配置参数直接写进资源文件# jdbc.properties jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyour_password # 连接池参数 jdbc.pool.maximumPoolSize10 jdbc.pool.minimumIdle2 jdbc.pool.connectionTimeout30000这里重点说三个参数maximumPoolSize不要设大一个桌面客户端 GUI 程序的并发请求量极低10 个连接绰绰有余connectionTimeout指获取连接的等待时间设 30 秒可以避免数据库假死时界面无限等待minimumIdle是保持的空闲连接数设 2 个能保证快速响应而不会白白占用资源。URL 里必须带serverTimezone否则新版 JDBC 驱动连 MySQL 8 会直接报时区错误。封装连接的工具类是一个单例// 位于 util/DBUtil.java public class DBUtil { private static HikariDataSource dataSource; static { try { Properties props new Properties(); props.load(DBUtil.class.getClassLoader().getResourceAsStream(jdbc.properties)); HikariConfig config new HikariConfig(); config.setDriverClassName(props.getProperty(jdbc.driver)); config.setJdbcUrl(props.getProperty(jdbc.url)); config.setUsername(props.getProperty(jdbc.username)); config.setPassword(props.getProperty(jdbc.password)); config.setMaximumPoolSize(Integer.parseInt(props.getProperty(jdbc.pool.maximumPoolSize))); config.setMinimumIdle(Integer.parseInt(props.getProperty(jdbc.pool.minimumIdle))); config.setConnectionTimeout(Long.parseLong(props.getProperty(jdbc.pool.connectionTimeout))); dataSource new HikariDataSource(config); } catch (IOException e) { throw new ExceptionInInitializerError(加载数据库配置失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }获取连接这里有一个习惯值得坚持永远不要在 DAO 内部自己 new 一个Connection实例而是通过DBUtil.getConnection()拿连接池管理的对象。这样当你需要排查问题时只需在 DBUtil 里打日志或加监控而不是去翻几十个 DAO 方法。4.3 时间与日期处理为什么建议直接用 java.time 而不是 java.util.Date图书管理系统里“截止日期”计算是常见逻辑——比如借书默认 30 天内归还。旧代码习惯用java.util.Date配合Calendar做加减踩过坑的人都知道Calendar.add(Calendar.DAY_OF_MONTH, 30)在跨月、跨年时逻辑本身没错但代码可读性极差而且Date的默认格式化输出Tue Jan 02 ...根本不是给人看的。从 JDK 8 开始就有java.time包处理日期计算用LocalDate非常干净// 位于 util/DateUtil.java public static LocalDate parseDate(String dateStr) { return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(yyyy-MM-dd)); } public static LocalDate calcDueDate(LocalDate borrowDate) { return borrowDate.plusDays(30); }Java 数据库驱动也支持LocalDate直接作为PreparedStatement参数stmt.setObject(1, borrowDate); // 而不是 setDate(1, Date.valueOf(borrowDate))同样查询结果集取出日期时用getObject(borrow_date, LocalDate.class)不再需要手动转java.util.Date再转java.sql.Date。5. 联动不生效、中文变问号、窗口假死六个常见问题的排查与避坑5.1 现象窗口启动后空白或控件重叠错乱现象一个 JFrame 里直接放了多个按钮和输入框运行后有些组件看不到或者拉伸窗口后布局乱得没法看。原因没有理解 JFrame 的 ContentPane 默认是 BorderLayout。往 BorderLayout 容器里连续添加多个没有指定约束的组件时后添加的会覆盖先添加的最终只剩最后一个可见。解决不要在 JFrame 上直接加组件。先创建一个 JPanel给这个 Panel 设置明确的布局管理器常用 BorderLayout 或 GridBagLayout再把所有子组件加到 Panel 上最后frame.add(panel)。如果子组件多且排列复杂用 GridBagLayout 并绘制网格辅助设计或者引入 MigLayout 库额外依赖但绝不建议新手用绝对定位setLayout(null) setBounds——那会让程序在高分屏或不同分辨率下直接崩坏。5.2 现象界面显示中文全是问号或乱码现象运行 Swing 程序后按钮标题、表格内容里的中文全部显示为“???”但在别的程序里数据库中文又正常。原因有三层可能——第一MySQL 表/字段字符集不是 utf8mb4底层存进去的就是乱码第二JDBC 连接 URL 没有指明 characterEncoding第三Java 源文件自身编码和编译器读取编码不一致比如文件是 UTF-8 而 javac 默认用了系统编码Windows 上可能是 GBK。解决三层逐个排查。第一层用ALTER TABLE book CONVERT TO CHARACTER SET utf8mb4;修正表结构第二层确保 JDBC URL 里有characterEncodingutf8见 4.2第三层在我的经验里最容易忽视——Windows 命令行用javac -encoding UTF-8编译或者直接在 IDE 里把项目全局编码设为 UTF-8。这三层都改对乱码必然消失。5.3 现象点击按钮后整个窗口卡死数秒甚至无响应现象点击“查询全部图书”后窗口变成白屏或无法拖拽等好几秒才恢复数据量稍大时表现更明显。原因Swing 是单线程模型所有界面更新必须在事件分发线程EDT上执行。如果在按钮的 ActionListener 里直接查数据库这个线程会被数据库 IO 阻塞期间任何重绘都暂停看起来就是“假死”。解决查询类操作放到后台线程结果返回后通过SwingUtilities.invokeLater或SwingWorker回到 EDT 更新表格。一个典型写法// 位于 view/BookManagePanel.java private void loadBookDataAsync() { // 启动后台线程执行查询避免阻塞 EDT new SwingWorkerVoid, Void() { private ListBook result; Override protected Void doInBackground() throws Exception { result controller.getAllBooks(); return null; } Override protected void done() { try { get(); // 捕获后台线程抛出的异常 refreshTable(result); } catch (Exception ex) { JOptionPane.showMessageDialog(BookManagePanel.this, 查询失败: ex.getMessage()); } } }.execute(); }5.4 现象执行 SQL 时提示“Unknown column”或字段名不匹配现象程序里写的查询语句报Unknown column created_at in field list对照数据库表结构检查确实有这一列。原因最常见的两种——数据库表结构根本没有这一列你记错了列名大小写或者当前连接的数据库选错了库。MySQL 在 Windows 下对表名大小写不敏感但对 Linux 是敏感的如果开发环境是 Windows、部署环境是 Linux表名或列名大小写不一致就会突然报错。解决先用客户端连上目标库确认列名别信“我记得”。然后在代码里统一命名风格——数据库列名用下划线created_atJava 字段用驼峰createdAt在 DAO 映射时显式对应不要依赖名称自动转换。5.5 现象关闭主窗口后程序还在后台运行数据库连接不释放现象点击窗口右上角的 X 关闭程序后任务管理器里 Java 进程还在或者再次运行程序时报“端口被占用”或“数据库连接数超限”。原因setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)不是所有窗口都设置了或者主窗口关了但还有非守护线程在跑比如 SwingWorker 或 Timer 没停。更隐蔽的是 HikariCP 连接池默认在 JVM 退出时不会主动关停所有连接如果进程没有退出连接会一直挂着。解决主窗口采用EXIT_ON_CLOSE并在关闭前钩子里关掉连接池// 位于 MainFrame.java this.addWindowListener(new WindowAdapter() { Override public void windowClosing(WindowEvent e) { // 先提交或回滚事务然后关闭连接池 DBUtil.closeDataSource(); } });5.6 现象借书没反应日志里却出现“库存为负”的异常现象并发借书时或者多人快速点击同一本书有时提示“库存不足”有时却出现available_copies变成 -1 甚至更低。原因检查库存和扣减库存是两个独立操作中间没有任何机制阻止另一个线程同时读到旧值。在单机 GUI 程序里如果 UI 线程用 SwingWorker 并发提交借书请求确实可能发生竞态。解决SQL 层面直接把“检查并扣减”合并成一条原子语句或者给借阅记录表加唯一约束。常见改造方式是把两处逻辑写成一个条件更新UPDATE book SET available_copies available_copies - 1 WHERE id ? AND available_copies 0这条语句返回受影响行数为 0 表示库存不足为 1 表示扣减成功。库存判断从“先查后改”变成了“单条原子操作”不再需要额外加锁或同步。6. 再进一步从“能跑”到“能交付”的几个习惯这个系统做完功能后真正的分水岭在交付环节。你可以在项目收尾阶段做三件小事把完成度从“作业水平”提到“工具水平”。第一件是引入日志记录取代零散的System.out.println。在 DAO 层和 Service 层的关键节点记录入参、操作人、执行结果排查问题时会发现自己在感谢过去的自己。常见做法是用java.util.logging或者引入 SLF4J Logback配置一个按日期滚动的文件输出。别小看这个动作图书管理系统的用户往往不是程序员出了问题他们描述不清楚日志里的时间戳和 SQL 是唯一的排查线索。第二件是做数据库初始化脚本。我习惯在项目根目录放一个db/init.sql包含建库、建表、插入管理员账号。新环境部署时不用开客户端手工敲建表语句一行命令就能完成。管理员账号的初始密码也写清楚并留一个“首次登录强制改密”的口子在 UI 里。常见做法是登录后检查must_change_password字段如果为真就弹出改密对话框。第三件是导出数据能力。图书管理系统运行久了积累的借阅历史有价值加一个“导出当前表格到 CSV”的功能不复杂——用FileWriter按行拼接逗号分隔即可。导出时注意把 JTable 的列头写进去并且统一换行符否则 Excel 打开会格式错乱。这套从 Swing 技术选型、MVC 分层、事务边界到连接池配置的完整链路是我做了多个管理类项目后沉淀下来的实践。刚学 Java 时我也曾觉得 Swing 过时但多次给小型内部团队做工具后渐渐明白一个道理技术栈不决定工具的价值决定价值的是数据一致性、可维护性和交付体验。图书管理系统这个方向最宝贵的收获不是那几百行界面代码而是“事务要原子、SQL 要预编译、界面线程要分离”这三条在任何技术栈都通用的工程底线。如果你也要做这个项目我的建议很简单先手写一遍再改造成分层结构。第一遍掉进坑里第二遍才能理解分层到底解决了什么。如果你按照这篇文章的做法做完并给了真实用户用会发现他们从来不关心你用没用“时髦框架”只关心借书操作三步之内完不完成、数据准不准、关掉重开之后一切还在不在——而你恰好能把这些问题都回答好。内容上有拿不准的地方随时拿代码来验证不要轻信网上的模板希望帮到你。本文还有配套的精品资源点击获取