简介:本资源是一套完整的基于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 注入,不碰 Hibernate
Swing 项目最忌过早引入重量级 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 List<Book> findAll() throws SQLException { String sql = "SELECT * FROM book ORDER BY create_time DESC"; List<Book> 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拼 SQL;Statement.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的自定义模型,数据存List<Book>,所有变更通过fireXXX()通知视图。
// BookTableModel.java public class BookTableModel extends AbstractTableModel { private List<Book> 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(List<Book> 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(List<Integer>)中更新进度条:
@Override protected void process(List<Integer> chunks) { int latest = chunks.get(chunks.size() - 1); progressBar.setValue(latest); }4.3 现象:Windows 上字体模糊、按钮边框发虚,Mac/Linux 正常
原因:JDK 默认未启用高 DPI 缩放支持,Win10/11 高分屏下 Swing 渲染失真。
解决:启动时添加 JVM 参数-Dsun.java2d.uiScale=1.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 SwingWorker<Void, 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 JAR:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals><goal>shade</goal></goals> </execution> </executions> </plugin>打包后检查 JAR 内是否含org/sqlite/JDBC.class。
5. 进阶技巧:用 Properties 实现配置热更新与多语言切换
一个真正可交付的桌面系统,不能每次改数据库路径或界面文字就重新编译。Swing 项目最实用的“软升级”能力,就是把配置项外置为config.properties,并在运行时监听文件变化——无需重启,改完保存即生效。
5.1 定义配置文件结构与加载逻辑
创建config.properties放在 JAR 同目录,内容如下:
# 数据库配置 db.path=library.db db.backup.enable=true db.backup.interval=86400 # 界面配置 ui.language=zh-CN ui.font.size=14 ui.theme=system # 功能开关 feature.export.excel=true feature.import.csv=false加载时用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 秒检查配置文件修改时间,避免高频 IO;SwingUtilities.updateComponentTreeUI()是 Swing 官方推荐的 UI 主题刷新方式,比手动repaint()更彻底;UIManager.put()修改全局字体,影响所有 JLabel、JButton 等组件。
参数说明:db.backup.interval=86400单位是秒(24 小时),业务层可据此触发自动备份;feature.export.excel=true作为开关,UI 层按钮setVisible(config.getBoolean("feature.export.excel"))控制显隐。
5.2 实现简易多语言支持:用 ResourceBundle + Locale 切换语言包
Swing 本身不支持 i18n,但ResourceBundle可以完美对接。准备两个文件:
messages_zh_CN.properties:btn.borrow=\u501F\u9605(借阅)messages_en_US.properties:btn.borrow=Borrow
加载时根据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弹窗。这些不是“最佳实践”,而是交付底线。希望帮到你。
本文还有配套的精品资源,点击获取