JavaWeb图书借阅系统实战:从能跑到能扛的完整案例 简介本资源是一套基于JavaWeb的图书借阅管理系统完整项目源码采用jspjavabeanmysqltomcat技术栈面向学习JavaWeb开发的学生与初学者可用于课程设计、毕业设计或实战练手。系统分为读者与管理员两类角色读者可完成注册登录、图书查询与借阅、借阅历史查看、归还图书及个人信息修改管理员则负责图书的增删改查、读者信息维护以及全部借阅记录的查询功能覆盖图书借阅业务的主要流程。压缩包共38个文件以17个jsp页面、3个css样式、2个sql脚本及xml配置、classpath、jar等工程文件为主整体约1.76MB结构清晰便于导入与二次开发。目前已有533人学习下载读者可借此掌握JSP页面开发、JavaBean封装、数据库连接与增删改查等核心技能并参考随附的数据库文件与项目说明快速完成环境搭建与调试。1. 从一份“能跑起来”的图书借阅系统说起JavaWeb 项目到底该长什么样很多人搜“基于JavaWeb的图书借阅管理系统”心里想的其实不是学术定义而是我能不能在本地把一套带登录、借书、还书、查书的系统跑起来顺便把 JavaWeb 这套东西吃透。这个标题背后对应的是一类非常典型的课程设计/毕业设计级项目Servlet JSP 或 Spring Boot MyBatis前端用 JSP、Thymeleaf 或 Vue数据库用 MySQL核心业务是图书、读者、借阅记录三张主表之间的增删改查和状态流转。它适合两类人一类是要交作业、要答辩的学生需要一套结构清晰、能讲清楚、能改得动的代码另一类是刚转 Java 后端、想找一个完整案例把“请求怎么进来、SQL 怎么出去、页面怎么渲染”串起来的初学者。真正决定这套系统能不能用的不是页面多漂亮而是借阅状态机、库存扣减和并发还书这三件事有没有处理干净。下面我按“先能跑、再能改、最后能扛”的顺序把这条路径拆开讲。2. 技术选型与最小可运行骨架ServletJSP 还是 Spring Boot2.1 两种主流路线的取舍搜“基于springboot的图书借阅管理系统的设计与实现”的人越来越多但“javaweb项目完整案例mysql”这类需求依然大量存在说明两条路线都有人走。我的建议很直接如果目标是快速交付、答辩时能讲清楚分层选 Spring Boot MyBatis-Plus MySQL如果课程明确要求手写 Servlet、不能上框架那就老老实实 Servlet JSP JDBC。两者的业务逻辑几乎一样差别只在“谁帮你管对象生命周期”和“SQL 写在哪”。维度ServletJSPJDBCSpring BootMyBatis上手门槛低但要手写大量样板中需理解注解和自动配置分层清晰度靠包名约定Controller/Service/Mapper 天然分层事务控制手动 setAutoCommitTransactional 声明式适合场景课程硬性要求、教学演示毕业设计、想继续扩展选型不要纠结“哪个更高级”要看你答辩时能不能把每一层讲明白。我见过太多人用了 Spring Boot结果被问“依赖注入是什么”就卡住反而扣分。2.2 数据库表结构三张主表加一张日志表图书借阅系统的核心是“书、人、借阅关系”。最小可用模型是四张表book、reader、borrow_record再加一张admin或直接复用 reader 的角色字段。下面是我一般会用的建表语句字段命名尽量直白方便后面写 SQL。-- 图书表库存用 total 和 available 两个字段避免每次 count 借阅记录 CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50), total INT NOT NULL DEFAULT 0, -- 总库存 available INT NOT NULL DEFAULT 0, -- 可借数量 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 读者表role 区分管理员和普通读者避免再建一张管理员表 CREATE TABLE reader ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存 MD5 或 BCrypt别存明文 real_name VARCHAR(30), role TINYINT NOT NULL DEFAULT 0, -- 0 读者 1 管理员 max_borrow INT NOT NULL DEFAULT 5 -- 可借上限 ); -- 借阅记录表status 是状态机的核心 CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL, reader_id BIGINT NOT NULL, borrow_time DATETIME NOT NULL, due_time DATETIME NOT NULL, -- 应还时间 return_time DATETIME, -- 实际归还时间未还为 NULL status TINYINT NOT NULL DEFAULT 0, -- 0 借出 1 已还 2 逾期 INDEX idx_reader (reader_id), INDEX idx_book (book_id) );available字段是这套设计的重点。很多人只存total每次借书时用total - (select count(*) from borrow_record where status0)去算数据量一大就慢而且并发下容易算错。用冗余字段换查询性能是这类系统里最划算的取舍。status用 0/1/2 而不是字符串是为了索引效率和状态判断方便。2.3 用 IDEA 跑通第一个接口“idea运行javaweb项目配置”是高频搜索词说明卡在环境上的人非常多。以 Spring Boot 路线为例最小可运行步骤是新建 Spring Initializr 项目勾选 Spring Web、MyBatis Framework、MySQL Driver然后在application.yml里配数据源。spring: datasource: url: jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true # 数据库下划线转 Java 驼峰配好后写一个BookController用RestController暴露/book/list能返回 JSON 就说明链路通了。这一步不要急着写页面先把“数据库→Mapper→Service→Controller”这条线打通后面加 JSP 或 Vue 都只是换渲染层。如果启动报Communications link failure九成是 MySQL 没启动或时区参数没加报Access denied就是账号密码问题别去改代码。提示IDEA 里如果 JSP 页面 404先检查pom.xml有没有加tomcat-embed-jasper依赖Spring Boot 内置 Tomcat 默认不解析 JSP。3. 借阅核心业务落地状态机、库存扣减与事务边界3.1 借书流程的状态流转借书不是简单的 insert 一条记录。完整流程是校验读者是否存在且未超借阅上限 → 校验图书available 0→ 扣减available→ 插入借阅记录 → 计算应还时间。这四步必须在一个事务里否则会出现“库存扣了但记录没插进去”的脏数据。下面是我常用的 Service 层写法。Transactional(rollbackFor Exception.class) public void borrow(Long bookId, Long readerId) { Reader reader readerMapper.selectById(readerId); // 1. 校验借阅上限统计该读者未归还记录数 Long borrowing borrowMapper.selectCount( new QueryWrapperBorrowRecord() .eq(reader_id, readerId) .eq(status, 0)); if (borrowing reader.getMaxBorrow()) { throw new BizException(已达借阅上限); } // 2. 扣库存用带条件的 update避免并发超借 int rows bookMapper.decreaseAvailable(bookId); if (rows 0) { throw new BizException(库存不足); } // 3. 插入借阅记录应还时间默认 30 天 BorrowRecord record new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowTime(LocalDateTime.now()); record.setDueTime(LocalDateTime.now().plusDays(30)); record.setStatus(0); borrowMapper.insert(record); }关键在decreaseAvailable这条 SQL它必须带available 0条件把判断和扣减合成一个原子操作UPDATE book SET available available - 1 WHERE id #{bookId} AND available 0;返回影响行数为 0 就说明库存没了直接抛异常回滚。这样即使两个请求同时进来数据库行锁也会保证只有一个能扣成功。如果先select再update中间就有窗口期高并发下必然超借。这是我在实际项目里踩过的坑测试时用 JMeter 开 50 个线程同时借同一本书先查后扣的写法能借出 50 本而带条件的 update 只会成功available本。3.2 还书与逾期判定还书逻辑比借书简单但有两个细节容易翻车。第一还书时要判断记录当前状态是不是“借出”已还的记录不能重复还第二还书时要把available加回去但加回去不能超过total。下面是对应的 SQL 和 Service 逻辑。Transactional(rollbackFor Exception.class) public void returnBook(Long recordId) { BorrowRecord record borrowMapper.selectById(recordId); if (record null || record.getStatus() ! 0) { throw new BizException(记录不存在或已归还); } // 更新记录状态和归还时间 record.setStatus(1); record.setReturnTime(LocalDateTime.now()); borrowMapper.updateById(record); // 库存加回但不能超过 total bookMapper.increaseAvailable(record.getBookId()); }UPDATE book SET available available 1 WHERE id #{bookId} AND available total;available total这个条件就是后悔药防止因为数据异常导致库存越还越多。逾期判定不需要定时任务查询时用due_time now() and status 0动态判断即可展示层标红。如果非要落库成 status2就写一个每天凌晨跑的定时任务用Scheduled注解但要注意任务执行时间别和备份冲突。3.3 分页查询与条件组合图书列表和借阅记录列表都涉及分页加多条件查询。用 MyBatis-Plus 的Page对象最省事但要注意QueryWrapper的条件拼接顺序like和eq混用时容易写出全表扫描。public PageBook pageBooks(int pageNum, int pageSize, String keyword) { PageBook page new Page(pageNum, pageSize); QueryWrapperBook wrapper new QueryWrapper(); if (StringUtils.hasText(keyword)) { // 标题或作者模糊匹配注意括号包裹 or 条件 wrapper.and(w - w.like(title, keyword) .or().like(author, keyword)); } wrapper.orderByDesc(create_time); return bookMapper.selectPage(page, wrapper); }wrapper.and(w - ...)这层包裹很关键。如果不加like title or like author会和前面的其他条件形成A and B or C逻辑就错了。这是 MyBatis-Plus 里最常见的翻车点查出来的数据莫名其妙多了一堆排查半天才发现是括号问题。4. 避坑与排查那些让系统“看着能跑、一用就崩”的细节4.1 中文乱码从数据库到页面的全链路现象图书标题存进去是????或者页面显示问号。原因通常有三层数据库字符集不是utf8mb4、JDBC URL 没加characterEncodingutf8、Tomcat 的server.xml没配URIEncoding。解决顺序是先用SHOW VARIABLES LIKE character%确认数据库再检查连接串最后看容器配置。Spring Boot 项目还要确认server.servlet.encoding.charsetUTF-8和forcetrue。三层都对了乱码基本消失。4.2 事务不生效方法内部调用是黑匣子现象borrow方法上加了Transactional但库存扣了、记录没插数据不一致。原因多半是同类内部方法直接调用比如this.borrow()这样走的是原始对象而不是代理对象事务注解形同虚设。解决办法是把借阅逻辑抽到另一个 Service或者用AopContext.currentProxy()拿代理。更隐蔽的一种是异常被try-catch吞了事务感知不到自然不会回滚。记住Transactional默认只对RuntimeException回滚捕获后不抛出等于没加。4.3 连接池耗尽连接忘了关现象系统跑一会儿就卡死日志报HikariPool-1 - Connection is not available。原因是用 JDBC 手写代码时Connection、Statement、ResultSet没在finally里关闭连接泄漏。用 MyBatis 或 JPA 一般不会但如果你在 Servlet 里直接DriverManager.getConnection就必须手动关。排查方法是看数据库show processlist如果大量连接处于Sleep状态且时间很长基本就是泄漏。解决就是统一用 try-with-resources或者干脆交给框架管理。4.4 库存超卖并发下的经典翻车现象压测时同一本书借出数量超过total。原因就是前面说的“先查后扣”写法。解决就是带条件的原子 update并且把校验逻辑放在 SQL 的where里而不是 Java 代码里。另外如果用了 Redis 缓存库存还要考虑缓存和数据库的一致性简单做法是扣减直接走数据库缓存只做查询加速并设短过期时间。4.5 日期类型映射LocalDateTime 报错现象实体类用LocalDateTimeMyBatis 查询报Cannot convert value of type String to LocalDateTime。原因是 MySQL 驱动版本和 MyBatis 类型处理器不匹配。解决是升级mysql-connector-java到 8.x并在实体字段上确认没有多余的JsonFormat干扰。如果前端传的是yyyy-MM-dd字符串Controller 参数上加DateTimeFormat(pattern yyyy-MM-dd)即可。5. 从能跑到能讲答辩演示与二次扩展的实用技巧走到这里系统基本能借能还了。但如果你是要答辩或者想把它写进简历光有 CRUD 还不够得让面试官或老师看到你处理过“边界”。我一般会加三个东西借阅排行榜、逾期提醒、以及一个简单的权限拦截。借阅排行榜用一条聚合 SQL 就能出展示哪本书被借得最多答辩时是个不错的亮点SELECT b.title, COUNT(*) AS cnt FROM borrow_record r JOIN book b ON r.book_id b.id GROUP BY r.book_id ORDER BY cnt DESC LIMIT 10;逾期提醒不需要复杂推送登录时查一次status0 and due_time now()的记录在首页顶部用红色横幅提示即可。权限拦截用 Spring 的HandlerInterceptor判断 session 里role是否为 1不是就拦掉/admin/**。这三样加起来不到 200 行代码但能让你的系统从“作业”变成“有想法的作业”。演示时有个小技巧提前准备两条数据一条正常借还一条逾期未还现场操作时先展示正常流程再展示逾期列表和排行榜最后打开数据库表给老师看状态字段的变化。顺序对了讲解就顺。另外别在答辩现场临时改代码我见过太多人现场改配置把项目跑崩的血泪经验就是演示前把数据库导出成 SQL 备份真崩了直接还原。最后说一个我自己的习惯每做完一个模块就在README.md里记一句“这个模块我踩了什么坑、怎么解的”。一开始觉得麻烦后来发现面试时被问到“你遇到过什么困难”这些记录就是现成的答案。图书借阅系统本身不复杂但把它做扎实、讲清楚比堆一堆用不上的功能更有价值。希望帮到你。本文还有配套的精品资源点击获取