简介本资源是一套完整的基于Java Swing与MySQL开发的图书管理系统课程设计/毕业设计项目面向计算机相关专业本科生及Java初学者解决传统图书管理中人工操作效率低、信息检索不便、借阅流程繁琐等实际问题。项目采用Swing构建图形化界面实现用户注册登录、图书模糊搜索、借还书操作及后台数据管理含图书、类别、用户、借阅记录四大模块兼顾功能性与用户体验。压缩包共116个文件包含20个核心Java源码、82个编译后Class文件如LoginFrm、AdminMenuFrm、UserMenuFrm等典型窗体类、9个XML配置文件用于界面布局或数据映射、4个依赖JAR包及1个IntelliJ项目配置文件整体体积2.94MB结构清晰便于理解MVC分层逻辑与Swing事件驱动机制。目前已有331人学习下载提供可直接运行的完整工程含全部业务逻辑实现、数据库建表脚本及基础测试用例是掌握Java桌面应用开发与数据库集成实践的优质入门范例。1. 为什么现在还要用 Java Swing 做图书管理系统——它不是古董而是可控性、低依赖、可交付的“确定性解”你可能刚刷到某招聘 JD 上写着“熟悉 Swing 或 JavaFX”心里一愣这玩意儿不是 JDK 8 就该退休了吗但真实产线里很多高校实训项目、中小型图书馆本地化管理终端、甚至某些政务内网边缘系统至今仍在用 Swing 搭建图书管理系统——不是因为怀旧而是因为它不依赖外部框架、不绑定 Spring Boot 的 Tomcat 生命周期、不引入 Maven 依赖地狱、不强制要求数据库连接池或 REST API 网关。它就是一个 JAR 包双击即用用户装完 JDK 就能跑管理员改个 properties 文件就能切库运维不用配 Nginx 反代、不用开防火墙端口、不用管 TLS 证书续期。这不是技术倒退而是在「交付确定性」和「运行轻量性」之间做了一次精准取舍。本文不讲 Swing 已淘汰只讲当你手头只有 JDK、一台 Windows 7 电脑、一份 Excel 图书清单、以及三天交付时间时怎么用纯 Swing JDBC 写出一个真正能录入、查询、借阅、归还、导出报表的图书管理系统——不花哨但能用不炫技但不出错不联网但数据不丢。适合 Java 初学者巩固 AWT 事件模型、JDBC 连接池基础、Swing 组件协同逻辑也适合老手快速搭一个离线原型验证业务流程。2. 从零启动用最简结构跑通图书管理核心流程Swing 不是 Web 框架没有 MVC 自动绑定也没有注解驱动路由。它的“最小可行系统”必须靠三层硬编码撑起来UI 层JFrame JPanel、业务层BookService 类、数据层JDBC 直连 SQLite。别急着上 H2 或 Derby——SQLite 是本项目唯一推荐的嵌入式数据库原因很实在单文件、无服务进程、Java 通过 sqlite-jdbc 驱动直连打包后整个系统就一个 JAR 一个 .db 文件用户双击即用删掉就清空比任何 ORM 都干净。2.1 创建主窗口与导航面板用 CardLayout 实现页面切换避免 JFrame 堆叠翻车Swing 最容易翻车的地方就是把所有按钮、表格、输入框全塞进一个 JFrame ——结果改一行布局整个 UI 错位。正确做法是用CardLayout管理多个功能面板录入页、查询页、借阅页主窗口只负责切换卡片。这样每个功能模块独立开发、独立测试后期加新功能也不影响旧逻辑。// MainFrame.java public class MainFrame extends JFrame { private CardLayout cardLayout; private JPanel cardPanel; public MainFrame() { setTitle(图书管理系统 v1.0); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setBounds(100, 100, 960, 600); cardLayout new CardLayout(); cardPanel new JPanel(cardLayout); // 添加各功能面板后续章节实现 cardPanel.add(new BookInputPanel(this), input); cardPanel.add(new BookSearchPanel(this), search); cardPanel.add(new BorrowPanel(this), borrow); add(cardPanel, BorderLayout.CENTER); // 底部导航栏 JPanel navPanel new JPanel(new FlowLayout(FlowLayout.RIGHT)); JButton inputBtn new JButton(图书录入); JButton searchBtn new JButton(图书查询); JButton borrowBtn new JButton(借阅管理); inputBtn.addActionListener(e - cardLayout.show(cardPanel, input)); searchBtn.addActionListener(e - cardLayout.show(cardPanel, search)); borrowBtn.addActionListener(e - cardLayout.show(cardPanel, borrow)); navPanel.add(inputBtn); navPanel.add(searchBtn); navPanel.add(borrowBtn); add(navPanel, BorderLayout.SOUTH); } }逻辑说明CardLayout是 Swing 原生布局管理器专为多页面切换设计。它不重绘整个窗口只切换可见组件内存占用低、响应快。cardPanel.add(panel, key)中的key是字符串标识符后续用cardLayout.show()切换时传入即可。参数说明setBounds(x,y,width,height)显式设窗体尺寸避免不同 DPI 下缩放异常BorderLayout.CENTER/SOUTH是标准边界布局比null布局更稳定且适配高分屏。2.2 设计图书实体与 DAO 层用 POJO PreparedStatement 防 SQL 注入不碰 HibernateSwing 项目最忌过早引入重量级 ORM。本系统用最朴素的BookPOJO 手写 JDBC DAO好处是SQL 语句完全可控、报错堆栈直达执行行、调试时能一眼看出哪条 INSERT 没绑参、事务边界清晰每个方法自己 commit/rollback。// Book.java public class Book { private Long id; private String isbn; private String title; private String author; private String publisher; private Integer stock; // 可借数量 private Date createTime; // 构造、getter/setter 省略IDE 自动生成即可 } // BookDao.java public class BookDao { private static final String DB_PATH library.db; private Connection conn; public BookDao() { initConnection(); } private void initConnection() { try { Class.forName(org.sqlite.JDBC); conn DriverManager.getConnection(jdbc:sqlite: DB_PATH); createTableIfNotExists(); } catch (Exception e) { throw new RuntimeException(数据库初始化失败, e); } } private void createTableIfNotExists() throws SQLException { String sql CREATE TABLE IF NOT EXISTS book ( id INTEGER PRIMARY KEY AUTOINCREMENT, isbn TEXT UNIQUE NOT NULL, title TEXT NOT NULL, author TEXT, publisher TEXT, stock INTEGER DEFAULT 0, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ; try (Statement stmt conn.createStatement()) { stmt.execute(sql); } } public void save(Book book) throws SQLException { String sql INSERT INTO book(isbn,title,author,publisher,stock) VALUES(?,?,?,?,?); try (PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, book.getIsbn()); ps.setString(2, book.getTitle()); ps.setString(3, book.getAuthor()); ps.setString(4, book.getPublisher()); ps.setInt(5, book.getStock()); ps.executeUpdate(); // 获取自增主键 try (ResultSet rs ps.getGeneratedKeys()) { if (rs.next()) { book.setId(rs.getLong(1)); } } } } public ListBook findAll() throws SQLException { String sql SELECT * FROM book ORDER BY create_time DESC; ListBook list new ArrayList(); try (Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { Book b new Book(); b.setId(rs.getLong(id)); b.setIsbn(rs.getString(isbn)); b.setTitle(rs.getString(title)); b.setAuthor(rs.getString(author)); b.setPublisher(rs.getString(publisher)); b.setStock(rs.getInt(stock)); b.setCreateTime(rs.getTimestamp(create_time)); list.add(b); } } return list; } }逻辑说明PreparedStatement是防 SQL 注入的铁律哪怕字段全是字符串也不能用String.format拼 SQLStatement.RETURN_GENERATED_KEYS是获取 SQLite 自增 ID 的关键参数否则getGeneratedKeys()返回空结果集createTableIfNotExists()在首次运行时建表避免手动建库。参数说明DB_PATH library.db是相对路径JAR 包运行时会生成在当前目录stock INTEGER DEFAULT 0设置默认值防止插入 NULL 导致后续借阅逻辑空指针ORDER BY create_time DESC保证新录入图书排在列表顶部符合用户直觉。3. 核心功能落地借阅状态闭环与表格实时刷新机制图书管理系统区别于普通 CRUD 的关键在于「借阅」这个状态变更操作——它要同时更新图书库存、记录借阅流水、校验库存是否充足、并让 UI 表格立刻反映变化。很多初学者卡在这里点了“借出”按钮数据库改了但 JTable 还显示旧数据。根本原因是没触发TableModel的fireTableRowsUpdated()通知。3.1 构建可编辑的图书表格用 AbstractTableModel 封装数据源避免 DefaultTableModel 的坑DefaultTableModel虽然简单但它内部用Vector存数据且addRow()/removeRow()方法线程不安全多线程操作比如后台异步加载数据极易抛ConcurrentModificationException。生产级 Swing 表格必须用继承AbstractTableModel的自定义模型数据存ListBook所有变更通过fireXXX()通知视图。// BookTableModel.java public class BookTableModel extends AbstractTableModel { private ListBook data new ArrayList(); private String[] columnNames {ID, ISBN, 书名, 作者, 出版社, 库存, 入库时间}; Override public int getRowCount() { return data.size(); } Override public int getColumnCount() { return columnNames.length; } Override public String getColumnName(int columnIndex) { return columnNames[columnIndex]; } Override public Object getValueAt(int rowIndex, int columnIndex) { Book book data.get(rowIndex); switch (columnIndex) { case 0: return book.getId(); case 1: return book.getIsbn(); case 2: return book.getTitle(); case 3: return book.getAuthor(); case 4: return book.getPublisher(); case 5: return book.getStock(); case 6: return new SimpleDateFormat(yyyy-MM-dd HH:mm).format(book.getCreateTime()); default: return ; } } // 提供外部更新数据的方法 public void setData(ListBook newData) { this.data new ArrayList(newData); fireTableDataChanged(); // 全量刷新 } // 单行更新用于借阅后库存变更 public void updateStock(Long bookId, Integer newStock) { for (int i 0; i data.size(); i) { if (data.get(i).getId().equals(bookId)) { data.get(i).setStock(newStock); fireTableRowsUpdated(i, i); // 只刷新这一行 break; } } } }逻辑说明fireTableDataChanged()通知整个表格重绘适用于首次加载或搜索结果替换fireTableRowsUpdated(i,i)只刷新第 i 行性能更好适用于单条记录状态变更如借出/归还SimpleDateFormat格式化时间避免Date.toString()输出带时区的混乱字符串。参数说明columnNames数组长度必须与getValueAt()的switch分支数一致否则JTable渲染越界setData()中new ArrayList(newData)是深拷贝防止外部修改影响表格数据源。3.2 实现借阅逻辑事务控制 库存校验 流水记录三合一借阅不是简单减库存。它必须满足① 库存 0 才允许借② 减库存和插入借阅记录必须原子执行要么都成功要么都回滚③ 借阅后 UI 表格立即更新。以下代码封装在BorrowService中调用方只需传入bookId和readerId读者编号本例简化为字符串// BorrowService.java public class BorrowService { private BookDao bookDao; private Connection conn; public BorrowService(BookDao bookDao) { this.bookDao bookDao; this.conn bookDao.getConn(); // 从 DAO 复用连接 } public boolean borrow(Long bookId, String readerId) { try { conn.setAutoCommit(false); // 开启事务 // 1. 查询当前库存 Book book findBookById(bookId); if (book null || book.getStock() 0) { throw new IllegalStateException(图书库存不足无法借阅); } // 2. 更新库存 int newStock book.getStock() - 1; updateBookStock(bookId, newStock); // 3. 记录借阅流水本例简化为内存 List实际应建 borrow_log 表 BorrowLog log new BorrowLog(); log.setBookId(bookId); log.setReaderId(readerId); log.setBorrowTime(new Date()); saveBorrowLog(log); conn.commit(); return true; } catch (Exception e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } JOptionPane.showMessageDialog(null, 借阅失败 e.getMessage(), 错误, JOptionPane.ERROR_MESSAGE); return false; } finally { try { conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } } } private Book findBookById(Long id) throws SQLException { String sql SELECT * FROM book WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Book b new Book(); b.setId(rs.getLong(id)); b.setIsbn(rs.getString(isbn)); b.setTitle(rs.getString(title)); b.setAuthor(rs.getString(author)); b.setPublisher(rs.getString(publisher)); b.setStock(rs.getInt(stock)); b.setCreateTime(rs.getTimestamp(create_time)); return b; } } } return null; } private void updateBookStock(Long bookId, int newStock) throws SQLException { String sql UPDATE book SET stock ? WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, newStock); ps.setLong(2, bookId); ps.executeUpdate(); } } private void saveBorrowLog(BorrowLog log) throws SQLException { // 实际项目应建表CREATE TABLE borrow_log(id INTEGER PRIMARY KEY, book_id, reader_id, borrow_time) System.out.println(借阅记录 log.getReaderId() 借走 ID log.getBookId()); } }逻辑说明conn.setAutoCommit(false)是事务起点findBookById()必须在事务内执行否则并发借阅时可能查到旧库存updateBookStock()和saveBorrowLog()都复用同一连接确保在同一个事务上下文中JOptionPane.showMessageDialog()是 Swing 原生弹窗比System.out.println更符合桌面应用交互习惯。参数说明BorrowLog是简化版流水对象实际部署需建borrow_log表并持久化readerId本例未做合法性校验如读者是否存在这是可扩展点但非 MVP 必选项。4. 避坑指南Swing 图形界面开发中 5 个血泪经验Swing 表面简单实则暗坑密布。以下问题均来自真实项目踩坑记录不是教科书理论而是“改了三小时才发现是这里”的玄学现场。4.1 现象JTable 点击列标题排序后新增行不显示或顺序错乱原因TableRowSorter默认对AbstractTableModel数据做深层排序但setValueAt()修改单元格值时TableRowSorter不会自动触发重排序导致视图与模型不一致。解决禁用自动排序改用Collections.sort()在模型层排序后调用fireTableDataChanged()。或使用TableRowSorter的toggleSortOrder()配合model.fireTableDataChanged()强制刷新。4.2 现象SwingWorker 执行耗时查询后JProgressBar不动直到任务结束才突变 100%原因SwingWorker的publish()方法必须配合process()回调才能更新 UI直接在doInBackground()里setProgress()不生效。解决在doInBackground()中调用publish(Integer...)在process(ListInteger)中更新进度条Override protected void process(ListInteger chunks) { int latest chunks.get(chunks.size() - 1); progressBar.setValue(latest); }4.3 现象Windows 上字体模糊、按钮边框发虚Mac/Linux 正常原因JDK 默认未启用高 DPI 缩放支持Win10/11 高分屏下 Swing 渲染失真。解决启动时添加 JVM 参数-Dsun.java2d.uiScale1.25按屏幕缩放比例调整或在main()开头加System.setProperty(sun.java2d.uiScale, 1.25); UIManager.setLookAndFeel(UIManager.getSystemLookAndFeel());4.4 现象点击“导出 Excel”按钮后程序假死 10 秒期间无法关闭窗口原因Swing 事件调度线程EDT被阻塞所有 UI 交互冻结。导出操作如 Apache POI 写文件必须异步执行。解决用SwingWorker封装导出逻辑done()回调中弹出完成提示new SwingWorkerVoid, Void() { Override protected Void doInBackground() throws Exception { exportToExcel(bookList); // 耗时操作 return null; } Override protected void done() { JOptionPane.showMessageDialog(frame, 导出完成, 提示, JOptionPane.INFORMATION_MESSAGE); } }.execute();4.5 现象打包成 JAR 后SQLite 驱动报java.lang.ClassNotFoundException: org.sqlite.JDBC原因Maven 打包时未将sqlite-jdbc的 JAR 合并进主 JAR或MANIFEST.MF中Class-Path缺失。解决用 Maven Shade Plugin 打 Fat JARplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/version executions execution phasepackage/phase goalsgoalshade/goal/goals /execution /executions /plugin打包后检查 JAR 内是否含org/sqlite/JDBC.class。5. 进阶技巧用 Properties 实现配置热更新与多语言切换一个真正可交付的桌面系统不能每次改数据库路径或界面文字就重新编译。Swing 项目最实用的“软升级”能力就是把配置项外置为config.properties并在运行时监听文件变化——无需重启改完保存即生效。5.1 定义配置文件结构与加载逻辑创建config.properties放在 JAR 同目录内容如下# 数据库配置 db.pathlibrary.db db.backup.enabletrue db.backup.interval86400 # 界面配置 ui.languagezh-CN ui.font.size14 ui.themesystem # 功能开关 feature.export.exceltrue feature.import.csvfalse加载时用FileInputStream读取并监听文件最后修改时间// ConfigManager.java public class ConfigManager { private static final String CONFIG_FILE config.properties; private Properties props new Properties(); private long lastModified 0L; public ConfigManager() { loadConfig(); startAutoReload(); } private void loadConfig() { try (FileInputStream fis new FileInputStream(CONFIG_FILE)) { props.load(fis); lastModified new File(CONFIG_FILE).lastModified(); } catch (IOException e) { // 首次运行无配置文件用默认值 props.setProperty(db.path, library.db); props.setProperty(ui.language, zh-CN); } } private void startAutoReload() { Timer timer new Timer(); timer.scheduleAtFixedRate(new TimerTask() { Override public void run() { File f new File(CONFIG_FILE); if (f.exists() f.lastModified() ! lastModified) { System.out.println(检测到 config.properties 变更正在重载...); loadConfig(); // 通知 UI 更新如刷新语言、字体 UIManager.put(Label.font, new Font(微软雅黑, Font.PLAIN, Integer.parseInt(props.getProperty(ui.font.size, 14)))); SwingUtilities.updateComponentTreeUI(MainFrame.getInstance()); // 重绘全局 UI } } }, 0, 5000); // 每 5 秒检查一次 } public String getProperty(String key, String defaultValue) { return props.getProperty(key, defaultValue); } }逻辑说明TimerTask每 5 秒检查配置文件修改时间避免高频 IOSwingUtilities.updateComponentTreeUI()是 Swing 官方推荐的 UI 主题刷新方式比手动repaint()更彻底UIManager.put()修改全局字体影响所有 JLabel、JButton 等组件。参数说明db.backup.interval86400单位是秒24 小时业务层可据此触发自动备份feature.export.exceltrue作为开关UI 层按钮setVisible(config.getBoolean(feature.export.excel))控制显隐。5.2 实现简易多语言支持用 ResourceBundle Locale 切换语言包Swing 本身不支持 i18n但ResourceBundle可以完美对接。准备两个文件messages_zh_CN.propertiesbtn.borrow\u501F\u9605借阅messages_en_US.propertiesbtn.borrowBorrow加载时根据config.properties中ui.language动态选择// I18nUtil.java public class I18nUtil { private static ResourceBundle bundle; public static void init(String lang) { Locale locale en_US.equals(lang) ? Locale.US : Locale.CHINA; bundle ResourceBundle.getBundle(messages, locale); } public static String get(String key) { return bundle.getString(key); } } // 在 MainFrame 构造函数中 String lang ConfigManager.getInstance().getProperty(ui.language, zh-CN); I18nUtil.init(lang); JButton borrowBtn new JButton(I18nUtil.get(btn.borrow)); // 自动匹配语言避坑提醒.properties文件必须用 UTF-8 编码中文需转 Unicode可用native2ascii工具否则ResourceBundle加载乱码Locale.CHINA对应zh_CN不是zh-CN破折号会导致加载失败。我带过的实习生里有三个在毕业设计答辩前两天因为没处理好JTable的线程安全更新导致评委点击“查询”后表格空白当场演示崩盘。后来他们统一加了SwingUtilities.invokeLater()包裹所有 UI 更新操作再没翻过车。Swing 的本质不是炫技而是把确定性刻进每一行代码里——数据库路径写死不行得抽成配置UI 字体写死不行得支持 DPI借阅失败不提示不行得JOptionPane弹窗。这些不是“最佳实践”而是交付底线。希望帮到你。本文还有配套的精品资源点击获取