☰
Java图书管理系统实操:JDBC连接MySQL与核心代码详解
2026/10/2 0:11:52 网站建设 项目流程

不搞花架子,直接说项目本身。图书管理系统是Java入门到进阶阶段出现频率最高的练手项目,同时也是很多计算机专业课程设计和毕业设计的首选题目。这个项目题目里有两个关键词:一是完整代码实现,二是连接MYSQL数据库。前者说明需要的是能直接运行、能看逻辑、能复现的整套代码,而不是零散的功能片段;后者说明整个系统的数据落点都在MySQL里,所有增删改查都要经过真实的数据库操作。这篇文章就围绕这两件事展开,把表结构设计、JDBC连接、核心业务逻辑、常见坑位全部过一遍,适合正在做课设、准备Java面试或者想搞清楚数据库连接细节的同学参考。

1. 项目整体设计与需求拆解

1.1 这个系统到底在解决什么问题

图书馆的日常运营非常依赖借还书登记、图书库存盘点、读者借阅记录查询这些操作。传统手写登记的方式效率低、容易出错,管理员想查一本书在哪、谁借走了、什么时候该还,都极其痛苦。图书管理系统做的事情,本质上就是为管理员提供一套数字化工具,让图书信息和借阅记录全部落库,通过程序完成信息的增删改查。

从用户视角来看,最基础的功能需求包括几个方面:

  1. 管理员登录系统,身份校验要可靠。
  2. 图书信息的录入、修改、删除和按条件查询。
  3. 借书操作要能校验库存和借阅状态。
  4. 还书操作要能更新库存和借阅记录。
  5. 借阅记录的查询,方便追踪每一本书的去向。

标题里强调“连接MYSQL数据库”,意味着这些数据不能优雅地躺在内存里——程序一关就全没了,而是要永久的、结构化的存在MySQL里。这样才能保证重启程序后图书数据还在,借书记录也在,系统才有真正的使用价值。

1.2 技术选型:为什么走JDBC直连而不是直接上框架

很多人会问,现在都用Spring Boot + MyBatis,为什么还要用纯JDBC手写连接数据库?我用这个项目回答你:因为跳过了JDBC直接学框架,你根本不知道数据库连接背后发生了什么。JDBC是Java连接数据库的标准接口,MyBatis底层也是封装了JDBC。你把JDBC这套流程——加载驱动、获取连接、创建语句、执行SQL、处理结果集、释放资源——走一遍,框架一上手就能看懂它到底在帮你做什么。

这个项目我建议采用“纯Java控制台 + JDBC + MySQL”的经典组合,而不是上来就套JSP、Servlet或者Spring Boot。原因有三:

  • 控制台交互让注意力集中在SQL语句和业务逻辑上,不需要分心处理页面跳转、表单提交这些Web层的东西。
  • JDBC连接数据库的过程清晰暴露,连接串怎么配、驱动怎么加载、PreparedStatement怎么用,全部看得见摸得着。
  • 代码量适中,适合一个人从零实现,还能在面试时把项目讲清楚。

这套组合做出来的代码结构,后续想迁移到Spring Boot版本时,DAO层和实体类几乎可以原封不动搬过去,改动成本很低。

1.3 数据库表结构设计:三张表撑起整个系统

数据库设计是这个项目的根基。我见过不少同学代码写了一半发现表结构不合理,回头改表又改代码,非常痛苦。设计阶段多花十分钟,后面能省下半天时间。

整个系统用三张表就够:

用户表(users):

  • id,主键自增,用户唯一标识。
  • username,用户名,登录时使用。
  • password,密码,落库时存MD5散列值,不存明文。
  • role,角色标识,管理员和普通读者的权限区分。
  • real_name,真实姓名,用于显示借阅人信息。

图书表(books):

  • id,主键自增,图书ID。
  • book_name,书名。
  • author,作者。
  • publisher,出版社。
  • price,价格,用DECIMAL(10,2)类型,避免浮点误差。
  • stock,库存数量,整数类型。
  • category,分类,方便按类别筛选。

借阅记录表(borrow_records):

  • id,主键自增。
  • book_id,关联图书表主键。
  • user_id,关联用户表主键。
  • borrow_date,借书日期。
  • return_date,应还日期。
  • status,借阅状态:1表示借出中,2表示已归还。

外键关系上,borrow_records表的book_id和user_id分别指向books和users的主键。从表设计上保证数据可追溯。图书库存的扣减在借书时同步更新,还书时同步恢复,这个业务逻辑放在后面讲。

2. 核心代码实现与关键细节

2.1 数据库连接工具类:JDBC连接的全过程

不管写什么功能模块,第一步永远是拿到数据库连接。把所有连接逻辑抽到一个工具类里,是项目初期就该做好的事。来找JDBC连接的经典五步:

  1. 加载驱动类。
  2. 定义数据库连接地址URL。
  3. DriverManager获取Connection对象。
  4. 通过Connection创建Statement或者PreparedStatement。
  5. 释放资源。

工具类代码我直接给出一个项目里实测可用的版本:

import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { // 数据库连接地址 private static final String URL = "jdbc:mysql://localhost:3306/library_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; private static Connection conn = null; // 静态代码块只加载一次驱动 static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } // 获取连接 public static Connection getConnection() throws SQLException { if (conn == null || conn.isClosed()) { conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); } return conn; } // 释放资源:关闭ResultSet、Statement、Connection public static void close(ResultSet rs, Statement stmt, Connection conn) { try { if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }

这里有几个细节值得说透:

驱动类名。MySQL 5.x之前用的是com.mysql.jdbc.Driver,MySQL 8.x后驱动类改成了com.mysql.cj.jdbc.Driver。用错会直接报ClassNotFoundException,这是高频报错点。

连接串参数。serverTimezone=Asia/Shanghai解决的是时区问题,MySQL 8及以上版本服务器跟本机时区不一致会报错。characterEncoding=utf8解决中文乱码。useSSL=false是关闭SSL校验,本地开发不需要加密通道,不关可能会报SSL连接错误。

静态代码块 vs 每次获取连接时加载驱动。Class.forName加载驱动只需要做一次,放进static代码块里是标准写法,不用每次getConnection都重复加载。驱动注册到DriverManager之后就常驻内存了。

2.2 用户登录模块:密码校验与SQL注入防护

登录功能几乎每个系统都有,但我要在这单独拉出来讲,是因为登录模块最容易暴露安全意识问题。我见过不少项目源码里用字符串拼接方式执行SQL,比如:

String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

这种写法非常危险。用户在用户名输入框输入admin' OR '1'='1,SQL语句会变成:

SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = ''

OR条件让整条WHERE语句恒为真,直接绕过密码校验登录成功。这就是最经典的SQL注入攻击,几乎所有数据库安全类面试题都会问它。

正确的做法是用PreparedStatement预编译:

public User login(String username, String password) { String sql = "SELECT id, username, password, role, real_name FROM users WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, md5(password)); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setRole(rs.getString("role")); user.setRealName(rs.getString("real_name")); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; }

注意两个细节。第一,PreparedStatement的参数用?占位,setString传值时由MySQL驱动做参数转义,用户输入的单引号会被当作普通字符处理,注入语句就失效了。第二,密码存的是MD5散列值,md5方法如下:

private static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(input.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException("MD5加密出错", e); } }

密码散列化之后的好处是,即使数据库文件泄露,别人看到的也是一串固定长度的32位十六进制字符,拿不到明文密码。这让登录模块多了一层安全保障。

2.3 图书管理核心业务:借书流程和事务边界

图书管理系统的核心业务流是借书和还书。借书不是简单插入一条记录就完事,它涉及多张表的数据联动。以借书为例,完整流程是:

  1. 校验用户存在且角色为读者。
  2. 校验图书存在且库存大于0。
  3. 扣减图书表的库存。
  4. 插入一条借阅记录。

这四个步骤里,任何一步失败,整个操作都必须回滚。比如库存扣减成功了但借阅记录插入失败,就会出现库存跟实际不相符的情况——书没借出去,库存没了。这是典型的事务场景。

public synchronized boolean borrowBook(int bookId, int userId) { Connection conn = null; PreparedStatement psUpdateStock = null; PreparedStatement psInsertRecord = null; try { conn = DBUtil.getConnection(); // 关闭自动提交,开启事务 conn.setAutoCommit(false); // 查询库存并锁定记录 String checkSql = "SELECT stock FROM books WHERE id = ? FOR UPDATE"; try (PreparedStatement psCheck = conn.prepareStatement(checkSql)) { psCheck.setInt(1, bookId); try (ResultSet rs = psCheck.executeQuery()) { if (rs.next()) { int stock = rs.getInt("stock"); if (stock <= 0) { throw new RuntimeException("库存不足,借阅失败"); } } else { throw new RuntimeException("图书不存在"); } } } // 扣减库存 String updateSql = "UPDATE books SET stock = stock - 1 WHERE id = ?"; psUpdateStock = conn.prepareStatement(updateSql); psUpdateStock.setInt(1, bookId); psUpdateStock.executeUpdate(); // 插入借阅记录 String insertSql = "INSERT INTO borrow_records(book_id, user_id, borrow_date, return_date, status) VALUES(?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), 1)"; psInsertRecord = conn.prepareStatement(insertSql); psInsertRecord.setInt(1, bookId); psInsertRecord.setInt(2, userId); psInsertRecord.executeUpdate(); // 提交事务 conn.commit(); return true; } catch (Exception 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 ex) { ex.printStackTrace(); } } }

这段代码有几个非常关键的细节。conn.setAutoCommit(false)关闭自动提交模式后,后续的SQL不会立刻生效,要等commit才真正写入数据库。rollback是回滚,让之前执行的SQL全部作废。关于行锁SELECT ... FOR UPDATE,它把查询到的图书记录锁住,避免多线程并发借书时大家都读到stock=1,然后同时扣减变成负数。

我自己实际测试过,去掉FOR UPDATE的情况下,用两个线程同时借同一本书且库存只剩1本,两个请求都可能成功,最终库存变成-1。加了行锁之后,第二个请求会阻塞等待第一个事务提交,提交后重新读取库存发现已经是0,直接抛异常,这才是正确行为。

2.4 还书流程:别忽略状态校验

还书逻辑比借书简单,但有一个很容易踩的坑:不校验借阅记录的状态就直接更新。

public boolean returnBook(int recordId) { String sql = "UPDATE borrow_records SET status = 2 WHERE id = ? AND status = 1"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, recordId); int rows = ps.executeUpdate(); if (rows > 0) { // 恢复库存 String updateStockSql = "UPDATE books b JOIN borrow_records r ON b.id = r.book_id SET b.stock = b.stock + 1 WHERE r.id = ?"; try (PreparedStatement ps2 = conn.prepareStatement(updateStockSql)) { ps2.setInt(1, recordId); ps2.executeUpdate(); } return true; } } catch (SQLException e) { e.printStackTrace(); } return false; }

SQL里带AND status = 1,确保只有借出中的记录才能执行归还。如果记录已经是已归还状态,UPDATE影响行数为0,不会出现重复恢复库存的情况。

3. 完整实操:从建库建表到跑通项目

3.1 环境准备:MySQL 8 + IDEA + 驱动

动手之前先把环境准备好。我以本机开发为例,说明每一步需要做什么。

第一步,安装MySQL 8。Windows环境下直接去MySQL官网下载MySQL Installer,只安装Server和Command Line Client。安装过程中的用户名密码记好,默认root用户需要设置一个自己的密码。安装完成后在命令行用mysql -u root -p验证能登录。

第二步,安装Navicat或者直接用MySQL自带的Workbench。Navicat胜在操作直观,建库建表看数据都方便,有条件的话可以用它快速核对数据是否写入成功。

第三步,IDEA创建Java项目,引入连接驱动。两种方式任选:

  • 下载mysql-connector-java的jar包,放在项目lib目录下,右键选择Add as Library。这个方式不需要额外的构建工具。
  • Maven项目在pom.xml里添加依赖:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

我用的是MySQL 8.0系列,所以驱动版本选8.0.33,跟服务器兼容很稳。如果你的MySQL是5.7及以下版本,驱动选5.1.x系列更省事。

3.2 建库建表与初始化数据

打开Navicat,新建数据库library_db,字符集选utf8mb4。用以下几个建表语句直接执行:

CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(32) NOT NULL, role VARCHAR(10) NOT NULL DEFAULT 'user', real_name VARCHAR(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE books ( id INT AUTO_INCREMENT PRIMARY KEY, book_name VARCHAR(100) NOT NULL, author VARCHAR(50) DEFAULT NULL, publisher VARCHAR(100) DEFAULT NULL, price DECIMAL(10,2) DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, category VARCHAR(50) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE borrow_records ( id INT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL, user_id INT NOT NULL, borrow_date DATETIME NOT NULL, return_date DATETIME DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1, CONSTRAINT fk_book FOREIGN KEY (book_id) REFERENCES books(id), CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

初始化数据也一并写入,方便程序启动后有东西可操作:

INSERT INTO users(id, username, password, role, real_name) VALUES (NULL, 'admin', MD5('123456'), 'admin', '系统管理员'), (NULL, 'zhangsan', MD5('abc123'), 'user', '张三'); INSERT INTO books(book_name, author, publisher, price, stock, category) VALUES ('Java核心技术卷I', '凯·S·霍斯特曼', '机械工业出版社', 119.00, 5, '计算机'), ('深入理解Java虚拟机', '周志明', '机械工业出版社', 129.00, 3, '计算机'), ('MySQL必知必会', 'Ben Forta', '人民邮电出版社', 59.00, 8, '数据库'), ('活着', '余华', '作家出版社', 35.00, 10, '文学');

有一点需要提前说清楚:MD5('123456')这个函数在MySQL 8中默认是可用的,生成的密文存进去之后,Java代码里用md5方法对用户输入的密码加密再比对,两边算法一致才能匹配。

我自己踩过初始化数据里直接写明文密码的坑。初期偷懒把password字段存成'123456',登录验证时也拿明文去比较,数据库里全是裸奔的用户密码。后来项目做完统一改用MD5,才把这个问题处理干净。建议从项目第一版开始就养成不落明文密码的习惯。

3.3 项目结构搭建:分包划分与实体类设计

代码不是堆在一个类里就能叫完整项目。合理的包结构让代码后期可维护,也更容易讲明白系统架构。我建议用以下分包方式:

com.library ├── entity/ # 实体类 │ ├── User.java │ ├── Book.java │ └── BorrowRecord.java ├── util/ # 工具类 │ └── DBUtil.java ├── dao/ # 数据访问层 │ ├── UserDAO.java │ ├── BookDAO.java │ └── BorrowDAO.java ├── service/ # 业务逻辑层 │ ├── UserService.java │ ├── BookService.java │ └── BorrowService.java └── ui/ # 界面展示与交互 └── MainMenu.java

分层的好处是,界面层只负责接收输入和展示结果,业务层处理业务规则,DAO层只能跟数据库打交道。举个实际场景:界面层需要显示“借书成功”,但它不应该直接写SQL,而是调用service层的方法,service内部组织多个DAO方法协同完成借书流程。这样改动界面不影响数据库代码,替换数据库连接方式也不影响界面。

实体类以Book为例:

public class Book { private int id; private String bookName; private String author; private String publisher; private BigDecimal price; private int stock; private String category; // 省略getter和setter方法 }

价格字段用BigDecimal而不是double,double在数据库和Java之间来回转换的时候,浮点误差会让人抓狂。数据库DECIMAL对应Java的BigDecimal,这是标准对应关系。

3.4 图书列表查询:分页与排序实操

图书列表的展示不只是一条SELECT那么简单。系统里的图书数量一旦上到几十本,控制台一屏打不完,前端展示的体验就很差。分页是必须有的能力。

public List<Book> queryBooksByPage(int pageNum, int pageSize, String keyword) { List<Book> bookList = new ArrayList<>(); int offset = (pageNum - 1) * pageSize; String sql = "SELECT id, book_name, author, publisher, price, stock, category FROM books"; // 关键字搜索增强体验 if (keyword != null && !keyword.isEmpty()) { sql += " WHERE book_name LIKE ?"; } sql += " ORDER BY id LIMIT ? OFFSET ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { int paramIndex = 1; if (keyword != null && !keyword.isEmpty()) { ps.setString(paramIndex++, "%" + keyword + "%"); } ps.setInt(paramIndex++, pageSize); ps.setInt(paramIndex, offset); try (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.setPrice(rs.getBigDecimal("price")); book.setStock(rs.getInt("stock")); book.setCategory(rs.getString("category")); bookList.add(book); } } } catch (SQLException e) { e.printStackTrace(); } return bookList; }

ORDER BY id LIMIT ? OFFSET ?是MySQL分页的标准姿势。offset的算法是(pageNum - 1) * pageSize,第一页偏移0条,第二页偏移pageSize条,很容易理解。LIKE关键字搜索时,预编译的?参数值直接拼上%即可,搜书名、作者、分类都通用。

关于LIKE查询有个性能细节:如果数据量大,LIKE '%关键字%'这种前置模糊匹配是走不了索引的,全表扫描。但图书管理系统这种量级的数据完全够用,不需要过度优化。如果想优化,可以引入Elasticsearch或者MySQL全文索引,那是后话。

排序方面同样用ORDER BY实现,热门借阅排名可以这样查:

String sql = "SELECT b.id, b.book_name, COUNT(r.id) AS borrow_count FROM books b " + "LEFT JOIN borrow_records r ON b.id = r.book_id " + "GROUP BY b.id, b.book_name ORDER BY borrow_count DESC";

LEFT JOIN保证一本都没被借过的图书也能出现在列表里,借书次数为0。COUNT(r.id)统计每本书的借阅记录条数,按降序排列得到热门图书排行。这个SQL在面试里也经常被问到,JOIN和GROUP BY的组合使用。

4. 常见问题与排查技巧实录

4.1 数据库连接失败:驱动、时区、权限三类大坑

连接数据库是整套系统最容易出问题的地方,而问题基本集中在三个方向:

第一类是ClassNotFoundException,报错内容是ClassNotFoundException: com.mysql.jdbc.Driver。这个100%是驱动类名或者驱动jar包的问题。MySQL 8.x的驱动类名是com.mysql.cj.jdbc.Driver,你还在用com.mysql.jdbc.Driver就会报这个错。另外确认引用的jar包版本,高版本驱动确认类名写对了没。

第二类是时区错误,报错一般是The server time zone value '�й���׼ʱ��' is unrecognized。原因是MySQL 8服务器默认使用系统时区,而JDBC连接串没有指定时区,两端对不上。解决方式是在URL追加serverTimezone=Asia/Shanghai。

第三类是Public Key Retrieval报错,MySQL 8的默认认证方式是caching_sha2_password,客户端连接时需要从服务端获取公钥做加密,JDBC默认行为不允许这个操作。URL里加useSSL=false&allowPublicKeyRetrieval=true即可解决。

我自己在项目联调阶段把这三类错误轮着遇了一遍,总结成一条经验:连接串务必一次性写完整,不要遇到一个错改一处,太折腾。

4.2 中文乱码:三层设置缺一不可

中文乱码问题出现得相当频繁,而且一乱就是一大片。排查要按三层来检查:

第一层数据库。建库和建表时指定CHARSET=utf8mb4,就是告诉MySQL这个数据库要存unicode字符,中文完全没问题。忘记指定的,可以用ALTER TABLE命令补:

ALTER TABLE books CONVERT TO CHARACTER SET utf8mb4;

第二层JDBC连接串。URL必须带characterEncoding=utf8,注意这里跟数据库里character_set_database的配置匹配起来。

第三层控制台输出。Windows下IDEA的控制台默认编码可能不是UTF-8,在IDEA的Settings里把Global Encoding、Project Encoding、Console的编码都设为UTF-8。命令行运行时,在编译和运行命令加上-Dfile.encoding=UTF-8参数。

这三个地方只要有一个不对,柜台就会显示一排烦人的问号。连续探查这个问题的经验教训是:先确认数据库存进去的中文是不是正常的,用Navicat直接看表。库里是乱的,就是数据库建表和连接串的问题;库是正常的但程序显示乱,就是控制台和JVM编码的问题。

4.3 业务逻辑Bug排查:事务没提交、库存负数、并发冲突

数据库连接层打通之后,业务层的坑就轮到在运行时慢慢暴露了。我整理一下最常遇到的几个:

不执行commit就断言数据写进去了。忘了setAutoCommit(false)之后一定要手动commit,或者写代码的时候根本没意识到事务已经开启,数据在另一个连接里死活看不到更新。排查方式:确认是否有事务开启,查到commit或rollback了吗。

笔记本还剩0本,借书还能成功。这是库存校验缺失导致的。解决方式在前面borrowBook方法里讲过,校验和扣减必须在同一事务里用行锁保护,绝不能在SELECT和UPDATE之间留出空档。

对了,损坏数据还可能来自重复借同一本书。如果借阅表里已经有一条status=1的同图书同用户记录,新借书请求不应该再成功。在插入新借阅记录前查询该用户该图书是否存在借出中的记录即可。

4.4 资源泄漏:连接不关,系统迟早卡到死

这个问题虽然常见,但在新手代码里几乎是批量出现。写完Connection不去close,获取了ResultSet不去close,程序跑不了几个来回就报Too many connections。MySQL默认连接上限是100多个,连接不释放就会把连接池占满。

我建议把所有资源关闭统一放到finally代码块里,或者直接使用JDK 7引入的try-with-resources语法。

try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ... } catch (SQLException e) { e.printStackTrace(); }

用了try-with-resources之后,Connection、Statement、ResultSet只要实现了AutoCloseable接口,就会在try块结束后自动关闭,代码少而且不容易遗漏。如果用的是我前面写的DBUtil,close方法也要在finally里确保调用。

真实项目中,连接不关闭属于那种“今天不出事、明天不出事、上线第三天就出事”的问题。你测试的时候数据量小,加上连接偶尔被GC回收,感受不到严重性。等系统真的被多个人同时用起来,连接数飙升,服务直接就瘫了。

5. 项目扩展方向与面试讲解建议

这个项目做完能跑通,只是第一步。真正让项目有价值的是你能够扩展它,并且在面试时把它讲成亮点。我给四个扩展方向的思路。

第一个方向是加上图形界面。控制台版本的逻辑代码是完整的,只是交互方式比较简陋。可以用Java Swing或JavaFX做界面,把MainMenu替换成登录窗口和主窗口,BookDAO、UserDAO这些类不需要大改。这样就变成了一个更像“系统”的东西。

第二个方向是升级Web版本。把项目改造成Spring Boot + MyBatis + Vue的前后端分离版本,数据库表结构几乎原封不动,DAO层代码翻译成MyBatis的Mapper接口。这个升级过程你自然就理解了框架到底帮你做了什么。

第三个方向是补充还书逾期和罚款逻辑。在BorrowRecord里加一个shouldReturnDate的字段,还书时用当前日期跟应还日期比较,超期天数乘以每日罚款金额,算出一个逾期费用记录,这样业务面就比普通的课设项目完整很多。

第四个方向是权限体系的强化。读者只能看到自己的借阅记录,管理员可以看全部,借书也有容量限制,这些都用role字段和SQL条件就能实现。

给面试讲项目的时候,不要停留在“我写了一个图书增删改查的系统”这种层面。重点讲三件事:数据库表怎么设计的,表间关系是什么;JDBC连接MySQL踩过哪些坑,怎么定位和解决的;借书的库存扣减和借阅记录的写入为什么放在一个事务里。把这些问题想透了,比堆砌功能列表管用得多。

我个人在带项目时最常跟新手说的一句话是:能跑通,只是代码的及格线;知道自己为什么能跑通,才算是学会。这个项目做完,你至少应该能回答:为什么用PreparedStatement而不是Statement,为什么借书要用事务,为什么库存要加行锁,为什么密码要加密存储。能在纸上画出三张表的关系,能说出连接MySQL的URL各个参数的含义。这些才是这个项目真正的收获。

最后再分享一个我在调试阶段的小习惯:把SQL语句先在Navicat里跑一遍,确认结果集返回正确了,再回到Java代码里联调。数据库查询这层走通了,代码层的排查范围瞬间缩小,问题定位效率非常高。这个习惯我用在所有的数据库项目里,实测下来很稳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询