简介:基于JAVA和MySQL的图书馆管理信息系统PDF文档,面向软件开发学习者、数据库设计初学者以及需要完成课程设计的高校学生,旨在帮助读者掌握图书借阅、归还、查询及读者信息管理系统的建设方法。文档系统梳理了从需求分析、ER图与关系模型转换,到数据库物理设计中的索引、视图、安全机制,再到Java Swing前端、JDBC数据库连接及测试部署的完整流程,并以课程设计报告形式组织,章节层次分明,便于对照查阅。包内仅1个PDF文件,整体644KB,内容紧凑,便于阅读和打印。目前已有99人学习,适合作为参考模板或实战蓝本。读者可从中获得图书、读者、借阅登记等核心表结构设计思路,以及系统模块划分、权限控制与异常处理等实现要点,有助于快速理解基于Java与MySQL的桌面信息系统开发方法。
1. 图书馆管理信息系统(基于JAVA和MySQL):先把最小闭环跑通再说
“图书馆管理信息系统(基于JAVA和MySQL)”这个标题,在课程设计和毕设里流传很广。但照着论文里的结构去复现时,很多人卡住的不是SQL写不出来,而是借书还书这件事落地时到处是边界:书刚被借走还显示在架、逾期天数算错一天、两个人同时借同一本书。
这类系统的核心,是把图书与读者维护、借书、还书、逾期罚款、统计查询这个最小闭环跑完整。数据落在MySQL,Java通过JDBC访问,业务规则在Java层实现。技术栈不新,却是理解“Java应用如何配合MySQL”的最小完整样本,能在一台电脑上跑通并讲清每个环节。
适合正在做课设或毕设的同学,也适合想补Java基础与MySQL配合细节的初级开发。照着把表、连接层和借还书事务搭起来,你对数据库应用系统的体感会扎实很多。
2. 先拆模块与数据表:图书馆管理系统的状态流转与建库SQL
动手写代码之前,先把模块和表结构定下来。这类课设翻车,大多翻在没想清楚状态流转,导致Java代码后来写得像打补丁。表结构定好了,业务逻辑其实很固定。
2.1 功能模块划分:借书、还书、逾期罚款的最小闭环
最简系统至少包含四块后台功能:图书管理、读者管理、借阅管理、统计查询,外加一个管理员登录。每块都不是孤立的:图书管理要维护馆藏总数与在架状态;读者管理要维护借书证状态;借阅管理是核心,一次借书会同时修改图书、读者和借阅记录三处数据;统计查询则依赖前面积累的数据。
我一般先把状态流转画出来,再写建表语句。图书有“在馆 / 借出 / 下架”三种状态;读者有“正常 / 挂失 / 注销”;借阅记录有“借出 / 已还 / 逾期未还”。这三个状态组之间互相约束,比如读者挂失时不能借书,图书下架时不能借出。
另一个课设里很常见的错误,是只建一张“借阅记录表”,然后用“最新一条记录是否存在”去推断图书是否在架。这个思路在没有并发时能用,一旦出现归还、续借、预约,查询就会变得极其别扭。正确做法是把“当前是否可借”冗余到图书表里,借出时扣减,还书时加回,借阅记录表只负责流水账。
罚款字段我建议直接放在借阅记录表里,而不是单独拆一张罚款表。逾期未还、还书时结算,都是一条记录上的事,报表统计也方便。只有当你要按年度汇总财务、跟其他费用一起管理时,再去拆独立罚款表。
2.2 建库建表:图书、读者、借阅、管理员四张核心表的CREATE语句
CREATE DATABASE IF NOT EXISTS library_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE library_system; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL COMMENT '密文,不要存明文', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='管理员表'; CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL COMMENT 'ISBN用字符型,别用INT', title VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), category VARCHAR(50), total_count INT NOT NULL DEFAULT 1 COMMENT '馆藏总数', available_count INT NOT NULL DEFAULT 1 COMMENT '当前可借数量', status TINYINT NOT NULL DEFAULT 0 COMMENT '0在馆 1借出 2下架', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_title (title), KEY idx_category (category) ) ENGINE=InnoDB COMMENT='图书表'; CREATE TABLE readers ( id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE COMMENT '借书证号', name VARCHAR(50) NOT NULL, phone VARCHAR(20), status TINYINT NOT NULL DEFAULT 0 COMMENT '0正常 1挂失 2注销', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='读者表'; CREATE TABLE borrows ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL COMMENT '应还日期 = 借出日期 + 借期', return_date DATE NULL, fine DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '逾期罚款', status TINYINT NOT NULL DEFAULT 0 COMMENT '0借出 1已还 2逾期未还', CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES books(id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES readers(id), KEY idx_status (status), KEY idx_due_date (due_date) ) ENGINE=InnoDB COMMENT='借阅表';这段建库建表有几个地方值得说明。字符集用utf8mb4而不是utf8,因为utf8在MySQL里实际是utf8mb3,存生僻字和部分符号会报错或乱码,utf8mb4才是完整的Unicode字符集。
ISBN字段必须用VARCHAR,不能用INT。ISBN有13位数字,还有可能带连字符,超出INT范围会直接报错,即使存得下,前导零也会丢失。图书总量和可借数量都定义成INT,罚款金额用DECIMAL(10,2),不能用FLOAT或DOUBLE,浮点数在金额累计时会累积误差。
索引的选择上,books.title和books.category用于模糊查询和分类筛选,borrows.status和borrows.due_date用于统计逾期和排序。外键在课设阶段建议保留,能让数据完整性多一层保障。但要注意,生产环境里很多团队会去掉外键,改用应用层保证,这是后话。
2.3 设计取舍:外键、冗余字段与索引选型的三个决定
外键在课设和面试里是加分项,但在高并发业务里经常被故意去掉。原因有两点:一是外键会让每次写操作都去检查关联表,锁的范围变大;二是分库分表后,外键在物理上根本不成立。这个系统只有几万条数据,保留外键完全没问题。
available_count是个典型的冗余字段。它让“查一本书能不能借”变成一次主键查询,不用去borrows表里统计未归还记录。代价是每次借书、还书都得多维护这个字段,而且并发下要小心扣成负数,这个坑第5章会专门讲。
关于排序和搜索,统计报表里的ORDER BY尽量走due_date、created_at这类有索引的字段,别在Java代码里把全表捞出来再用内存排序。模糊查询里LIKE '%关键字%'用不上普通B+树索引,数据量小无所谓,量大了要换全文索引或者搜索引擎,这是后话,但不该在课设阶段养成全表扫描的习惯。
3. JDBC连接与数据访问层:让Java程序真正连上MySQL
表建好之后,第一个要解决的是连接问题。“mysql安装教程”和“java环境变量配置”是这条路上最先出现的两个热搜词,但真正卡人的往往是驱动版本和URL参数。
3.1 驱动选择与JDBC URL参数:时区、SSL、字符集怎么配
// MySQL 5.x 时代的驱动入口是 com.mysql.jdbc.Driver // MySQL 8.x 必须换成 com.mysql.cj.jdbc.Driver Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/library_system" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&allowPublicKeyRetrieval=true"; Connection conn = DriverManager.getConnection(url, "root", "你的密码");Class.forName在JDBC 4.0之后其实可以省略,因为驱动jar包里的META-INF/services会自动注册,但保留它能让新手看清楚驱动加载的过程,也不影响运行。
URL里的参数才是真正的坑。很多人照着老教程写jdbc:mysql://localhost:3306/library_system,在MySQL 8上直接报SSL connection error或者时区错误。这里的useSSL=false表示本地开发不做SSL握手,自签名证书在客户端不受信任时不会卡住连接;serverTimezone=Asia/Shanghai解决MySQL 8把时区默认成UTC导致的时间差8小时问题;characterEncoding=utf8配合建库时的utf8mb4才能保证中文不乱码。
| 参数名 | 取值 | 作用 | 不配会怎样 |
|---|---|---|---|
| useUnicode | true | 启用Unicode字符集转换 | 中文乱码 |
| characterEncoding | utf8 | 指定客户端编码 | 中文乱码 |
| useSSL | false | 关闭SSL握手 | 报 SSL connection error |
| serverTimezone | Asia/Shanghai | 指定服务器时区 | 时间差8小时 |
| allowPublicKeyRetrieval | true | 允许客户端获取公钥 | 报 Public Key Retrieval is not allowed |
最后那个allowPublicKeyRetrieval=true是MySQL 8的caching_sha2_password认证插件引入的。旧版驱动连接8.0会直接失败,很多人卡在这一步很久,实际上换一个8.x版本的驱动jar就能解决。下载驱动时要注意,jar包版本和MySQL服务器版本最好对齐,5.7的库配8.x驱动能跑通,反过来大概率翻车。
3.2 封装一个DBUtil:连接、关闭、查询与更新的通用方法
import java.sql.*; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/library_system" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai" + "&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 关闭顺序:ResultSet -> Statement -> Connection,顺序反了会浪费连接 if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }这个DBUtil把每一次获取连接和关闭资源的样板代码收拢到一个类里。关闭顺序必须是从里到外:先ResultSet,再Statement,最后Connection。顺序反了虽然不一定立刻报错,但长时间运行会积累大量未释放的资源,尤其是连接池场景下,连接泄漏是服务端“越跑越慢”的一个隐蔽原因。
如果项目里没有引入任何依赖管理工具,驱动jar要手动放到项目的lib目录并加入构建路径。不要图省事把jar路径写进系统的CLASSPATH环境变量,换一台机器、换一个IDE就会找不到类。用IDE的项目依赖或者Maven管理mysql-connector-j才是正路。
3.3 连接池与事务边界:什么时候用,什么时候别硬上
这个桌面端课设系统,单机单用户,直接用DriverManager完全够了,不必上连接池。但如果你把系统做成了Java Web应用,或者用Spring Boot包了一层HTTP接口,那每次请求都用DriverManager创建连接就非常浪费。MySQL建立连接要经过TCP握手、认证、权限校验,耗时通常在几十毫秒到上百毫秒,高并发下会成为瓶颈。常见做法是换HikariCP连接池,它能复用连接,并且帮你处理连接失效后的重连。
连接池的配置核心是三个参数:maximumPoolSize控制池子里最多放多少连接,一般设为CPU核心数 * 2 + 1;minimumIdle控制空闲时保留多少连接;connectionTimeout控制获取连接的最大等待时间。课设或者内部小系统,这几个参数用默认值就行,不用反复调整。
事务边界这块是重头戏。借书要同时改图书和插入借阅记录,必须保证原子性。很多人把DBUtil.getConnection()当成普通查询在用,每条SQL各拿一个连接,结果事务根本不存在——因为每个连接是独立的,各自会自动提交。正确的做法是在业务方法里获取一个连接,关掉自动提交,全部SQL执行完统一commit,任何一个失败都rollback:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,事务从这里开始 // 执行多条写操作:扣库存、插借阅记录…… conn.commit(); // 全部成功,提交 } catch (SQLException e) { if (conn != null) { conn.rollback(); // 任何一步失败,回滚全部 } throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } }setAutoCommit(false)之后,所有SQL都在同一个事务里执行,直到commit才真正落盘。这里最忌讳的是把getConnection()封装成一个静态方法,每条SQL都调用一次,然后还指望它是同一个事务。事务必须绑定同一个Connection,没有例外。
4. 借书还书的核心业务:事务、状态流转与逾期计算
模块和连接层就绪后,核心业务就集中到两个方法上:借书和还书。这两个方法覆盖了事务、状态流转、日期计算和并发控制,是这个项目里最值得反复写的部分。
4.1 借书流程:先查后扣,两条写操作必须放进同一个事务
public boolean borrowBook(int bookId, int readerId) throws SQLException { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 第一步:校验图书和读者状态 String checkSql = "SELECT available_count, status FROM books WHERE id = ? FOR UPDATE"; PreparedStatement checkStmt = conn.prepareStatement(checkSql); checkStmt.setInt(1, bookId); ResultSet rs = checkStmt.executeQuery(); if (!rs.next()) { throw new SQLException("图书不存在"); } int available = rs.getInt("available_count"); int bookStatus = rs.getInt("status"); if (bookStatus == 2 || available <= 0) { throw new SQLException("图书不在馆或不可借"); } String readerSql = "SELECT status FROM readers WHERE id = ?"; PreparedStatement readerStmt = conn.prepareStatement(readerSql); readerStmt.setInt(1, readerId); ResultSet readerRs = readerStmt.executeQuery(); if (!readerRs.next() || readerRs.getInt("status") != 0) { throw new SQLException("读者状态异常,无法借书"); } // 第二步:扣减可借数量,同时把图书状态置为借出 String updateBook = "UPDATE books SET available_count = available_count - 1, " + "status = 1 WHERE id = ? AND available_count > 0"; PreparedStatement updateStmt = conn.prepareStatement(updateBook); updateStmt.setInt(1, bookId); if (updateStmt.executeUpdate() == 0) { throw new SQLException("图书已被借出"); } // 第三步:插入借阅记录,借期默认30天 String insertBorrow = "INSERT INTO borrows (book_id, reader_id, borrow_date, due_date, status) " + "VALUES (?, ?, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY), 0)"; PreparedStatement insertStmt = conn.prepareStatement(insertBorrow); insertStmt.setInt(1, bookId); insertStmt.setInt(2, readerId); insertStmt.executeUpdate(); // 在同一个事务里,清理资源时不要关闭 conn conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }这段代码的核心在SELECT ... FOR UPDATE。它会对books表里id = ?这一行加排他锁,锁一直保持到事务提交或回滚。两个管理员同时借同一本书时,第二个事务会阻塞在SELECT上,直到第一个事务提交后才能读到最新数据,从而避免把库存扣成负数。
UPDATE books SET available_count = available_count - 1 ... WHERE id = ? AND available_count > 0是第二道防线。即使前面的行锁没有命中,这个条件也会保证只有库存大于0时才会执行更新,executeUpdate()返回0就说明没抢到。
due_date用DATE_ADD(CURDATE(), INTERVAL 30 DAY)在SQL端计算,借期30天写死在SQL里。如果想把借期做成参数,就把30换成?,再通过setInt传入。这种写法比在Java端算好日期再传字符串更可靠,日期格式不会出错。事务里抛出的异常会触发rollback(),三条SQL要么全成功,要么全不生效,不会出现图书状态改了、借阅记录没插进去的中间状态。
4.2 还书与逾期罚款:用LocalDate算清每一天
public boolean returnBook(int borrowId) throws SQLException { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); String query = "SELECT book_id, due_date, return_date, status FROM borrows WHERE id = ? FOR UPDATE"; PreparedStatement qStmt = conn.prepareStatement(query); qStmt.setInt(1, borrowId); ResultSet rs = qStmt.executeQuery(); if (!rs.next() || rs.getInt("status") == 1) { throw new SQLException("借阅记录不存在或已归还"); } int bookId = rs.getInt("book_id"); java.sql.Date dueDate = rs.getDate("due_date"); // 用 java.time 计算逾期天数,避免 Date 的过期 API 出错 java.time.LocalDate dueLocal = dueDate.toLocalDate(); java.time.LocalDate today = java.time.LocalDate.now(); long overdueDays = java.time.temporal.ChronoUnit.DAYS.between(dueLocal, today); if (overdueDays < 0) { overdueDays = 0; } // 罚金规则:每天0.5元,封顶50元 double fine = Math.min(overdueDays * 0.5, 50.0); String updateBorrow = "UPDATE borrows SET return_date = CURDATE(), " + "fine = ?, status = 1 WHERE id = ?"; PreparedStatement uStmt = conn.prepareStatement(updateBorrow); uStmt.setDouble(1, fine); uStmt.setInt(2, borrowId); uStmt.executeUpdate(); // 归还后把库存加回来,如果这是最后一本,状态改回在馆 String updateBook = "UPDATE books SET available_count = available_count + 1, " + "status = 0 WHERE id = ?"; PreparedStatement bStmt = conn.prepareStatement(updateBook); bStmt.setInt(1, bookId); bStmt.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }日期计算是这里最容易出错的地方。java.util.Date的老API在计算“相隔几天”时容易踩时区、毫秒的坑,我一般直接用java.time.LocalDate。ChronoUnit.DAYS.between返回的两个日期相隔天数,负数表示还没到期,直接归零。
还书时同样要对借阅记录加FOR UPDATE,防止同一本书被两次归还。归还的最终状态统一写成status = 1,不管它之前是“借出”还是“逾期未还”,都在归还动作里一笔勾销。图书的available_count加回1,status恢复成0。
逾期罚金规则是典型的业务参数,每天0.5元、封顶50元写死在代码里是为了课设演示。如果想让规则可配置,可以抽到常量类,或者建一张config表放罚金单价和封顶金额,业务方法里读配置。这样答辩时改需求不用改代码,加分项。
4.3 统计查询与分页:排序走索引,检索用参数化
-- 逾期未还图书列表(按逾期天数倒序) SELECT r.card_no, r.name AS reader_name, b.title AS book_title, br.borrow_date, br.due_date, DATEDIFF(CURDATE(), br.due_date) AS overdue_days FROM borrows br JOIN readers r ON br.reader_id = r.id JOIN books b ON br.book_id = b.id WHERE br.status IN (0, 2) AND br.due_date < CURDATE() ORDER BY overdue_days DESC LIMIT 0, 20; -- 借阅次数最多的10本图书 SELECT b.title, COUNT(*) AS borrow_times FROM borrows br JOIN books b ON br.book_id = b.id GROUP BY br.book_id, b.title ORDER BY borrow_times DESC LIMIT 10;ORDER BY overdue_days DESC里那个overdue_days是别名,MySQL允许在ORDER BY里使用别名,但在WHERE里必须用原始字段比较,因为别名在WHERE阶段还不存在。排序字段如果想走索引,直接排due_date就行,别在Java层做内存排序。
Java侧执行查询时,凡是带用户输入的条件,一律用PreparedStatement的?占位,不能直接拼接字符串。这既是防SQL注入,也是让MySQL能复用执行计划。分页用LIMIT offset, size,页数变大后offset很大时效率会下降,课设数据量小不用纠结。
统计查询不需要事务,连接用完立即关闭即可。把报表SQL封装成独立的查询方法放ReportDAO,跟借还书的写操作分开,代码结构会清晰很多,答辩时也容易讲。
5. 避坑与排查:JAVA+MySQL项目最常见的5个翻车现场
这5个坑我基本都踩过一遍,写下来省得你反复试错。有些问题看起来像“玄学”,其实都能从驱动版本、URL参数、事务边界、字符集这几条线找到根因。
5.1 环境层翻车:驱动丢失、SSL连接错误、认证插件不匹配
现象:运行时报ClassNotFoundException: com.mysql.cj.jdbc.Driver。
原因:绝大多数是驱动jar没进classpath。很多人照着老教程把jar路径写进系统环境变量CLASSPATH,结果换目录、换IDE、换机器就找不到类;还有人是Maven项目但没加mysql-connector-j依赖。
解决:用IDE的“Add to Build Path”或者Maven/Gradle管理依赖,别动系统CLASSPATH。确认jar版本和MySQL服务器版本在同一个大版本:MySQL 8配8.x驱动,MySQL 5.7配5.1.x以上驱动也能用。
现象:能加载驱动,但连接时报Communications link failure或SSL connection error。
原因:连接URL少了serverTimezone,或没关SSL。MySQL 8默认开启SSL要求,本地开发没有合法的服务器证书,握手阶段就失败;serverTimezone缺失会让连接时区设置失败,同样报错。
解决:URL加上?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true。这三件套能解决绝大多数MySQL 8连接问题。mysql ssl连接错误这个关键词搜出来的一大半答案,都是这三件套。
现象:连接时提示Public Key Retrieval is not allowed,或者Access denied for user。
原因:MySQL 8默认认证插件是caching_sha2_password,旧版驱动(5.1.x)不认这个插件,或者不允许自动获取公钥做加密传输。
解决:把驱动升级到8.x并在URL加allowPublicKeyRetrieval=true。如果你用的是5.7版本的数据库,创建用户时也可以指定mysql_native_password,但那是为了兼容老客户端,生产环境不建议绕开新认证机制。
5.2 数据层翻车:中文乱码、并发借同一本书
现象:Java往MySQL里写入中文,查出来全是??。
原因:这是一条链路的问题,可能坏在三个位置:建库时字符集不是utf8mb4;连接URL没带characterEncoding=utf8;MySQL服务端character_set_server配置不对。只改一个位置不一定见效。
解决:按“建库、连接、服务端”三层排查。建库语句加DEFAULT CHARACTER SET utf8mb4;URL加useUnicode=true&characterEncoding=utf8;最后确认MySQL配置文件[mysqld]段有character_set_server=utf8mb4。三层都对齐,中文就稳定了。
现象:两个管理员同时借同一本书,库存被扣成负数,或者出现两条相同的借阅记录。
原因:先SELECT判断可借,再UPDATE扣减,这两步之间有间隙。两个事务都读到同一本可借,然后各自执行UPDATE,后执行的把前一个覆盖成负数。MySQL默认的锁分类里,InnoDB在可重复读隔离级别下,普通UPDATE会对命中的行加排他锁,但前提是你的事务和索引都正确,而且SELECT不加锁时,两个连接确实能同时读到旧的库存。
解决:在借书的第一步就执行SELECT ... FOR UPDATE,对这条记录加排他锁,让第二个事务阻塞到第一个事务提交;同时UPDATE里带上AND available_count > 0作为最后一道校验,executeUpdate()返回0就说明没抢到。这两层都做了,并发借书的问题才真正堵死。
关于MySQL事务处理和锁分类的知识背景,这里有个简单认知:InnoDB的锁是索引锁,WHERE id = ?能命中主键索引,所以锁的是这一行;如果条件字段没有索引,InnoDB会锁更多行甚至全表,这也是为什么前面强调索引要建在查询和更新条件的字段上。
6. 进阶:从单机到Web化,离生产还差哪几步
6.1 用JDK定时任务做逾期提醒:不急着上框架
课设里“逾期未还”通常靠管理员手动查询,但很多真实需求是系统每天自动扫描一次,把逾期读者列出来。说到定时任务,很多人一上来就想用Quartz、XXL-Job这些框架。对于这个小系统,JDK自带的ScheduledExecutorService完全够用:
import java.time.LocalDate; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class OverdueTask { public static void start() { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); // 每天凌晨2点执行一次,初始延迟按需设置 scheduler.scheduleAtFixedRate(() -> { String sql = "SELECT COUNT(*) AS cnt FROM borrows " + "WHERE status = 0 AND due_date < ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setDate(1, java.sql.Date.valueOf(LocalDate.now())); ResultSet rs = ps.executeQuery(); if (rs.next() && rs.getInt("cnt") > 0) { // 这里只输出数量,实际项目会发站内信或通知 System.out.println("当前逾期未还记录数:" + rs.getInt("cnt")); } } catch (Exception e) { // 定时任务要记录异常,不能吞掉 e.printStackTrace(); } }, 0, 24, TimeUnit.HOURS); } }scheduledAtFixedRate保证按固定频率执行,凌晨2点跑一次统计。真实项目里这里会发短信或站内信,但通信接口是另一个话题。小系统用JDK自带定时器,足够了,等量大了再换xxl-job或者Quartz不迟。
6.2 并发借书的验证方法与Web化第一步
改完事务代码后,怎么证明并发控制真的生效?别用脑补,开两个终端连同一个数据库,同时执行借书事务。第一个事务提交前,第二个会阻塞在SELECT ... FOR UPDATE上;第一个提交后,第二个读到的是新库存,如果库存已被借光,UPDATE返回0并回滚。观察这个现象,比看十遍理论都管用。
Web化的第一步不是急着引入Spring Boot,而是把借书、还书、查询这些业务方法从界面代码里抽出来,改成无状态Service类,输入参数就是bookId、readerId,不再依赖Swing或控制台。然后把DBUtil换成HikariCP连接池,最后才是加Controller层。这三步做完后用JMeter模拟20个并发借同一个ID,看事务是否仍然正确。
我做这类项目时吃过最大的亏,是还没把表结构想清楚就急着写界面,后期几乎每个功能都在为表结构打补丁。现在不管题目的PDF写得怎么样,我都会先花半天把状态流转和事务边界画清楚再动手。希望帮到你。
本文还有配套的精品资源,点击获取