
简介这是一套基于JavaWeb技术栈的图书借阅管理系统项目源码采用JSPJavaBeanMySQLTomcat经典架构适合JavaWeb初学者、毕业设计或课程设计参考也可作为图书馆小型信息化管理的练习原型。系统围绕读者与管理员双角色设计读者可完成注册登录、条件查询与借阅图书、查看借阅历史、归还图书及个人信息维护管理员可对图书进行增删改查对读者进行删除和修改并查询全部借阅记录业务模块覆盖常见CRUD操作。资源包共38个文件压缩包大小仅1.76MB包含17个JSP页面、Java类、CSS样式、SQL数据库脚本、项目配置文件和说明文档等目录结构简洁便于导入部署与二次开发。目前已有531人学习下载。配套内容还提供详细的运行必看说明和《图书借阅管理系统介绍》文档能帮助用户快速完成数据库导入、连接配置及项目启动适合用来理解JavaWeb分层开发与基础业务实现。1. 项目定位与需求梳理一个图书借阅系统到底要做什么图书借阅管理系统几乎是每个学JavaWeb的人绕不开的一个题目。不管是课程设计、毕业设计还是刚入门想找个完整案例练手这个项目永远排在推荐榜前列。原因很简单它的业务模型足够经典功能边界清晰又恰好覆盖了JavaWeb开发最核心的知识点——Servlet生命周期、JSP页面渲染、JDBC数据库操作、Session会话管理、Filter过滤器拦截以及最基础的前后端数据交互。但很多人在动手之前根本没想清楚这个系统“到底要做什么”上来就建工程、写代码结果做到一半发现功能互相冲突表结构改了又改。我见过太多人卡在这种地方所以先把需求聊透。1.1 核心功能边界与角色划分一个标准的图书借阅管理系统核心一定围绕“图书”和“借阅”这两个关键词展开。往细了拆功能大致落在三块图书管理图书信息的增删改查包括书名、作者、ISBN、分类、库存总量、当前可借数量、上架状态。读者管理读者的注册、信息维护、借阅状态查看。读者可能分普通用户和管理员两种角色普通用户能查书、借书、还书、看自己的借阅记录管理员额外拥有图书上下架、读者账户管理、逾期订单处理这些权限。借阅流程管理借书、还书、续借、预约、逾期处理。这一块是整个系统的业务核心也是最容易出逻辑漏洞的地方。有人会问需不需要做“预约”和“续借”我的建议是如果做课程设计可以把“预约”先砍掉但“续借”建议保留因为它能帮你把“借阅记录状态机”这个数据模型想清楚——一条借阅记录从借出到归还中间可能要经历续借、逾期、归还等多个状态这个状态流转本身就是很好的设计练习。1.2 这个项目适合谁、能锻炼什么能力如果你是初学者我强烈建议不要直接上Spring Boot而是从JSP Servlet JDBC这套“最原始”的组合开始。原因我在后面“技术选型”会展开说。如果你是有一定经验的开发者想快速回顾JavaWeb基础这个项目也够用——它的业务逻辑不算复杂但麻雀虽小五脏俱全足够让你把HTTP请求处理、数据库事务、会话跟踪这些概念重新过一遍。2. 技术选型与工程结构为什么选JSPServletMySQL这套组合技术选型这件事核心原则是“匹配目标”。你不能说为了练手就非要用最冷门的技术也不能为了炫技把简单项目做成微服务架构。图书借阅管理系统这个体量最合理的技术组合就是JSP Servlet JDBC MySQL前端用JSP内置的JSTL/EL表达式或者干脆手写HTML AJAX。2.1 为什么不直接上Spring Boot很多初学者直接拿Spring Boot做这个项目做完发现除了“自动配置很爽”之外对底层原理还是一头雾水。我的观点是Spring Boot适合做生产级项目但不适合做“理解原理”的项目。图书借阅管理系统里借一本、还一本、查个书单这种场景下Spring Boot帮你省掉的恰恰是你最需要练习的部分——你亲手写一个Servlet、手动拿到HttpSession、自己用PreparedStatement拼SQL才能理解框架到底帮你做了什么。用这套“老技术”还有一个实际好处它对运行环境的要求极低一个Tomcat、一个MySQL、一个IDEA或者VSCode就能跑起来部署也不用碰Docker、Nginx这些额外的东西对新手非常友好。2.2 三层架构与包结构规划项目一旦脱离“单文件写到底”的阶段就必须讲究分层。我用的是最经典的Web三层架构表现层web层放Servlet和JSP。Servlet负责接收请求、调用业务逻辑、跳转页面JSP负责展示数据不在JSP里写Java业务代码。业务层service层承接具体的业务规则比如“借书时检查库存是否充足”“还书时判断是否逾期”。这一层是项目最容易写乱的地方很多人把业务逻辑全堆在Servlet里最后Servlet动辄几百行根本没法维护。数据层dao层封装JDBC操作职责是“把数据存取做好”不掺入业务判断。包结构我建议这样分com.library ├── entity // 实体类Book, Reader, BorrowRecord ├── dao // 数据访问接口 实现类 ├── service // 业务逻辑接口 实现类 ├── servlet // 控制器LoginServlet, BookServlet, BorrowServlet... ├── filter // 过滤器LoginFilter, EncodingFilter └── utils // 工具类DBUtil连接管理这个结构是老工程的标准范式你以后看很多开源老项目会发现都是这个布局提前熟悉不吃亏。2.3 开发环境与工具链准备开发环境我实测下来比较顺手的一套是JDK 8或11别用太新的版本Tomcat 9对JDK 8支持最稳。Tomcat 9.0别装Tomcat 10因为Jakarta EE改名之后Servlet包名从javax.servlet变成了jakarta.servlet网上大量老教程直接跑不通新手很容易被坑。MySQL 5.7或8.0都可以注意连接驱动版本要配套。开发工具IDEA社区版完全够用如果你想用VSCode也可以装上Java Extension Pack和Tomcat插件就能跑只是调试体验比IDEA差一些。强烈建议用IDEA因为它对Tomcat的集成做得最顺。3. 数据库设计三张核心表和那些容易忽略的字段数据库设计是整个项目的地基。表结构设计得不好后面写代码就是一场灾难。这个项目最核心的表就三张图书表、用户表、借阅记录表。我见过很多人在这三张表上反复改字段核心原因是一开始没想清楚“一条借阅记录到底要记录什么信息”。3.1 核心表的字段设计图书表book字段名类型说明idINT PK AUTO_INCREMENT主键isbnVARCHAR(20)ISBN编号注意要加唯一索引book_nameVARCHAR(100)书名authorVARCHAR(50)作者categoryVARCHAR(30)分类publisherVARCHAR(50)出版社total_countINT总库存available_countINT当前可借数量statusTINYINT上架状态1在架 0下架create_timeDATETIME创建时间update_timeDATETIME更新时间用户表reader字段名类型说明idINT PK AUTO_INCREMENT主键usernameVARCHAR(30)用户名唯一passwordVARCHAR(64)密码建议MD5加密存储real_nameVARCHAR(30)真实姓名roleTINYINT角色1管理员 0普通读者borrow_countINT当前未还数量statusTINYINT账户状态1正常 0禁用create_timeDATETIME注册时间借阅记录表borrow_record字段名类型说明idINT PK AUTO_INCREMENT主键reader_idINT用户IDbook_idINT图书IDborrow_timeDATETIME借出时间due_timeDATETIME应还时间return_timeDATETIME实际归还时间未归还是NULLstatusTINYINT0借出中 1已归还 2逾期已归还renew_countINT续借次数3.2 为什么外键要用逻辑关联而不是物理外键我在设计借阅记录表时没有加物理外键FOREIGN KEY只保留了逻辑上的reader_id和book_id字段。原因很实际物理外键在插入、更新时会有额外的约束检查影响写入性能而且一旦未来要分库分表或者做数据迁移物理外键会成为很大的障碍。这不是说外键没用而是对于这个体量的管理系统逻辑关联加代码层校验完全够用灵活性反而更高。还有一个细节borrow_time和due_time我用的都是DATETIME而不是TIMESTAMP。TIMESTAMP有2038年问题虽然离我们很远但DATETIME的存储范围更大而且不受时区影响这一点在后面“常见问题”里会细说时区问题真的会坑人。3.3 初始化数据的准备工作建完表之后千万别急着写代码先往库里插入一批测试数据。我建议至少准备50本图书、10个读者用户、若干条借阅记录覆盖“借出中”“已归还”“逾期”三种状态。这样后面调试分页、筛选、逾期计算时数据一拉出来就能看出逻辑对不对不用临时造数据。4. 核心功能落地登录、CRUD、借还书流程的完整实现这部分是动手环节我把每条主线功能的实现思路和关键代码都过一遍重点讲“为什么这么写”不光是贴代码。4.1 登录与会话管理用Filter统一做登录校验登录功能的实现本身不难难点在于“登录后才能访问”这个约束怎么做。很多人是每个Servlet里都写一遍“判断session里有没有user”的代码又重复又容易漏。正确的做法是用Filter统一拦截。我在web.xml里配置了一个LoginFilter拦截所有/*请求然后做排除判断public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行登录页面、登录接口、静态资源 if (uri.endsWith(/login.jsp) || uri.endsWith(/login) || uri.contains(/static/) || uri.endsWith(/register.jsp)) { chain.doFilter(req, resp); return; } // 其余请求一律校验会话 Object user request.getSession().getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); }这里有个细节排除路径一定要想全比如CSS、JS、图片这些静态资源如果不放行页面样式会全部丢失。我当时第一次做就漏了静态资源排查了半天才发现是Filter把CSS拦了。4.2 图书查询与分页LIMIT和总页数计算图书列表页是系统里最常用的页面如果不做分页几百条数据渲染到一个页面上性能会非常难看。分页的核心公式就两句话查总数SELECT COUNT(*) FROM book WHERE status 1查当页数据SELECT * FROM book WHERE status 1 LIMIT ?, ?LIMIT的两个参数分别是“偏移量”和“每页条数”。偏移量由(当前页码 - 1) * 每页条数计算得出比如每页10条第1页偏移0第2页偏移10。总页数用数学函数向上取整(int) Math.ceil((double) totalCount / pageSize)。分页代码本身不难但有一个我踩过的坑如果你在查询条件里加了keyword搜索那么COUNT(*)的SQL和列表SQL的WHERE条件必须保持一致否则会出现“搜索结果有3页但第三页是空白”这种诡异问题。建议把条件拼接封装成一个方法两处复用。4.3 借书和还书事务处理是核心借书的流程看起来简单就两件事往借阅记录表插一条数据把图书的available_count减1。但两件事如果不放在一个事务里就会出现“记录插进去了库存没减”或者反过来“库存减了记录没插上”的脏数据。JDBC操作事务核心是先setAutoCommit(false)全部执行成功后commit()任何一步异常就rollback()Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 校验图书状态和库存 Book book bookDao.getById(conn, bookId); // 注意要传入conn if (book null || book.getAvailableCount() 0) { throw new BusinessException(图书不存在或库存不足); } // 2. 插入借阅记录 borrowDao.insert(conn, readerId, bookId); // 3. 更新图书库存注意要在插入成功之后 bookDao.decreaseAvailable(conn, bookId); conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); // 恢复默认 conn.close(); } }注意这里的关键点dao层的方法要接收Connection参数让多个数据库操作共用一个连接这样才能保证它们在一个事务里。很多人初学时分不清“同一个连接”和“多个连接”的本质区别导致事务形同虚设。还书的流程类似插入归还时间、把图书available_count加1、更新借阅记录状态为“已归还”。4.4 逾期判断这个业务逻辑藏得住逾期判断是借书系统里最有意思的部分因为它的逻辑不在“借书”和“还书”的操作瞬间而在“日常运行”中——你得有一个定时任务或者动态判断机制来发现哪些记录逾期了。在这个项目里我采用的方式是“动态判断”查询借阅记录时如果return_time IS NULL AND due_time NOW()就判定为逾期。这种做法的好处是代码简单不需要额外跑定时任务缺点是“逾期状态”不是持久化的每次查询都要动态算。如果你希望能在后台管理页面“固定”看到逾期记录可以加一个定时任务比如每天凌晨跑一次把逾期记录的状态字段更新为“已逾期”。但课程设计阶段动态判断完全够用不推荐上来就引入定时任务框架。5. 常见问题排查我在这套系统上踩过的坑最后这部分是干货中的干货我把做这类系统最常见的几个问题集中列出来很多是错误信息特别隐蔽、排查特别费时间的。5.1 中文乱码必须三处同时设置中文乱码几乎是每个JavaWeb新手第一个遇到的噩梦。乱码的本质是字符编码不一致解决思路就是“所有环节都用UTF-8”缺一不可JSP顶部加% page contentTypetext/html;charsetUTF-8 %。Servlet里设置request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)也可以用EncodingFilter统一处理。JDBC连接URL上加参数useUnicodetruecharacterEncodingUTF-8。漏掉任何一处都可能产生乱码。尤其要注意的是request.setCharacterEncoding(UTF-8)只对POST请求的body有效GET请求的URL参数编码要在Tomcat的server.xml里配置URIEncodingUTF-8这一点我和很多人提过十有八九会漏。5.2 日期差计算错误MySQL和Java别混着用计算逾期天数最直观的写法是用Java代码拿到当前时间和due_time做一个Date相减除以一天的毫秒数。但在Java里做日期差计算踩坑点在于毫秒计算会忽略闰秒、夏令时等时间细微差别最好是借助LocalDate的ChronoUnit.DAYS.between来实现long overdueDays ChronoUnit.DAYS.between( dueTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), LocalDate.now() );更简单的办法是直接在SQL里算SELECT DATEDIFF(NOW(), due_time) FROM borrow_record WHERE id ?。实测下来SQL的方式最省事也最不容易出错。两种方式都可以别混着用——如果你在Java里算了天数又在SQL里算了一遍两边口径不一致出来的结果必然对不上。5.3 404和500的常见诱因如果你启动Tomcat后访问项目出现404先检查两件事一是项目的访问路径context path是不是/二是web.xml里Servlet的url-pattern映射是否正确。500则大概率是数据库连接问题——驱动jar包有没有放在WEB-INF/lib目录下MySQL版本和驱动版本是否兼容Class.forName(com.mysql.cj.jdbc.Driver)的类名有没有写对。老版本的MySQL驱动类名是com.mysql.jdbc.Driver新版换成了com.mysql.cj.jdbc.Driver这个坑也很常见。5.4 数据库连接管理不要每次手动开关连接我一开始是用DriverManager.getConnection()在需要时手动创建连接的后来发现并发的请求一多数据库连接就频繁爆掉报Too many connections。推荐的做法是引入一个简单的连接池比如Druid或者HikariCP配置几句就能用性能差距是肉眼可见的。用连接池要注意一个点连接池管理的是连接复用但ThreadLocalConnection如果你用了事务必须保证同一个请求的所有DAO操作拿的是同一个连接。简单设计是在进入Service方法时从连接池借用连接放入ThreadLocalDAO层从ThreadLocal取Service方法结束时归还连接。最后再分享一个小技巧这个项目做完之后如果你想再往上走一步我建议你试着给它加一个“借阅排行榜”——就是统计哪本书被借得最多、哪个读者借书最频繁。这个功能不需要加表一条带GROUP BY的SQL就能搞定但它逼着你去思考“聚合查询”和“多表关联”这两个你迟早要面对的知识点。我个人做了这么多JavaWeb项目最深的一点体会是与其不停追逐新框架不如把一个经典项目吃透。图书借阅管理系统就是这样一块“磨刀石”你把它里面的事务、会话、分页、权限、数据建模这些问题真正搞明白了后面不管学Spring Boot还是学微服务底子都比别人扎实一截。本文还有配套的精品资源点击获取