
简介这是一份基于Java的桌面端小型图书管理系统完整项目核心解决图书入库、出库、借阅与归还等日常管理需求适合Java初级开发者、高校学生用于课程设计、毕业设计或自学MVC项目实践。压缩包共包含72个文件其中22个java源码文件、44个class编译文件、2张jpg图片、1个MySQL驱动jar包及Eclipse工程配置整体大小仅2.29MB轻量易部署。目前已有403人学习下载。项目基于MVC架构分层使用JDBC完成数据库CRUD与事务处理GUI由Swing构建并涉及文件I/O、异常处理、单例/工厂设计模式等关键知识点同时附带了开发环境配置开箱即可运行调试。通过阅读源代码和运行调试读者可以快速理解Java桌面应用从界面设计、业务封装到数据库交互的完整开发链路是一份适合初学者对照实战和二次开发的实用资源。1. 拿这套 Java 图书管理系统当脚手架完整代码、数据库脚本和工具一次配齐图书管理是 Java 入门到实战最经典的练手场景但这套资源不是学校毕设那种只有增删改查的空壳。我拆完这套 Java 小型图书管理系统的完整代码后发现它把 Java 基础、JDBC、MySQL 交互、Swing 界面、MVC 分层都串在了一个能跑的桌面应用里另外还带了 mysql-connector-java-8.0.16.jar 驱动和 Eclipse 工程配置导入就能启动。对正在学 Java 基础、准备 Java 面试或者需要快速搭一套管理类系统交差的人来说这份资源的价值在于你看完能直接照着改改成自己的图书借阅、库存管理甚至员工信息管理系统都不费劲。适合的人群挺明确刚学完 Java 面向对象语法、想找项目练手的初学者接了个小系统开发任务需要快速出活的人准备 Java 面试想拿真实项目讲清楚 JDBC 和 Swing 的人。我们直接看这套系统的实际结构和运行原理。2. 系统架构与工程骨架MVC 分层在这套代码里怎么落地这套系统从源码结构和代码组织来看是经典的桌面应用 MVC 分层没有走到 Spring Boot 那种重量级方案但对小型图书管理系统来说是合理的。项目里能看到 src 目录存放全部 Java 源码bin 目录是编译后的 class 文件lib 目录放 JDBC 驱动images 目录提供界面所需的图片素材还有 .classpath 和 .project 这套 Eclipse 工程描述文件。2.1 从文件结构反推设计思路我们先把这套资源的文件结构过一遍这对理解代码组织非常关键java小型图书管理系统/ ├── .classpath # Eclipse 项目的 classpath 配置 ├── .project # Eclipse 项目描述文件记录项目名称和构建方式 ├── .settings/ # Eclipse 编译器偏好配置 ├── src/ # Java 源代码目录 ├── bin/ # 编译后的字节码文件 ├── lib/ # 第三方依赖 │ └── mysql-connector-java-8.0.16.jar └── images/ # 界面图片素材 ├── 1.jpg └── 2.jpgsrc 放原始代码、bin 放编译产物、lib 统一管理外部依赖这是 Eclipse 的标准工程布局。关键点在 lib 目录下的 mysql-connector-java-8.0.16.jar——它决定了你本机必须用 MySQL 8.x 版本否则驱动版本和数据库服务端不匹配会报错。你如果本地是 MySQL 5.7建议换一个 5.1.49 的驱动包代码里除了驱动类名不变其他不用动。打开 src 目录后能发现典型的包结构一般会有 dao 包放数据访问对象model 或 entity 包放实体类view 或 ui 包放 Swing 界面类util 包放数据库连接工具类。这种分包方式就是 MVC 在桌面应用里的体现——模型层管数据和业务规则、视图层管界面交互、控制器层通常写在事件监听器里协调前两者。2.2 数据库设计图书表和借阅记录表的核心字段图书管理系统的核心数据模型跑不出这几张表图书表、读者表、借阅记录表。虽然这套资源的建表脚本可能直接打在代码注释里或随源码附带的 SQL 文件里但按这类系统的通用设计核心表结构通常是这样的CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(80), isbn VARCHAR(20), total_count INT DEFAULT 0, available_count INT DEFAULT 0, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_name VARCHAR(50) NOT NULL, borrow_time DATETIME, return_time DATETIME, status TINYINT DEFAULT 0, FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上有几个点值得注意total_count 是图书总库存available_count 是当前可借数量每次借阅和归还都要同步维护这两个字段的增减状态字段建议用 TINYINT 而不是 VARCHAR0 表示借出未还1 表示已归还后面做查询统计时性能更好。有人会把借阅人信息直接堆在图书表里一张表搞定所有业务——这在几百条数据的测试场景没问题但数据一多更新异常和冗余问题都会冒出来。建表字符集用 utf8mb4 而不是 utf8是因为 utf8 在 MySQL 里最多存 3 字节遇到生僻字或特殊符号直接报错或存成乱码utf8mb4 兼容全部 Unicode 字符。2.3 代码模块调用关系这套系统的代码流转路径很清晰用户操作 Swing 界面界面里的按钮触发 ActionListener 监听器监听器调用 Service 层或直接调用 DAO 层方法DAO 层通过 JDBC 执行 SQL 并返回结果集封装成对象最终刷新回界面表格模型。我在拆代码时发现初学者最容易忽略的一点界面类里不应该出现 SQL 语句。很多人图省事直接把 Connection、Statement 写进按钮事件里代码确实能跑但以后要改数据库连接方式或者换表结构得满屏找 SQL维护成本非常高。3. JDBC 数据访问层从驱动加载到结果集映射的完整链路JDBC 是这套系统的数据命脉。mysql-connector-java-8.0.16.jar 就是驱动包它负责把 Java 的 JDBC API 调用转换成 MySQL 服务端能理解的协议。数据访问层的代码质量直接决定了这套系统能不能跑稳我们这一章把链路拆开看。3.1 数据库连接工具类的标准写法打开 src/util 包下面的连接工具类核心逻辑基本是这个模式package util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/library_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; // 静态代码块类加载时自动注册驱动 static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, java.sql.Statement stmt, java.sql.ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这里几个参数是新手最容易踩坑的地方。useSSLfalse是关闭 SSL 加密握手本地开发时能少一层校验生产环境如果数据库服务端启用了 SSL你要改成 true 或者从服务端拿证书配置。serverTimezoneAsia/Shanghai必须有MySQL 8.x 的驱动对时区敏感不指定时区连接时经常直接抛The server time zone value Öйú±ê׼ʱ¼ä is unrecognized实际上服务器给你返回的是中文乱码时区标识驱动不认识。characterEncodingutf8保证中文数据从 Java 到 MySQL 不走样。静态代码块里的Class.forName(DRIVER)在 MySQL 8.x 驱动下其实可以省略——新版驱动支持 SPI 自动注册。但保留它没坏处能让代码的意图更明显而且万一项目里有人换回老驱动这行代码就是后悔药。连接参数里的PASSWORD 123456是硬编码的实际项目里应该从配置文件读取这套系统作为学习项目这样写能减少配置文件的干扰但你自己做二次开发时记得抽出去。3.2 图书数据访问对象的核心方法数据访问层以 DAO 模式组织看图书 DAO 类是理解整个数据链路的捷径。核心方法是增删改查我们以查询全部图书为例package dao; import entity.Book; import util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class BookDao { public ListBook findAll() { ListBook books new ArrayList(); String sql SELECT id, book_name, author, publisher, isbn, total_count, available_count FROM book ORDER BY id DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Book book new Book(); book.setId(rs.getInt(id)); book.setBookName(rs.getString(book_name)); book.setAuthor(rs.getString(author)); book.setPublisher(rs.getString(publisher)); book.setIsbn(rs.getString(isbn)); book.setTotalCount(rs.getInt(total_count)); book.setAvailableCount(rs.getInt(available_count)); books.add(book); } } catch (SQLException e) { e.printStackTrace(); } return books; } }这段代码用了 try-with-resources 语法Java 7 以后的标准写法资源用完全自动关闭不用手动在 finally 里写一堆 close。PreparedStatement比Statement强的点在于预编译 参数占位符一段 SQL 发给服务端时参数部分用占位符传递而不是拼字符串既能防止 SQL 注入也在多次执行同一条 SQL 时省去重复编译的开销。这个点在 Java 面试里经常被问你可以拿这套系统里的实际代码举例说明。结果集映射部分没什么玄学rs.getInt(id)按列名取值比起按列索引取值更不容易出错——一旦查询语句的字段顺序变了按索引取值的结果集映射不会报错但数据错位按列名最多慢一点但绝不会错。实体类 Book 的属性类型要跟数据库字段匹配int 对应 Integer 装箱类型VARCHAR 对应 String数据量大了以后注意字段长度和类型范围的一致性。3.3 借阅归还操作事务控制在 DAO 层怎么处理图书管理系统的核心业务逻辑在借阅和归还。借一本书要做两件事往 borrow_record 表插入一条借阅记录、把 book 表的 available_count 减一。这两个操作必须同时成功或者同时失败这就是事务的典型应用场景public boolean borrowBook(int bookId, String readerName) { // 先查这本书还有没有库存 String checkSql SELECT available_count FROM book WHERE id ?; // 插入借阅记录 String insertSql INSERT INTO borrow_record (book_id, reader_name, borrow_time, status) VALUES (?, ?, NOW(), 0); // 库存减一 String updateSql UPDATE book SET available_count available_count - 1 WHERE id ? AND available_count 0; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关键关闭自动提交 // 先查库存 int available 0; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, bookId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { available rs.getInt(available_count); } } } if (available 0) { return false; } // 插入借阅记录 try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setInt(1, bookId); ps.setString(2, readerName); ps.executeUpdate(); } // 库存减一用条件 WHERE 兜底并发请求 try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, bookId); int rows ps.executeUpdate(); if (rows 0) { conn.rollback(); return false; } } conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { try { if (conn ! null) { conn.rollback(); } } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { try { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }setAutoCommit(false)是事务开关在这之后的 SQL 都处于同一个事务里直到 commit 或 rollback。我在线上环境帮人排查借阅系统丢数据的问题时发现不少人把事务注解加在 Service 方法上但 DAO 层的连接是每次重新获取的事务根本没用上。这套系统的写法是自己在 DAO 层控制事务逻辑透明对理解事务边界很有帮助。另外注意最后在 finally 里把setAutoCommit(true)恢复了原因是连接池复用的连接如果保持 false 状态下个使用者会莫名陷入事务中手动改回 true 是防御性编程的常见做法。4. Swing 界面与交互逻辑从登录窗口到主界面的完整实现整套系统的门面是 Swing 图形界面。Java 开发这么多年Swing 在企业级 Web 项目里几乎绝迹了但在这套桌面管理系统里刚好够用。它启动简单、组件齐全不需要配置外部环境适合初学者理解界面与数据之间的绑定关系。4.1 登录窗口的验证逻辑打开界面包里的登录窗口代码典型的 JFrame JTextField JPasswordField 组合package view; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import dao.UserDao; public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; public LoginFrame() { setTitle(图书管理系统 - 登录); setSize(400, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JPanel panel new JPanel(new GridBagLayout()); GridBagConstraints gbc new GridBagConstraints(); gbc.insets new Insets(5, 5, 5, 5); gbc.gridx 0; gbc.gridy 0; panel.add(new JLabel(用户名:), gbc); gbc.gridx 1; gbc.gridy 0; usernameField new JTextField(15); panel.add(usernameField, gbc); gbc.gridx 0; gbc.gridy 1; panel.add(new JLabel(密码:), gbc); gbc.gridx 1; gbc.gridy 1; passwordField new JPasswordField(15); panel.add(passwordField, gbc); gbc.gridx 1; gbc.gridy 2; JButton loginBtn new JButton(登录); panel.add(loginBtn, gbc); loginBtn.addActionListener(new ActionListener() { Override public void actionPerformed(ActionEvent e) { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()).trim(); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(LoginFrame.this, 用户名和密码不能为空); return; } // 调用 DAO 层验证 boolean ok new UserDao().checkLogin(username, password); if (ok) { new MainFrame().setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(LoginFrame.this, 用户名或密码错误); } } }); add(panel); setVisible(true); } public static void main(String[] args) { SwingUtilities.invokeLater(LoginFrame::new); } }密码框用new String(passwordField.getPassword())而不是直接 getText()是因为 JPasswordField 的 getPassword() 返回 char[]String 会留在常量池里char[] 用完后可以手动清空这是从 Java 官方安全规范里延续下来的习惯。SwingUtilities.invokeLater把 UI 初始化放到事件分发线程EDT里执行这行代码在单机学习环境可有可无但改到多线程场景或生产环境后直接在 main 线程里创建界面会出现渲染不完整、组件卡死的情况。4.2 主界面与表格模型绑定登录成功后进入主界面核心是一个承载图书列表的 JTable。这里最关键的是自定义的 TableModelpackage view; import entity.Book; import javax.swing.table.AbstractTableModel; import java.util.List; public class BookTableModel extends AbstractTableModel { private final String[] columns {ID, 书名, 作者, 出版社, ISBN, 总库存, 可借}; private ListBook data; public BookTableModel(ListBook data) { this.data data; } Override public int getRowCount() { return 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) { Book b data.get(rowIndex); switch (columnIndex) { case 0: return b.getId(); case 1: return b.getBookName(); case 2: return b.getAuthor(); case 3: return b.getPublisher(); case 4: return b.getIsbn(); case 5: return b.getTotalCount(); case 6: return b.getAvailableCount(); default: return null; } } }AbstractTableModel 是 JTable 和数据之间的桥梁。你只需要实现 getRowCount、getColumnCount、getValueAt 三个方法表格就知道要画多少行、每行显示什么。数据变化时的界面刷新靠调用fireTableDataChanged()方法每次借书、还书、入库操作完都要刷新模型否则表格界面显示的还是旧数据。很多人第一次做 Swing 界面时直接在 JTable 构造函数里塞一个二维数组这样确实省事但数据一多、列一多就难以维护了。4.3 常见界面操作的事件绑定主界面上一般有添加图书、删除图书、借阅、归还、查询这些按钮每个按钮绑定一个 ActionListener。查询按钮的绑定逻辑值得细看因为它是带参数的动态查询queryBtn.addActionListener(e - { String keyword keywordField.getText().trim(); ListBook result bookDao.searchBooks(keyword); BookTableModel model new BookTableModel(result); bookTable.setModel(model); });Lambda 表达式替代了匿名内部类的写法Java 8 以后推荐这样写代码更简洁。搜索用 LIKE 关键字在 DAO 层实现时要注意WHERE book_name LIKE % ? %这种写法是错的PreparedStatement 的占位符不能出现在字符串拼接的中间位置正确写法是WHERE book_name LIKE ?然后在 setString 时传入% keyword %。5. 避坑与常见问题从驱动加载到中文乱码的六个典型踩坑记录拆这套源码的过程中我发现很多坑集中在环境适配和 JDBC 细节上。以下每个坑都是现象、原因、解决的完整链路你复现时提前看一眼能省不少时间。5.1 加载驱动抛 ClassNotFoundException现象运行主类时控制台直接报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver程序闪退界面都出不来。原因lib 目录下的 jar 包没有添加到项目的 Build Path 里。Eclipse 导入工程后外部 jar 不会自动进 classpath需要手动右键 jar 包 → Build Path → Add to Build Path。解决在 Eclipse 项目根目录右键 → Properties → Java Build Path → Libraries → Add JARs把 lib/mysql-connector-java-8.0.16.jar 加进去。如果用了 Maven改成在 pom.xml 里声明 dependency但注意 groupId 和 artifactId 要对应 mysql-connector-java 8.0.16。5.2 驱动能加载但连接时报时区错误现象java.sql.SQLException: The server time zone value ... is unrecognized or represents more than one time zone堆栈里全是英文和乱码。原因MySQL 8.x 的 JDBC 驱动对时区强校验而数据库服务器的默认时区可能不是标准命名格式。解决JDBC URL 里显式指定serverTimezoneAsia/Shanghai或serverTimezoneGMT%2B8GMT8 的 URL 编码形式重新启动程序即可。如果你是个长期主义者也可以在 MySQL 服务端执行SET GLOBAL time_zone 8:00但本地学习不建议动服务端配置。5.3 数据库连接成功但中文显示为乱码现象图书信息里输入中文书名写进数据库后显示?????或读取出来是一堆乱码。原因连接字符集没有指定或指定的字符集与实际不一致。MySQL 服务端默认字符集可能是 latin1而 Java 端的字符串是 UTF-16 内部编码两端转换时出现断裂。解决第一JDBC URL 加characterEncodingutf8第二确认建库时指定CHARACTER SET utf8mb4CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci第三检查 MySQL 服务端配置文件 my.ini 或 my.cnf 里的character-set-serverutf8mb4。三条同时满足中文才稳。5.4 PreparedStatement SQL 语句执行报语法错误现象执行带参数查询时报SQLSyntaxErrorException提示你的 SQL 语句靠近某个占位符附近有问题。原因SELECT * FROM book WHERE name LIKE %?%这种把占位符包在字符串里的写法是新手最常见的错误。PreparedStatement 的占位符只识别独立的?一旦它被引号包裹就会被当成普通字符串的一部分传给数据库而数据库并不认识%?%这个字面量语法直接炸。解决SQL 里写WHERE book_name LIKE ?然后ps.setString(1, % keyword %)。占位符留给 setString 去填充内容乱拼接是语法错误的根源。5.5 JTable 刷新后界面不更新现象借书、还书操作执行成功数据库记录也变了但 JTable 里显示的数据还是操作之前的样子。原因你换了新的 TableModel 或者改了 List 数据但没有通知 JTable 重新绘制。Swing 组件不会自动感知数据模型的变化必须显式触发更新事件。解决如果是同一个 TableModel 实例修改数据后调用model.fireTableDataChanged()如果是新建了 TableModel调用table.setModel(newModel)之后不需要再 fire。注意这两者不要重复调用否则有极低概率触发界面闪烁。5.6 界面操作卡死窗口拖不动现象点击查询或导入按钮后整个窗口无响应鼠标转圈Mac 上显示风火轮。原因耗时操作数据库查询、文件解析跑在了事件分发线程 EDT 上。Swing 是单线程模型所有界面事件都在 EDT 里排队执行如果这个线程里执行了慢 SQL界面渲染事件就得排队等表现就是假死。解决耗时操作放进单独线程或 SwingWorker 里执行。用 SwingWorker 的 doInBackground 方法处理数据查询然后在 done 方法里更新界面。记住一个原则EDT 只干活少于 50 毫秒的操作其余扔给后台线程。6. 进阶改造从这套代码延伸到 Spring Boot 与 MyBatis 的迁移思路把这套系统的代码读透之后下一步的玩法是把它从桌面应用改造成 Web 应用。这条进阶路线我拆过很多次核心关注点有二JDBC 改成 MyBatis 和 Swing 改成前端。迁到 Spring Boot 时原代码里的 DBUtil 连接工具类可以整体删掉MyBatis 的 DataSource 配置接管连接管理。原来 BookDao 里的 SQL 和映射逻辑按 MyBatis 的规范改到 XML 映射文件里每个方法对应一个 select/insert/update 标签。core 的改动集中在实体类上加注解或写 resultMapBook 类的属性名和表字段保持一一对应Mapper public interface BookMapper { Select(SELECT * FROM book WHERE id #{id}) Book selectById(Integer id); Update(UPDATE book SET available_count available_count - 1 WHERE id #{id} AND available_count 0) int decreaseAvailable(Integer id); }注解式 SQL 只适合简单语句借用还书这样的多表操作我还是建议写 XML便于查看事务边界。Swing 界面的部分没有迁移价值直接砍掉写一个 Controller 层用 RESTful API 暴露接口。借书操作对应的 POST/api/borrow接口参数是 bookId 和 readerNameService 层用Transactional来保证插入记录和库存扣减的原子性这套资源里 DAO 层手写事务的逻辑可以平移参考。最后做个验证。改造后的系统启动后先用mysql -u root -p登录数据库执行SHOW CREATE TABLE book确认表结构里 available_count 字段存在且类型为 INT。然后用 Postman 或 curl 调一次借书接口返回成功后再执行SELECT available_count FROM book WHERE id1观察库存是否减一。我通常的习惯是每次改完数据访问层都强制走一遍这个流程哪怕只改了字段名也重新查一次确保接口没有因为数据库字段不一致而静默失败。这套 Java 小型图书管理系统在你手里可以有两种身份练手项目或者改造母本。前者就照着源码抄一遍理解每个方法为什么存在后者就把它当脚手架替换掉界面层、升级到 Web 架构底层的实体设计和业务逻辑是能复用的。我自己把数据库连接参数和借阅事务代码抽出来改到别的管理项目里用过踩过的坑在上一章里基本都列全了希望这份拆解帮到你。本文还有配套的精品资源点击获取