简介Java基础学习常面临理论知识与实际项目脱节的困境而数据库操作、事务一致性、SQL注入防护等核心概念正是连接二者的关键。理解JDBC连接MySQL的底层机制掌握PreparedStatement预编译原理并结合Swing构建可视化界面是提升工程能力的有效路径。图书管理系统作为经典练手项目涵盖了登录验证、增删改查、借还书流程、外键约束与索引设计等完整环节业务逻辑清晰且技术栈轻量。通过分层架构entity/dao/service/ui组织代码能够深入理解事务边界和库存一致性维护为后续学习MyBatis、Spring等框架打下坚实基础。本文以IDEA为开发环境从数据库设计到核心功能实现逐步拆解帮助读者避开中文乱码、驱动加载等常见陷阱快速完成一个可演示的课程设计作品。 很多学Java的人都会遇到同一个尴尬语法、集合、面向对象、异常处理都学了一遍IDE也装好了但真要独立做一个东西的时候大脑一片空白。图书管理系统恰好是填这个坑的经典项目我当年做课程设计选的就是它。这个系统麻雀虽小五脏俱全登录、增删改查、借还书、表关系、事务处理全都能练到而且业务逻辑简单不用花太多时间去理解行业规则能把精力集中在“怎么用Java把一件事做成”上。如果你正在准备课程设计、毕业设计或者想找个项目练手来对付面试里的基础题这篇博文应该能帮你少走不少弯路。我在这篇文章里会从整体设计思路、环境准备、数据库设计到具体功能模块的代码实现再到我实际踩过的坑完整走一遍基于IDEA的Java图书管理系统开发流程。项目中用的技术栈是Java SE Swing JDBC MySQL不引入Spring等重型框架就是希望在讲清楚核心逻辑的同时让你能真正把每一行代码都吃透。1. 项目整体设计与思路拆解1.1 图书管理系统到底考的是什么很多同学拿到“图书管理系统”这个题目第一反应是“就这不就是个增删改查吗”。但真做起来就会发现这个项目考察的从来不是某个高深的技术点而是你对基础知识点的串联能力。一个完整的图书管理系统至少要覆盖以下几个方面Java基础语法和面向对象设计类的抽象、封装、继承、多态怎么落地。集合框架的使用内存中存储查询结果、管理数据列表。异常处理数据库操作、用户输入校验都会抛异常怎么处理才合理。JDBC编程连接数据库、执行SQL、处理结果集、释放资源。SQL和数据库设计表结构、主外键关系、索引、常用查询语句。分层思想界面代码和业务代码、数据访问代码分离这决定了项目后期好不好维护。调试能力断点调试、日志输出、定位空指针和SQL异常。换句话说这个项目就是一个“把Java基础全部串起来”的综合演练。我见过不少同学用几百行代码把所有逻辑堆在一个类里也能跑通但一旦要加功能就痛不欲生。这就是设计思路没跟上。1.2 技术栈选型为什么用Swing JDBC而不是Spring Boot我明确知道现在很多教程或者培训都在推Spring Boot Vue前后端分离的图书管理系统看着高大上但是对于刚学完Java基础的同学来说那个学习曲线实在太陡了。Swing/JavaFXSwing虽然老但它作为Java自带的GUI框架不需要额外部署Web容器没有前后端联调的概念代码写起来非常直观。你点一个按钮监听器里直接调用业务方法结果立刻显示在界面上反馈闭环很短。JDBC MySQLJDBC是Java访问数据库最底层的方式把Connection、Statement、ResultSet这些核心对象玩明白以后学MyBatis、JPA的时候你就能理解框架到底帮你封装了什么。不用Spring Boot不是Spring Boot不好而是对于一个以练手和课程设计为目的的项目来说Spring Boot带来了大量“冗余概念”——IOC容器、自动配置、依赖注入、Maven依赖管理……这些概念会给新手造成极大的认知负担。很多同学一上来就套Spring Boot结果出了问题根本分不清是业务代码的问题还是框架配置的问题。如果你已经熟练掌握Java基础且时间紧迫必须交付课程设计那我依然推荐先用纯Java Swing JDBC把系统做出来。这套方案开发和调试成本最低演示效果也足够直观。1.3 分层设计别把代码全堆在一个类里做这个项目时我见过最经典的反面教材就是登录、查询、添加、删除、界面渲染全写在一个类里面一个文件动辄一千多行看着确实“能跑”但后续任何一点改动都提心吊胆。合理的做法是把项目拆成四层entity实体层对应数据库中的表比如Book、User、BorrowRecord类里的字段和表的列一一对应。dao数据访问层专门负责和数据库打交道一个实体对应一个Dao接口和实现类比如BookDao里面定义addBook()、deleteBook()、findAll()等方法。service业务层处理业务逻辑比如借书时要检查库存、判断读者是否存在、生成借阅记录这些规则写在Service里不写在Dao里。ui界面层只负责创建窗口、组件、监听用户点击然后调用Service层方法把结果显示出来。这样分层之后每层各司其职。就算你的系统很小坚持这种习惯也能让你在代码量增长时保持头脑清醒。更重要的是课程设计答辩时老师问你“你这个项目怎么设计的”你能讲出一个清晰的分层架构印象分会好很多。2. 环境准备与数据库设计2.1 开发环境清单与IDEA配置做这个项目之前先把环境检查一遍顺序不能乱。JDK版本我建议使用JDK 8或者JDK 11。虽然现在JDK 17、JDK 21都出了但对于这个项目来说JDK 8最稳定网上遇到问题也最容易搜到解决方案。如果你的机器装了高版本JDK也可以但要注意下面说的编译级别问题。IDEIntelliJ IDEA社区版完全够用不用折腾旗舰版。社区版免费开源支持Java开发、Swing界面设计器、数据库插件对这个项目来说没有功能缺失。数据库MySQL 5.7或8.0都可以。MySQL 8.0在驱动连接串上有点区别后面会讲如果你不想踩时区的坑就用5.7。图形化工具Navicat、DBeaver或者IDEA自带的Database面板都行。我推荐DBeaver免费且功能足够。IDEA配置里有几个关键点我提醒一下。第一新建项目时Project SDK要选对如果你装了多个JDK版本务必在Project Structure里确认一下。第二要确认项目的Language Level和对应的JDK版本一致否则会出现编译错误最典型的就是热词里的“源发行版17需要目标发行版17”这个我后面会单独讲。第三IDEA里统一设置UTF-8编码路径是Settings - Editor - File Encodings把Global Encoding、Project Encoding、Properties Files全部设为UTF-8。还有环境变量。如果在命令行里运行java命令提示找不到那就是环境变量没配好。需要配置JAVA_HOME指向JDK安装目录然后在Path里加上%JAVA_HOME%\bin。配置完之后新开一个命令行窗口输入java -version验证能输出版本号就说明成功了。2.2 数据库表结构设计三张表搞定核心业务图书管理系统的核心是两件事管图书、管借还。所以最基础的表结构只需要三张表用户表、图书表、借阅记录表。我先给出建表SQL这里的设计思路要好好看。-- 用户表 CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码, real_name VARCHAR(50) NOT NULL COMMENT 真实姓名, role TINYINT NOT NULL DEFAULT 0 COMMENT 角色0管理员1普通用户, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表 CREATE TABLE book ( id INT NOT NULL AUTO_INCREMENT, book_name VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) DEFAULT NULL COMMENT 作者, publisher VARCHAR(100) DEFAULT NULL COMMENT 出版社, isbn VARCHAR(30) DEFAULT NULL COMMENT ISBN号, total_count INT NOT NULL DEFAULT 1 COMMENT 总数量, remain_count INT NOT NULL DEFAULT 1 COMMENT 剩余可借数量, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_book_name (book_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表 CREATE TABLE borrow_record ( id INT NOT NULL AUTO_INCREMENT, book_id INT NOT NULL COMMENT 图书ID, user_id INT NOT NULL COMMENT 借书人ID, borrow_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 借书时间, return_time DATETIME DEFAULT NULL COMMENT 还书时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0借出未还1已归还, PRIMARY KEY (id), KEY idx_book_id (book_id), KEY idx_user_id (user_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book (id), CONSTRAINT fk_borrow_user FOREIGN KEY (user_id) REFERENCES user (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个设计细节值得展开说。一是用户表里加了role字段用来区分管理员和普通用户。虽然很多课设版本直接做一个登录就行但加上这个字段你就需要用到“条件判断”和“界面权限”代码里会多一些复杂度也更好向答辩老师展示。二是book表里故意设计了total_count和remain_count两个字段。大家第一反应可能会想剩余数量不是可以通过借阅记录算出来吗理论上可以但实际开发中为了提高查询效率我们通常会把这种高频使用的统计值冗余存储在业务表里。借书时remain_count减1还书时加1再配合事务保证数据一致性。这个设计点也可以作为答辩中的亮点。三是借阅记录表里的status字段。归还时间return_time为空时表示未归还但其实用status字段来判断更高效因为有些业务场景下即使归还了也需要保留return_time作为历史数据。两者配合使用既方便查询未归还记录也能保留完整的借阅历史。2.3 为什么要添加外键和索引很多教材上的示例代码建表时省去了外键和索引这在真实项目里是不可取的。添加外键可以保证数据完整性比如你去删除一本已经被借阅的图书数据库会因为外键约束而拒绝删除从数据层面防止产生孤儿记录。索引的作用更直接图书管理系统中查询频率最高的是按书名模糊搜索加了idx_book_name索引后即使数据量上万条查询速度也不会太慢。不过要注意一点外键约束如果你用Swing界面来操作删除数据时经常会碰到“违反约束”的异常。这其实不是坏事因为你的Service层代码必须去处理这些业务规则比如删除图书前先检查是否有未归还的借阅记录。你在代码里处理好这个逻辑用户体验才会好。3. 实操过程与核心功能实现3.1 IDEA中创建项目与包结构规划环境配置好之后打开IDEA新建一个普通的Java项目不需要勾选Maven虽然Maven管理依赖很方便但这里为了减少学习成本我选择直接把MySQL驱动jar包手动导入。创建项目后在src目录下新建包建议结构如下com.library ├── Main.java // 程序入口 ├── entity // 实体类 │ ├── User.java │ ├── Book.java │ └── BorrowRecord.java ├── dao // 数据访问层 │ ├── UserDao.java │ ├── BookDao.java │ └── BorrowRecordDao.java ├── service // 业务层 │ ├── UserService.java │ ├── BookService.java │ └── BorrowService.java ├── ui // 界面层 │ ├── LoginFrame.java │ ├── MainFrame.java │ └── BookManagePanel.java └── util // 工具类 └── DBUtil.java这个包结构看起来多但是每一个类都很小职责非常明确。实体类就是纯数据Dao类只有SQL操作Service类只写业务规则UI类只管界面和事件监听。以后你想排查一个Bug定位到具体的方法效率比在“上帝类”里翻找快得多。3.2 数据库连接工具类DBUtil的写法JDBC最底层的代码就是获取数据库连接。为了避免每个Dao类里都重复写一遍加载驱动、打开连接的逻辑我们会封装一个DBUtil工具类。package com.library.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这里有几个要点必须说清楚。第一JDBC 4.0之后其实不需要手动Class.forName加载驱动了但是写上并没有坏处而且能让你更直观地理解“驱动”这个概念的来源。第二驱动类名要注意。MySQL 5.x版本用的是com.mysql.jdbc.DriverMySQL 8.x版本要求用com.mysql.cj.jdbc.Driver。如果你导入的是8.x版本的驱动jar包却写5.x的类名运行时会直接报ClassNotFoundException。第三URL里的几个参数很关键。characterEncodingutf8解决中文乱码useSSLfalse解决连接时的SSL警告serverTimezoneAsia/Shanghai解决MySQL 8.x的时区错误。这几个参数属于“不加必出问题”的经典配置。如果你的数据库没有创建library_db数据库需要在Navicat里先执行CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4然后再建表。如果没建库就运行程序会报Unknown database。3.3 登录功能的实现与防SQL注入登录是整个系统的入口。界面不用做得花哨一个窗口两个输入框一个登录按钮就够了。核心逻辑在UserDao和UserService里。public User login(String username, String password) { String sql SELECT * FROM user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setRealName(rs.getString(real_name)); user.setRole(rs.getInt(role)); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; }这里最需要注意的就是使用PreparedStatement的占位符?传参而不是用字符串拼接SQL。我用这个例子经常跟人讲字符串拼接做登录认证如果用户名输入 OR 11那SQL语句就会变成SELECT * FROM user WHERE username OR 11 AND password 任意值条件恒成立登录直接被绕过。这就是教科书级别的SQL注入。PreparedStatement之所以安全是因为它会对参数进行预编译和转义处理从机制上堵住了这个漏洞。登录成功之后需要把当前用户信息保存下来。简单做法是在MainFrame里维护一个User属性或者用一个静态的Session工具类来保存登录用户。这样主界面才能根据当前用户角色决定显示哪些功能按钮比如普通用户不显示“用户管理”模块。界面层的登录按钮监听器基本逻辑是btnLogin.addActionListener(e - { String username txtUsername.getText().trim(); String password new String(txtPassword.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, 用户名和密码不能为空); return; } User user userService.login(username, password); if (user ! null) { new MainFrame(user).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, 用户名或密码错误); } });这里有一个小细节值得提一下密码框用JPasswordField而不是JTextField而且取回密码时用getPassword()返回char数组而不是用getText()返回String。原因是String是不可变对象会在内存中留存char数组用完后可以手动清空。虽然Swing的项目基本不涉及极端安全场景但这是个好习惯。3.4 图书管理模块从JTable数据展示到增删改登录进去之后的核心页面就是图书列表。用Swing的JTable来展示数据是最常规的做法。JTable的数据模型是TableModel需要封装一层。我之前写代码时喜欢直接把ResultSet的数据遍历放进二维数组再放到DefaultTableModel里这种方式写起来最快但有个问题表格里的数据和数据库状态无法联动。所以更规范的做法是先用BookDao的findAll()方法查出List 再把这个集合转换成表格行数据刷新方法单独拆出来。public void refreshBookTable() { ListBook bookList bookService.getAllBooks(); DefaultTableModel model (DefaultTableModel) bookTable.getModel(); model.setRowCount(0); // 清空旧数据 for (Book book : bookList) { model.addRow(new Object[]{ book.getId(), book.getBookName(), book.getAuthor(), book.getPublisher(), book.getIsbn(), book.getTotalCount(), book.getRemainCount() }); } }添加图书和修改图书功能通常通过弹出一个JDialog表单来实现。这里有一个关键的代码设计添加和编辑共用同一个表单通过传入的Book对象是否为null来判断当前是新增还是更新。编辑时字段预填新增时字段清空。if (editBook null) { bookService.addBook(newBook); } else { newBook.setId(editBook.getId()); bookService.updateBook(newBook); } refreshBookTable();删除图书就简单一些先获取表格选中行的图书ID然后弹确认框防止误删。删除后也要刷新表格。我在做这个模块时踩过的坑是JTable获取选中行时如果用table.getSelectedRow()拿到的是视图上的行号如果表格数据被排序或者过滤过这个行号可能和模型行号不一致。解决方案是用table.convertRowIndexToModel(selectedRow)转换一下。对于数据量小的图书管理项目这个坑不常出现但面试时能提出来是加分项。3.5 借书还书事务处理与库存一致性借书还书是这个项目里业务逻辑最密集的地方也是最能体现“你会不会写真正的业务代码”的模块。借书的业务流程是选择一本图书、选择一个借阅人、点击借书。系统要做的操作有三步检查这本书的剩余数量remain_count是否大于0。在borrow_record表中插入一条记录book_id和user_id对应好status为0。更新book表把remain_count减1。这三步操作必须打包成一个整体要么全部成功要么全部失败。比如数据库在插完记录后突然崩溃了库存没减那系统就出现了“数据不一致”——明明没库存了记录里却显示借出去了。解决数据一致性的核心机制就是数据库事务。我用代码来说明public boolean borrowBook(int bookId, int userId) { Connection conn null; try { conn DBUtil.getConnection(); // 关闭自动提交开启事务 conn.setAutoCommit(false); // 1. 查询图书剩余数量 PreparedStatement ps1 conn.prepareStatement(SELECT remain_count FROM book WHERE id ? FOR UPDATE); ps1.setInt(1, bookId); ResultSet rs ps1.executeQuery(); int remainCount 0; if (rs.next()) { remainCount rs.getInt(remain_count); } if (remainCount 0) { conn.rollback(); return false; } // 2. 插入借阅记录 PreparedStatement ps2 conn.prepareStatement(INSERT INTO borrow_record (book_id, user_id, status) VALUES (?, ?, 0)); ps2.setInt(1, bookId); ps2.setInt(2, userId); ps2.executeUpdate(); // 3. 减少库存 PreparedStatement ps3 conn.prepareStatement(UPDATE book SET remain_count remain_count - 1 WHERE id ?); ps3.setInt(1, bookId); ps3.executeUpdate(); // 全部成功提交事务 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(); } } }这里有两个细节要注意。一是我在查询剩余数量时用了FOR UPDATE。这是悲观锁的写法作用是查询这些行的时候对它们加排他锁阻止其他并发事务在本次事务提交前修改同一行。图书管理系统这种低并发场景其实用不上悲观锁但对于学习目的来说理解这个写法能让你以后接触高并发面试题时对“行锁”有一个直观的认识。二是事务方法里的conn不能在Dao层创建然后每个方法都关闭。因为关闭连接会释放事务上下文。所以在BorrowService里你需要自己获取一个Connection然后把所有数据库操作都推进这个连接里执行。这意味着你需要在DAO层增加支持“传入Connection”的重载方法或者在Service层直接写SQL操作。对于课程设计来说我建议直接在Service里写这一段逻辑不必强行套DAO因为一旦牵扯到多表的原子性操作DAO层一拆事务边界反而容易搞乱。等以后学Spring了Transactional注解就是专门解决这个痛点的。还书逻辑是借书的逆过程更新borrow_record表把return_time设为当前时间、status设为1再更新book表把remain_count加1。这里也需要事务包裹否则可能出现“记录归还了库存没加回来”的情况。3.6 主界面的菜单与功能整合系统主界面建议用JFrame JTabbedPane来做顶部是一个工具栏或者菜单栏中间是用选项卡切换的多个功能面板图书管理、借阅管理、用户管理。每个功能面板独立成一个类继承JPanel。这样MainFrame里代码很薄只是负责把各个面板挂上去。public MainFrame(User loginUser) { this.loginUser loginUser; setTitle(图书管理系统 - 当前用户 loginUser.getRealName()); setSize(900, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JTabbedPane tabbedPane new JTabbedPane(); tabbedPane.addTab(图书管理, new BookManagePanel()); tabbedPane.addTab(借阅管理, new BorrowManagePanel()); // 管理员专属功能 if (loginUser.getRole() 0) { tabbedPane.addTab(用户管理, new UserManagePanel()); } add(tabbedPane, BorderLayout.CENTER); setLocationRelativeTo(null); }这个设计思路很简单但有一个很实际的好处如果你后期想增加“图书分类”或者“统计报表”功能只需要新建一个Panel类再addTab一行代码挂上去就行完全不影响现有代码。4. 常见问题与排查技巧实录4.1 中文乱码问题这是最常被问到的。Swing窗口里的中文乱码源头往往是IDEA的编译编码和运行编码不一致。我习惯把IDEA的项目编码、全局编码都设为UTF-8同时菜单栏Help - Edit Custom VM Options里加上-Dfile.encodingUTF-8。如果数据库里的中文显示乱码大概率是建表时没指定utf8mb4或者连接串里没加characterEncodingutf8。还有一个容易忽略的点MySQL的my.ini配置文件里character-set-server也要设为utf8mb4否则即使建表指定了字符集某些系统环境下还是会出现连接层转换问题。4.2 提示ClassNotFoundException: com.mysql.cj.jdbc.Driver这说明MySQL驱动jar包没有正确导入到项目中。IDEA里检查一下Project Structure - Libraries确认驱动jar包是否添加以及是否勾选了“添加到模块”的选项。另外还要注意驱动jar包不能只放在文件夹里必须通过IDEA的Add as Library操作正式导入运行时classpath里才会包含它。4.3 连接数据库报Communications link failure这个报错背后常见有三层原因。第一层是MySQL服务没启动。Windows下可以打开服务管理器找到MySQL服务确认状态是“正在运行”。第二层是连接参数错误最常见的是端口号不对MySQL默认3306如果你装的是多个实例或者改了端口要检查URL里有没有写对。第三层是MySQL 8.x的加密认证方式问题如果用了root账号和旧密码需要在MySQL命令行执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;否则驱动无法通过认证。4.4 编译错误源发行版17需要目标发行版17这个报错基本上是因为IDEA的Project Structure里Project SDK选了JDK 17但Java Compiler的字节码版本写的不是17导致javac编译时发现源文件版本比目标版本高。解决方法是Settings - Build, Execution, Deployment - Compiler - Java Compiler把Project bytecode version改成和Project SDK一致。如果你用的不是最新特性我更建议直接把SDK换成JDK 8或11省掉这些兼容性问题。4.5 运行项目时提示OutOfMemoryError: insufficient memory热词里就出现了这个。IDEA本身比较吃内存如果同时开了多个项目再跑数据库和GUI程序确实可能出现内存不足。在IDEA菜单Help - Change Memory Settings里可以调整堆内存改成1024MB或2048MB。另外也要确认你的电脑本身内存够用特别是一些配置比较低的机器建议关掉不用的浏览器标签页和后台程序。4.6 Lombok相关警告“You arent using a compiler supported by Lombok”如果你用了Lombok的Data注解来简化实体类但IDEA没装Lombok插件或者版本不兼容就会出现这个警告。解决方案很简单Settings - Plugins搜索安装Lombok插件然后重启IDEA。如果还不行看看pom.xml里Lombok版本是不是太高和当前JDK不兼容。实际上对于这个项目的实体类手写getter/setter也就几十行不是特别必要用Lombok省去这个环节还能避免折腾。4.7 已登录用户无法看到未归还记录这个更像是逻辑问题。查询未归还借阅记录时SQL是SELECT ... FROM borrow_record WHERE user_id ? AND status 0。如果你只是简单的左连接查询可能由于status字段没加条件导致把所有历史记录都列出来了。建议单独写一个DAO方法比如getBorrowingRecordsByUserIdSQL条件限定status0代码语义一目了然。4.8 常见问题速查表问题现象可能原因解决方式界面中文乱码IDEA文件编码与运行编码不一致统一设为UTF-8VM options加-Dfile.encodingUTF-8数据库中文乱码表字符集不是utf8mb4建表时指定DEFAULT CHARSETutf8mb4ClassNotFoundException驱动jar未加入项目库Project Structure添加jar并勾选模块Communications link failureMySQL服务未启动/端口错误/认证问题检查服务状态、端口、密码加密方式Access denied for user用户名或密码错误检查DBUtil里的账号密码是否匹配源发行版17需要目标发行版17SDK版本和编译级别不一致统一SDK和Compiler版本点登录按钮没反应监听器未绑定或空指针异常断点调试定位NullPointerException位置借书时库存没扣事务未提交或SQL执行前异常检查事务提交和回滚逻辑JTable表格不刷新刷新后未重新设置模型重新执行setModel或notifyTableChanged5. 扩展建议这套系统后续还能怎么玩如果你把上面这些功能都跑通了那你已经具备了一个Java基础项目的基本功。但课程设计交上去之后不要急着把代码扔一边。我建议按以下几个方向练一练每一步都能让你的项目质感提升一个档次增加图书分类功能book表加category字段界面上加下拉框过滤。这会让你接触更多SQL查询和界面联动的细节。增加借阅历史统计比如统计某个月借阅量、热门图书Top10涉及GROUP BY、ORDER BY、LIMITSQL能力会提升不少。改成Web版把Swing界面层换成Servlet JSPService和Dao层基本上可以直接复用。这个迁移过程会让你理解前后端交互是怎么回事。引入Maven和MyBatis把原来手动导入jar包改为Maven依赖管理把Dao层换成MyBatis你会发现框架帮你解决了很多重复劳动。用JDK新特性重构if我把代码里的匿名内部类改成Lambda表达式把for循环改成Stream这个项目就变成了一个很好的Java新特性练兵场。我个人做过无数次这种图书管理系统第一次做的时候磕磕绊绊光环境就折腾了两天中间被中文乱码、驱动加载、数据库连接各种问题轮番轰炸。但做完之后整个Java基础的知识体系一下子打通了很多概念不再是孤立的知识点而是变成了解决问题时顺手拿起的工具。这也是我建议你手敲代码而不是复制代码的原因。哪怕你照着文章敲一遍遇到问题、解决问题的过程才是真正转化成能力的过程。本文还有配套的精品资源点击获取