ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java图书管理系统完整项目实战:JDBC连接MySQL数据库全流程解析

Java图书管理系统完整项目实战:JDBC连接MySQL数据库全流程解析 很多人第一次接触完整的 Java 项目都是从一个图书管理系统开始的。这玩意儿几乎是 Java 课设、毕业设计、甚至面试手写代码的常客。但说实话网上搜“JAVA图书管理系统完整代码实现 连接MYSQL数据库”能直接白嫖的源码很多能跑起来的却不多能讲清楚每一步为什么这么写的更是少之又少。我这篇不是给你甩一个压缩包而是把一个连接 MySQL 数据库的 Java 图书管理系统源代码从数据库设计到核心模块完整拆开揉碎讲一遍。写完之后你不仅能跑通还能在老师或面试官问你“为什么用 JDBC 不用 MyBatis”“表结构为什么这么设计”的时候说出个一二三四来。这篇内容的定位很明确适合正在做课设的在校生、准备 Java 实习面试的求职者以及想在本地搭一个完整 Java Web 项目练手的人。我会带你把项目骨架、数据库脚本、连接池配置、登录校验、图书 CRUD、借还书流程全部过一遍每一段核心代码都附上实现理由和踩坑记录最后再整理一份我实际调试过程中遇到的经典问题和排查思路。1. 项目概述与整体设计思路1.1 为什么是图书管理系统需求与场景分析图书管理系统几乎是 Java 入门项目里的“Hello World 加强版”。它不像电商系统那样有复杂的商品规格、订单状态机、支付回调但覆盖了一个业务系统最基本的全部要素用户身份管理员/读者、核心业务数据图书、读者、借阅记录、增删改查图书管理、业务流程借书、还书、统计查询热门图书、借阅排行榜。麻雀虽小五脏俱全。从教学角度看图书管理系统能很好地串联起 Java 语言的几个关键知识点面向对象的设计思想Book、Reader、BorrowRecord 这些实体类的抽象。JDBC 数据库操作连接数据库、增删改查、事务管理。MVC 分层思想控制层、业务层、数据访问层分离。集合框架和工具类的使用对结果集的封装处理。异常处理和资源关闭Java 7 的 try-with-resources 语法。从实际应用角度看学校图书馆、小型企业图书角、甚至社区阅览室都存在真实的图书借还管理需求。图书管理系统的业务逻辑非常清晰状态变化可预期尤其是“先查库存、入库时扣减库存、还书时恢复库存并计算逾期费”这套流程几乎可以迁移到所有库存型业务系统上。这也是它作为面试题库常客的原因——面试官可以从这个小项目里看出你是否有模块化思维、是否注意边界条件、是否懂事务和并发控制。1.2 技术选型的背后考量为什么用 Java MySQL首先要明确这是一个传统型的 Java 单体项目不走微服务、不走前后端分离如果你做的是 Swing 客户端版那就是桌面应用如果你做的是 JSP/Servlet 或者 Spring Boot Vue那就是 Web 版。我这次以最经典的“控制台 JDBC”版本为例因为它最能暴露底层逻辑问题。技术栈选定的理由其实很现实Java跨平台大学课程标配面向对象特性明显JDBC 是 Java 官方提供的数据库访问规范掌握它是后续学习 ORM 框架MyBatis、JPA的基础。MySQL开源免费社区活跃安装配置资料极多是绝大多数院校指定的数据库。而且 MySQL InnoDB 引擎支持事务和外键刚好满足图书借还对数据一致性的要求。JDBC 原生操作不用框架才能看清数据库操作的全流程。你理解了 Statement、PreparedStatement、ResultSet 这些底层接口的工作方式再去用 MyBatis 时就会明白它替你封装了什么。这里有个很重要的认知不要觉得不用框架就是落后。面试中经常出现“你的项目中 SQL 注入怎么防”“JDBC 连接怎么管理”“事务边界怎么控制”这类问题如果你连 JDBC 底层的参数占位符绑定都不清楚那就算 Spring Boot MyBatis 写得再顺也答不上来。我见过太多同学简历上写“熟练使用 MyBatis”结果连“#{}和${}的区别”都讲不明白就是因为跳过了 JDBC 阶段。2. 数据库设计与连接层实现2.1 MySQL 表结构设计从需求中提炼数据模型在写任何一行 Java 代码之前首先要把表建出来。很多新手一开始就急着写 DAO 层代码然后发现需要什么字段再回来改表这是最麻烦的错误。正确的打开方式是先把业务对象画出来再映射成表结构。图书管理系统的核心数据对象有三个图书图书编号、书名、作者、出版社、分类、价格、库存总量、当前可借数量、上架时间。读者读者的真实姓名、学号或工号、联系电话、注册时间、状态正常、禁用。借阅记录借书单号、图书 ID、读者 ID、借书日期、应还日期、实际还书日期、状态借出中、已归还、逾期、逾期天数、逾期费。实际操作中我一般还会加一张管理员表与读者表分开因为权限完全不同。完整的建表脚本如下MySQL 8.0 及以上版本测试通过CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; -- 管理员表 CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, book_no VARCHAR(20) NOT NULL UNIQUE COMMENT 图书编号, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(80), category VARCHAR(30), price DECIMAL(8,2), total_count INT NOT NULL DEFAULT 0, available_count INT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 读者表 CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号/工号, name VARCHAR(50) NOT NULL, phone VARCHAR(20), status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, register_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, borrow_no VARCHAR(30) NOT NULL UNIQUE COMMENT 借书单号, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE, status TINYINT DEFAULT 0 COMMENT 0借出中 1已归还, overdue_days INT DEFAULT 0, fine DECIMAL(6,2) DEFAULT 0, FOREIGN KEY (book_id) REFERENCES book(id), FOREIGN KEY (reader_id) REFERENCES reader(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个值得关注的设计细节图书编号book_no独立于主键id存在这是业务编号和物理主键分离的做法。主键是系统内部的唯一标识业务编号是面向用户展示的可能带前缀比如 JS001、WX002两个不要混为一谈。available_count是可借数量初始等于total_count。为什么不每次借书都查询“总库存减去借出中记录数”因为高频查询场景下冗余字段的性能优势非常明显。当然代价是你要维护这个字段的一致性每次借还操作都要同步更新。这是典型的“空间换时间”策略。借阅记录表加了外键约束。在单体小项目中外键能保证数据完整性防止出现“借阅记录指向一本不存在的图书”这种脏数据。当然在大型分布式系统里外键会影响性能和扩展性因此会被放弃但在课设和小型系统中请你老老实实加上外键。2.2 JDBC 连接配置从加载驱动到获取连接Java 连接 MySQL 最核心的就是 JDBCJava Database Connectivity。它的工作流程可以概括为四步加载驱动、获取连接、执行 SQL、释放资源。在 MySQL 8.0 和 8.1 版本中驱动类已经不是之前的老com.mysql.jdbc.Driver而是换成了com.mysql.cj.jdbc.Driver。Jar 包的名称也有讲究8.x 对应mysql-connector-java-8.0.x.jar下载时如果不确定版本用 8.0.33 这一个版本基本能覆盖绝大多数场景。先看一下我推荐的连接管理工具类代码这是项目的地基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?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4allowPublicKeyRetrievaltrue; private static final String USERNAME root; private static final String PASSWORD your_password; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception e) { e.printStackTrace(); } } } } }URL 里那一长串参数每一个都有它存在的理由useSSLfalse本地开发不需要 SSL 加密如果不加这个参数MySQL 8.0 会报一堆 SSL 连接警告烦人但不致命。serverTimezoneAsia/ShanghaiMySQL 8.0 默认使用的时区与 Java 的java.util.Date转换有时差不加这个参数日期字段会差 8 个小时。characterEncodingutf8mb4保证中文正常存取并且支持 emoji 字符。allowPublicKeyRetrievaltrue这个参数非常关键。MySQL 8.0 默认使用 caching_sha2_password 认证插件在非 SSL 连接下客户端需要从服务器获取 RSA 公钥才能完成密码加密传输如果没有这个参数会直接报错。另外要注意驱动加载方式。Class.forName(com.mysql.cj.jdbc.Driver)这行代码的作用是让类加载器加载驱动类驱动类的静态初始化块执行时会自动向DriverManager注册驱动实例。这个动作放在static{}静态代码块中保证类加载时只执行一次这比每次连接时都重复加载要高效。2.3 资源管理与连接池选择的经验教训数据库连接是稀缺资源不能“用完就扔”那么简单。传统的 JDBC 写法要求你在 finally 块里手动关闭 ResultSet、Statement、Connection顺序不能乱。麻烦归麻烦但这保证了资源不泄漏。public ListBook findAllBooks() { ListBook books new ArrayList(); String sql SELECT * FROM book; 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.setBookNo(rs.getString(book_no)); // ... 省略字段赋值 books.add(book); } } catch (SQLException e) { e.printStackTrace(); } return books; }细心的你看出来了我用的是 try-with-resources 语法。这个语法是 Java 7 开始提供的编译后自动生成关闭代码省去了手写 finally 的麻烦也减少了忘记关闭资源的可能性。强烈建议在做课设时就养成这个习惯面试中这是一个明显的加分项。如果你做的是 Web 项目Spring Boot 或传统 Servlet 项目那就要考虑引入连接池。HikariCP 是目前性能最好的连接池也是 Spring Boot 2.x 之后默认的数据库连接池技术。我自己在本地测试时用一个非常简单的配置就能跑通spring: datasource: url: jdbc:mysql://localhost:3306/library_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 30000为什么需要连接池因为每次建立数据库连接都要经过 TCP 三次握手、MySQL 认证、资源分配整个过程可能耗时几十到几百毫秒。连接池的核心思路是预创建一批连接放在池里用的时候取出用完归还避免了频繁创建和销毁的开销。我的理解就是把“每次做饭都要现买食材洗菜切菜”变成“备好常用食材随取随用”。3. 核心功能模块与代码实现3.1 登录模块不只是比对用户名密码图书管理系统肯定有登录入口。但你如果把登录简单理解为“查一下用户名和密码是否匹配”那只能拿 60 分。一个合格的登录模块要考虑几个问题密码能不能明文存连续错误要不要锁定登录成功后的身份信息怎么保留在控制台版本的简化实现中我们把密码用 MD5 加盐处理。这段代码量不大但对安全的意识很重要import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; public class MD5Util { public static String generateSalt() { byte[] bytes new byte[8]; new SecureRandom().nextBytes(bytes); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b)); } return sb.toString(); } public static String md5WithSalt(String password, String salt) throws NoSuchAlgorithmException { String toEncrypt salt password; MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(toEncrypt.getBytes()); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); } }登录校验的核心逻辑放在业务层流程是根据用户名查出用户如果不存在直接返回“用户名或密码错误”不精确提示用户不存在防止攻击者探测账号。取出该用户记录的盐值与输入的密码拼接后计算 MD5。如果计算出的哈希值与数据库中的一致登录成功。登录成功后把用户 ID、角色管理员/读者保存到 Session 或全局上下文对象中供后续权限判断使用。控制台版本我通常维护一个全局变量LoginContext里面记录当前登录者的身份信息。如果是 Web 项目就用HttpSession来保存。public class LoginContext { private static Long currentUserId; private static String currentUsername; private static boolean isAdmin; public static void setLoginInfo(Long userId, String username, boolean admin) { currentUserId userId; currentUsername username; isAdmin admin; } public static boolean isAdmin() { return isAdmin; } public static Long getCurrentUserId() { return currentUserId; } }这里有个我踩过的坑很多新手把密码明文存在数据库里图省事。如果你的项目要被部署到真实环境中哪怕只是校园网环境明文密码就是一个重大安全隐患一旦数据库泄露所有用户的密码全部暴露。加盐 MD5 虽然在今天不算最强SHA-256 加盐或者 BCrypt 更好但在课设项目里已经足以说明你有一份安全意识。3.2 图书管理模块增删改查的 JDBC 标准实现图书管理的核心就是 CRUD增、删、改、查。听起来简单但把代码写得清晰、整洁、易于扩展是区分新手和老手的关键。先看新增图书的实现public boolean addBook(Book book) { String sql INSERT INTO book (book_no, book_name, author, publisher, category, price, total_count, available_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, book.getBookNo()); ps.setString(2, book.getBookName()); ps.setString(3, book.getAuthor()); ps.setString(4, book.getPublisher()); ps.setString(5, book.getCategory()); ps.setBigDecimal(6, book.getPrice()); ps.setInt(7, book.getTotalCount()); ps.setInt(8, book.getAvailableCount()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }为什么用PreparedStatement而不是Statement这是一道经典面试题答案就是防止 SQL 注入。SQL 注入的本质是把用户输入拼接到 SQL 语句中改变了 SQL 原本的语义。比如如果用户名输入的是 OR 11用 Statement 拼接出来的查询就变成了SELECT * FROM admin WHERE username OR 11 AND password xxx这会让未授权用户绕过身份验证。而PreparedStatement使用预编译 参数占位符?把用户输入当纯数据处理而不是 SQL 代码的一部分从根上堵住了这个漏洞。再来看查询这里最容易犯的错误是“一次性查询所有字段不管用不用得到”。在小数据量时感受不明显但在数据库表数据量大时影响非常明显。查询模块我一般建议支持按书名模糊搜索、按分类筛选、按出版社筛选的组合条件查询。组合查询时 SQL 要动态拼条件这是新手容易出错的地方public ListBook searchBooks(String keyword, String category) { StringBuilder sql new StringBuilder(SELECT * FROM book WHERE 11); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append( AND (book_name LIKE ? OR author LIKE ? OR book_no LIKE ?)); String likePattern % keyword.trim() %; params.add(likePattern); params.add(likePattern); params.add(likePattern); } if (category ! null !category.trim().isEmpty()) { sql.append( AND category ?); params.add(category.trim()); } sql.append( ORDER BY id DESC); ListBook books new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { // 封装 Book 对象 Book book new Book(); book.setId(rs.getInt(id)); // 省略字段设置 books.add(book); } } } catch (SQLException e) { e.printStackTrace(); } return books; }WHERE 11这个写法很多人不理解觉得是废话。其实它是为了拼接动态条件的方便后面的所有条件都是以AND开头追加的无需在加第一个条件时特殊处理“是否已经有 WHERE 子句”。虽然看起来有点 hack但在动态 SQL 场景中这个技巧非常实用。当然如果你用 MyBatis 的动态 SQL 标签就不需要这么写了但底层逻辑是相通的。3.3 借书与还书事务处理是核心借书流程看起来简单检查库存 - 插入借阅记录 - 减少库存。但这个流程有个致命的关键点如果在“插入借阅记录”成功之后、“减少库存”执行之前程序突然崩溃了那么数据库里就会出现一条借阅记录但图书库存没有减少的数据不一致情况。小系统可能看不出来但一旦出问题排查成本极高。解决这个问题的标准方案就是事务Transaction。事务保证一组操作要么全部成功要么全部失败回滚。public boolean borrowBook(int bookId, int readerId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查图书是否存在且可借数量大于0 String checkSql SELECT available_count FROM book WHERE id ? FOR UPDATE; int availableCount; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, bookId); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) { return false; } availableCount rs.getInt(available_count); } } if (availableCount 0) { return false; } // 2. 生成借书单号 String borrowNo generateBorrowNo(); Date borrowDate new Date(); Calendar cal Calendar.getInstance(); cal.setTime(borrowDate); cal.add(Calendar.DAY_OF_MONTH, 30); // 默认借期30天 Date dueDate cal.getTime(); // 3. 插入借阅记录 String insertSql INSERT INTO borrow_record (borrow_no, book_id, reader_id, borrow_date, due_date, status) VALUES (?, ?, ?, ?, ?, 0); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setString(1, borrowNo); ps.setInt(2, bookId); ps.setInt(3, readerId); ps.setDate(4, new java.sql.Date(borrowDate.getTime())); ps.setDate(5, new java.sql.Date(dueDate.getTime())); ps.executeUpdate(); } // 4. 扣减库存 String updateSql UPDATE book SET available_count available_count - 1 WHERE id ? AND available_count 0; 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(); } } }这段代码里的几个细节非常值得你研究SELECT ... FOR UPDATE是行级锁意思是锁定这条记录防止并发情况下两个事务同时读到可借数大于 0 然后同时执行借书操作导致超借。不加这个锁并发场景下库存就会超卖这和秒杀系统里的超卖问题本质一模一样。UPDATE book SET available_count available_count - 1 WHERE id ? AND available_count 0这条语句本身就是原子的即使没有前面的行锁它也能保证扣减操作不会把库存扣成负数。这是数据库层面的一种条件更新技巧。事务中一旦出现异常必须在 catch 块中调用rollback()回滚。如果只捕获异常不回滚事务会一直悬着连接池中的连接耗尽就是迟早的事。还书流程是借书流程的镜像。核心逻辑包括根据借阅记录 ID 查询状态必须是“借出中”才能还。更新借阅记录的return_date为当前日期status改为 1已归还。计算逾期天数。如果当前日期晚于due_date逾期天数 当前日期 - 应还日期。计算逾期费用一般按每天 0.1 元计算但注意上限。更新图书的available_count加 1。这里有个容易忽略的细节计算日期差时直接相减会得到 Long 类型的毫秒数需要换算成天数。正确的换算方式是long diffMillis returnDate.getTime() - dueDate.getTime(); int overdueDays (int) (diffMillis / (24 * 60 * 60 * 1000)); if (overdueDays 0) { // 有逾期 } else { overdueDays 0; }但这里还有一个更隐蔽的坑java.util.Date的比较和处理没有时区感知如果你的 JDBC 连接 URL 中serverTimezone参数设置不对就会出现算出来的逾期天数相差 1 天的情况。所以前面强调serverTimezoneAsia/Shanghai不是没有原因的。3.4 读者管理与统计查询系统的完整拼图一个完整的图书管理系统不可能没有读者管理。读者管理的 CRUD 和图书管理高度相似但有两个额外的点值得注意删除读者前要检查该读者是否有未归还的借阅记录。如果有直接删除会导致外键约束失败或者导致历史记录丢失正确做法是禁止删除或者标记为“禁用”。读者状态字段是一个软删除/禁用标记。用status0表示禁用而不是物理删除记录。这样既能保护借阅记录的外键完整性又能保留读者历史数据。统计查询通常是这类项目的加分项。我在项目里实现了几个常见的查询-- 查询借阅排行榜按借阅次数排序 SELECT b.id, b.book_name, COUNT(br.id) AS borrow_count FROM book b LEFT JOIN borrow_record br ON b.id br.book_id GROUP BY b.id, b.book_name ORDER BY borrow_count DESC LIMIT 10; -- 查询当前借出中的所有图书 SELECT br.borrow_no, b.book_name, r.name AS reader_name, br.borrow_date, br.due_date FROM borrow_record br JOIN book b ON br.book_id b.id JOIN reader r ON br.reader_id r.id WHERE br.status 0;多表 JOIN 查询是数据库操作中必须掌握的技能图省事的写法是先在 Java 里查图书再逐条查读者和借阅记录这样会产生 N1 查询问题性能会指数级恶化。正确的思路是让数据库来完成关联一条 SQL 带出所有需要的数据。4. 常见问题与排查技巧实录4.1 JDBC 连接失败从 ClassNotFoundException 到 Communications link failure说句实在话图书管理系统这类课设项目中八成以上的 Bug 都集中在数据库连接环节。我把这些问题按照从早到晚的排查顺序整理成一个速查表报错信息原因分析解决办法ClassNotFoundException: com.mysql.jdbc.Driver用了老版本驱动类名或 Jar 包没有导入项目确认 MySQL 8.x 使用com.mysql.cj.jdbc.Driver检查 Jar 包是否在 WEB-INF/lib 或构建路径中Access denied for user rootlocalhost用户名或密码错误或该用户无远程连接权限核对 MySQL 账号密码确认是否使用rootlocalhost连接而非root%Communications link failure数据库服务没有启动、端口错误、防火墙拦截检查 MySQL 服务是否运行使用netstat -ano | findstr 3306查看端口监听情况Public Key Retrieval is not allowedMySQL 8.0 的 caching_sha2_password 认证机制连接 URL 加上allowPublicKeyRetrievaltrueuseSSLfalseThe server time zone value is unrecognized服务器时区设置不完整连接 URL 加上serverTimezoneAsia/ShanghaiUnknown database library_db数据库没有创建或名字的大小写写错先执行CREATE DATABASE library_db确认库名最容易被忽略的是第 2 条里的用户权限问题。MySQL 8.0 安装后root 用户默认只允许 localhost 连接如果你用 Navicat 等可视化工具连不上去先检查一下 root 用户是否被 Crypto 插件禁止了远程访问解决办法是用命令行登入后执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY your_password; FLUSH PRIVILEGES;这里值得单独聊一聊Communications link failure。这个报错 90% 的情况是 MySQL 服务根本没启动。Windows 下打开服务管理器WinR 输入services.msc找到 MySQL 服务右键启动即可。还有 10% 的情况是你在 URL 中写了localhost但 MySQL 配置中只绑定了 127.0.0.1 或只绑定了 IPv6 地址或者本机 hosts 文件把 localhost 解析到了::1上。遇到这种情况可以试着把 URL 改成jdbc:mysql://127.0.0.1:3306/library_db绕过 hosts 解析。4.2 中文乱码问题从数据库到控制台逐层排查中文乱码是 Java 数据库项目中排名第二的问题而且乱码出现在不同环节修复方式完全不同。拿我遇到的一个真实案例来说把图书名字“三体”插入数据库后查询出来变成了“???”。我当时排查的步骤是这样的第一步检查character_set_server。在 MySQL 命令行执行SHOW VARIABLES LIKE character%如果character_set_server是 latin1那数据库层面的存储就会出现问题需要修改 my.ini 配置文件Windows 在 MySQL 安装目录下Linux 在 /etc/my.cnf中的字符集设置并重启服务。第二步检查表字符集。如果数据库创建时没有指定 utf8mb4默认继承服务器的字符集表也会是 latin1。我建议在建库时显式指定就像第一节的脚本那样写DEFAULT CHARSETutf8mb4。第三步检查 JDBC URL。如果数据库和表都是 utf8mb4 了但 URL 里没有设置characterEncodingutf8mb4Java 和 MySQL 之间的传输也会乱码。这里要注意有些教程写的是characterEncodingutf-8这也能兼容大部分情况但为了严谨我统一用 utf8mb4 更合适。第四步检查 Java 控制台输出编码。如果你的数据在数据库里是正确的中文但从 Java 打印出来乱码问题就出在 IDE 控制台的编码设置。在 IDEA 中右键 Run Configuration在 VM options 里加一行-Dfile.encodingUTF-8在 Eclipse 中则是 Window - Preferences - General - Workspace把 Text file encoding 改为 UTF-8。字符串编码问题的排查原则是“沿数据流逐层检查”源文件编码 - JDBC 传输编码 - 数据库存储编码 - 输出端编码。每一层都设置成 UTF-8 或 utf8mb4问题就自然消失了。4.3 代码层面的实战心得从能跑到写得好我在实际写完整个图书管理系统之后有三次重构每一次都带来质的提升。第一版把所有业务逻辑写在 main 方法里几千行堆在一起自己能看但永远不敢给别人看。第二版拆成了 DAO Service Main 的三层结构逻辑清楚了一点。第三版引入了自定义异常、事务管理和一些工具类这才算是一个“拿得出手”的项目。这里有几个我认为最有价值的优化点非常适合一开始就注意第一DAO 层不要在方法内部自己创建 Connection。正确做法是把 Connection 作为参数传入或者用 ThreadLocal 绑定这样才能保证同一个业务操作复用同一个连接从而支持事务。如果每个 DAO 方法都自己调DBUtil.getConnection()你根本无法控制事务边界。第二不要用SELECT *。在 ResultSet 中手动rs.getString(book_name)看起来也没有多麻烦但当表结构变化时SELECT *返回的字段顺序或数量改变代码就会出现隐蔽的错位问题。建议永远列出明确的字段名。第三接收用户输入时一定要做非空校验。很多课设项目崩溃不是因为数据库或逻辑问题而是用户输入了一个空字符串或超长字符串导致 SQL 执行失败或插入的数据不合法。从系统设计的角度我推荐写一个简单的校验工具类统一校验长度和空值。第四也是最重要的分层设计。我强烈建议把这些代码组织成至少三个包com.library.entity // 实体类Book, Reader, Admin, BorrowRecord com.library.dao // 数据访问层BookDao, ReaderDao, BorrowRecordDao, AdminDao com.library.service // 业务层BookService, BorrowService, StatisticsService com.library.util // 工具类DBUtil, MD5Util, 日期工具 com.library.view // 控制台界面逻辑或 Controller 层哪怕你只用纯控制台输出也请遵守这个分层。一个实体类被 Service 层调用Service 层被 View 层调用每层只负责自己的职责这就是最基本的分层架构思想。5. 项目演进方向与个人经验复盘5.1 从控制台到图形界面Swing 与 Web 的选择如果你准备把这个项目做成一个有模有样的作品集项目控制台版本显然不够看可以考虑两个升级路线路线一Swing 桌面版。写一个LoginFrame、BookManageFrame、BorrowManageFrame用 JTable 展示数据配合 DefaultTableModel 刷新表格。Swing 的学习曲线不高界面控件拖拽也能完成适合快速做课设演示。但注意 Swing 开发中要避免在事件线程中执行耗时 SQL需要用 SwingWorker 来做异步数据加载。路线二Web 版Spring Boot Thymeleaf/Vue。这是当前就业市场上最主流的技术栈。Spring Boot 会自动配置数据源和事务管理你只需要把原来的 DAO 层替换为 MyBatis Mapper 接口把 Service 层加上Service注解再写几个 REST 接口前端用 Vue 或 Thymeleaf 渲染。这条路线最大的价值在于它能展示你掌握主流开发框架的能力。我个人的建议是如果你是面试准备优先做 Web 版如果只是课设Swing 版或控制台版足够拿分。毕竟大多数老师的验收标准是功能完整度 答辩表现而不是技术栈的时髦程度。5.2 性能与安全优化建议一个小的图书管理系统当然不需要考虑分布式和微服务但有三个方向非常值得做第一SQL 语句的性能优化。给常用的查询字段加上索引尤其是book_no、reader_no、borrow_record表中的status和due_date字段。索引不是越多越好因为每次 INSERT 和 UPDATE 都要同步维护索引。对于图书管理系统这个规模给高频查询字段加索引就够了。第二密码算法的升级。前面提到 MD5 加盐只能算基础防线。如果你想让项目加分可以换成BCrypt或SHA-256使用 Java 自带的MessageDigest或引入 jBCrypt 库。在简历上写“对用户密码进行 BCrypt 加密存储”面试官大概率会追问 BCrypt 和 MD5 的区别你要能答出 BCrypt 自动加盐、密钥迭代次数可配置等特性。第三记录操作日志。在图书管理系统中图书的增删改操作、用户的借还操作、管理员的登录操作都应记录日志。日志不仅有助于排查问题也是业务审计的要求。可以用两行代码实现建一张operation_log表Service 层在每个业务动作后 insert 一条记录。虽然在课设中不要求但“有日志意识”在面试中是非常好的加分项。5.3 我做完这个项目后的几点体会把这个项目完整写过一遍之后我的感受是真正难的不是某一个技术点而是把十几个技术点串成一个完整的业务流程。图书借还里的事务管理、库存扣减里的并发控制、组合查询里的动态 SQL以及那无处不在的连接关闭任何一环出问题整个项目都无法稳定运行。先完成再完美。千万不要想着一次性把所有功能、所有细节、所有异常处理都写好那是理想化的流水线做法。实际开发中我建议分三个里程碑先跑通完整的 CRUD再加业务流程借还书最后做统计查询和异常处理。每完成一个里程碑都测试一轮确保当前阶段没有大问题再进入下一阶段。最后再分享一个小技巧如果你要维护 SQL 语句强烈建议打开 MySQL 命令行或者 Navicat先在工具里把 SQL 调试通过再复制到 Java 代码中。如果调试不过报错的因果关系在数据库工具中展示得远比 Java 堆栈信息清晰。我在 Java 代码和数据库客户端之间反复切换调试速度至少提升了一倍。
返回列表