JSP+Servlet+MySQL图书馆借阅系统源码解析与实战部署

发布时间:2026/10/28 12:57:11
JSP+Servlet+MySQL图书馆借阅系统源码解析与实战部署

简介:这是一套面向计算机相关专业学生与Java Web初学者的图书馆图书借阅管理系统完整源码,采用jsp+servlet+dao+mysql技术栈实现,可作为毕业设计、课程设计或作业参考。系统区分管理员、学生与游客三种角色:管理员可登录、管理用户与图书、处理罚款缴纳;学生可登录、借阅归还图书、查看借阅记录并修改密码;游客可浏览图书列表与详情。压缩包共247个文件,约6.7MB,包含45个java源文件、45个class编译文件、20个jsp页面、18个js脚本、13个css样式、10个jar依赖及sql建库脚本、xml配置等,覆盖从数据库到前端的完整工程结构。项目已在jdk1.8/1.9与Tomcat8.x/9.x下测试运行成功,支持Eclipse、Myeclipse、IDEA导入,并附文档说明与管理员、学生测试账号。目前已有144人学习,适合需要完整赛题方案、排错思路与可二次开发代码的读者参考。

1. 从一份 jsp+servlet+mysql 图书馆借阅系统源码说起:它到底能帮你省下多少事

如果你正在做 JavaWeb 课程设计或者毕业设计,大概率绕不开「图书馆图书借阅管理系统」这个题目。它几乎是每个计算机专业学生都会遇到的经典命题:读者要能查书、借书、还书,管理员要能管书、管人、管借阅记录。听起来不复杂,但真从零开始写,光是 JSP 页面跳转、Servlet 请求分发、MySQL 表结构设计这三块就能耗掉一两周。而一份完整的 jsp+servlet+mysql 图书馆图书借阅管理系统源码,加上配套的文档说明和数据库脚本,本质上就是把这段重复劳动压缩成一次「读懂 + 改配置 + 跑通」的过程。它适合三类人:赶课程设计 deadline 的学生、想拿一个完整 JavaWeb 项目练手的新人、以及需要快速搭出可演示原型的小团队。核心价值不在于代码多高级,而在于它把 MVC 分层、JDBC 连接、会话管理这些知识点串成了一条能跑通的链路,让你能把精力放在理解和改造上,而不是卡在环境配置和表结构设计上。

2. 拆开这份源码:JSP+Servlet+MySQL 三层结构到底怎么分工

2.1 为什么这个组合至今仍是 JavaWeb 入门首选

JSP + Servlet + MySQL 这套组合,放在今天的技术栈里确实不算新,但它有一个其他框架替代不了的优势:透明。Spring Boot 帮你把请求映射、数据库连接、事务管理全封装好了,出问题你只能看日志猜;而 JSP + Servlet 的每一层都是裸露的,浏览器发一个请求,Tomcat 怎么解析、Servlet 怎么拿到参数、JDBC 怎么执行 SQL、结果怎么塞回 JSP 渲染,整条链路你都能在代码里一行行跟下来。对于要理解 HTTP 请求生命周期、Session 机制、MVC 分层这些基础概念的人来说,这种透明性比开发效率更重要。

具体到图书馆借阅系统,典型的分工是这样的:JSP 负责展示层,包括登录页、图书列表页、借阅记录页;Servlet 充当控制器,接收表单提交、调用业务方法、决定跳转到哪个页面;MySQL 存储图书信息、读者信息、借阅记录三张核心表;中间用 JDBC 或者简单的 DAO 模式做数据访问。这个结构不复杂,但每一层都有容易翻车的地方,后面会逐个拆。

2.2 数据库表结构:三张核心表怎么设计才不返工

拿到源码后第一件事不是急着跑,而是先看数据库脚本。图书馆借阅系统的表结构设计直接决定了后面业务逻辑好不好写。常见做法是三张主表加若干辅助表:

表名核心字段说明
bookid, isbn, title, author, publisher, total_count, available_count图书信息,available_count 是关键
readerid, card_no, name, phone, status读者信息,status 控制是否可借
borrow_recordid, book_id, reader_id, borrow_date, due_date, return_date, status借阅记录,status 区分在借/已还/逾期

这里最容易踩的坑是库存字段。很多源码只用一个count字段表示库存,借书减一、还书加一。但实际场景里你需要区分「总藏书量」和「可借数量」,因为有人借走没还的时候,总藏书量不变,可借数量减少。如果只用一个字段,管理员想查「这本书一共几本」就查不到了。所以total_count和available_count要分开,借书时只减available_count,还书时只加available_count。

另一个容易忽略的是借阅记录的状态字段。如果只用return_date是否为空来判断是否归还,遇到逾期未还的情况就没法区分「正常在借」和「已经超期」。加一个status字段,用 0/1/2 分别表示在借、已还、逾期,后面做统计和提醒会方便很多。

2.3 从登录到借书:一个完整请求在代码里怎么流转

理解代码流转最好的方式就是跟一个完整请求。以「读者登录后借一本书」为例,整条链路是这样的:

第一步,用户在login.jsp输入卡号和密码,表单提交到LoginServlet。Servlet 的doPost方法里拿到参数,调用ReaderDao.findByCardNo()查数据库,比对密码。验证通过后把读者对象塞进session.setAttribute("reader", reader),然后重定向到图书列表页。

第二步,图书列表页bookList.jsp从 session 里取读者信息,同时通过BookServlet查询所有可借图书展示出来。每本书后面跟一个「借阅」按钮,点击后提交到BorrowServlet。

第三步,BorrowServlet拿到bookId和 session 里的readerId,先检查这本书的available_count是否大于 0,再检查这个读者有没有逾期未还的记录。两个条件都满足,才执行借书操作:available_count减一,插入一条borrow_record,设置borrow_date为当前时间,due_date为当前时间加 30 天。

这个流程里,事务处理是必须的。减库存和插记录要么都成功,要么都回滚。很多简单源码在这里只写两条独立的 SQL,一旦第二条失败,库存就白白少了一本。正确做法是用 JDBC 的setAutoCommit(false)手动控制事务,两条 SQL 都执行成功后再commit()。

// BorrowServlet 中借书的核心逻辑 Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 扣减可借数量,同时用 available_count > 0 做乐观锁 String sql1 = "UPDATE book SET available_count = available_count - 1 WHERE id = ? AND available_count > 0"; PreparedStatement ps1 = conn.prepareStatement(sql1); ps1.setInt(1, bookId); int affected = ps1.executeUpdate(); if (affected == 0) { conn.rollback(); // 库存不足,跳回列表页并提示 return; } // 2. 插入借阅记录 String sql2 = "INSERT INTO borrow_record(book_id, reader_id, borrow_date, due_date, status) VALUES(?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), 0)"; PreparedStatement ps2 = conn.prepareStatement(sql2); ps2.setInt(1, bookId); ps2.setInt(2, readerId); ps2.executeUpdate(); conn.commit(); // 两条都成功才提交 } catch (Exception e) { if (conn != null) conn.rollback(); e.printStackTrace(); } finally { DBUtil.close(conn); }

这段代码的关键点有三个:setAutoCommit(false)开启事务、available_count > 0作为更新条件防止超借、rollback()保证异常时数据一致。参数上,due_date用DATE_ADD(NOW(), INTERVAL 30 DAY)直接由数据库计算,避免 Java 端和数据库端时间不一致的问题。30 天是常见借阅周期,改成 60 或 90 只需要改这个数字。

3. 把源码跑起来:Tomcat + MySQL 环境配置与部署步骤

3.1 MySQL 安装配置:从下载到建库的完整命令

源码能不能跑起来,一半取决于 MySQL 装得对不对。Windows 上常见做法是下载 MySQL 8.0 的 zip 包解压后手动配置,或者用安装版一路下一步。不管哪种方式,最终要保证三件事:服务能启动、root 密码能登录、字符集是 utf8mb4。

解压版配置的核心步骤是:在解压目录下新建my.ini,写入端口、basedir、datadir 和字符集配置,然后以管理员身份执行初始化命令。

# 1. 初始化数据目录(会生成临时密码,记下来) mysqld --initialize --console # 2. 安装服务 mysqld --install MySQL80 # 3. 启动服务 net start MySQL80 # 4. 用临时密码登录后必须改密码 mysql -u root -p ALTER USER 'root'@'localhost' IDENTIFIED BY '你的新密码';

my.ini里必须写对的两行是character-set-server=utf8mb4和default_authentication_plugin=mysql_native_password。前者保证中文不乱码,后者保证老版本的 JDBC 驱动能连上 MySQL 8.0。很多「Access denied」或者「Unknown character set」的报错,根源都在这里。

建库和导入脚本就一条命令:

mysql -u root -p -e "CREATE DATABASE library DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p library < library.sql

导入完成后用SHOW TABLES;确认三张核心表都在,再用SELECT COUNT(*) FROM book;看看有没有初始数据。如果 book 表是空的,登录进去也看不到书,借阅功能就没法测。

3.2 Tomcat 部署与 JDBC 连接配置的四个必改项

Tomcat 的安装相对简单,下载解压后配置CATALINA_HOME环境变量即可。真正容易出问题的是项目里的 JDBC 配置。拿到源码后,先找到数据库连接工具类,通常叫DBUtil.java或BaseDao.java,里面有四个值必须改成你自己的:

// DBUtil.java 中需要修改的四个参数 private static final String URL = "jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; // 改成你的 MySQL 用户名 private static final String PASSWORD = "你的密码"; // 改成你的 MySQL 密码 private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; // MySQL 8.0 用 cj 驱动

URL 里的三个参数各有作用:useSSL=false关掉 SSL 避免证书报错,serverTimezone=Asia/Shanghai解决时区差 8 小时的问题,characterEncoding=utf8保证中文参数不乱码。驱动类名从 MySQL 5.x 的com.mysql.jdbc.Driver变成了 8.0 的com.mysql.cj.jdbc.Driver,写错了会直接抛ClassNotFoundException。

部署时把项目打成 war 包丢进webapps目录,或者直接在 IDE 里配置 Tomcat 运行。访问http://localhost:8080/项目名/login.jsp能看到登录页,说明部署成功。如果 404,检查 war 包名和访问路径是否一致;如果 500,看 Tomcat 日志里具体的异常堆栈。

3.3 登录失败与中文乱码:两个最高频的启动问题

登录失败分两种:连数据库都连不上,和连上了但查不到用户。前者看异常信息,Communications link failure说明 MySQL 服务没启动或者端口不对;Access denied for user说明用户名密码错了。后者通常是数据问题,比如 reader 表里没有初始用户,或者密码字段存的是明文但代码里做了 MD5 加密。打开 reader 表看一眼数据,再对照 LoginServlet 里的密码比对逻辑,基本就能定位。

中文乱码分请求乱码和响应乱码。请求乱码是 POST 表单提交的中文到 Servlet 里变成问号,解决方式是在 Servlet 的doPost第一行加request.setCharacterEncoding("UTF-8")。响应乱码是 JSP 页面显示中文变乱码,需要在 JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>。如果是 GET 请求的中文参数乱码,还得改 Tomcat 的server.xml,在 Connector 标签里加URIEncoding="UTF-8"。这三个地方都改到位,乱码问题基本绝迹。

4. 避坑与排查:这份源码最容易翻车的五个地方

4.1 借书超借:库存扣减没有加条件判断

现象是两个人同时借同一本书,明明只剩一本,两个人都借成功了,available_count变成 -1。原因是扣减库存的 SQL 只写了UPDATE book SET available_count = available_count - 1 WHERE id = ?,没有加AND available_count > 0这个条件。并发情况下两个线程都读到库存为 1,都执行了扣减。

解决办法就是在 UPDATE 语句里加上AND available_count > 0,然后判断executeUpdate()的返回值。返回 0 说明库存已经被别人扣完了,当前请求应该回滚并提示「库存不足」。这是最省事的乐观锁做法,不需要额外加锁字段。

4.2 还书后库存对不上:事务没包住两条 SQL

现象是还书操作偶尔只成功一半,借阅记录状态改成已还了,但库存没加回去。原因是还书逻辑里先更新borrow_record的return_date和status,再更新book的available_count,两条 SQL 没有放在同一个事务里。第一条成功第二条失败,数据就不一致了。

解决方式跟借书一样,用setAutoCommit(false)把两条 SQL 包起来,都成功再 commit。另外还书时要注意判断这条借阅记录是不是已经还过了,如果status已经是 1,就不应该再执行还书操作,否则库存会被多加。

4.3 日期格式报错:Java 的 Date 和 MySQL 的 datetime 对不上

现象是插入借阅记录时抛Data truncation: Incorrect datetime value或者日期差了一天。原因是 Java 里用new java.util.Date()直接塞进PreparedStatement的setDate(),而 MySQL 字段是datetime类型,时区处理不一致。

稳妥做法是统一用java.sql.Timestamp或者直接在 SQL 里用NOW()让数据库生成时间。如果必须在 Java 端处理,用SimpleDateFormat格式化成yyyy-MM-dd HH:mm:ss字符串再插入,同时确保 JDBC URL 里的serverTimezone和数据库实际时区一致。

4.4 分页查询越界:最后一页数据重复或为空

现象是图书列表翻到最后一页时,要么显示上一页的重复数据,要么直接空白。原因是分页 SQL 的LIMIT offset, size里 offset 计算错了。常见错误是offset = pageNum * pageSize,正确应该是offset = (pageNum - 1) * pageSize。页码从 1 开始,第一页 offset 应该是 0。

另外还要处理总页数的边界。如果总记录数是 21 条,每页 10 条,总页数是 3 页,第三页只有 1 条。计算总页数用Math.ceil(total * 1.0 / pageSize),注意要转成浮点数再除,否则整数除法会丢掉小数部分导致页数少一页。

4.5 会话过期后操作报空指针

现象是用户登录后放着不动,过一段时间再点借书,页面报NullPointerException。原因是 session 超时后session.getAttribute("reader")返回 null,代码没做判空就直接调用了 reader 的方法。

解决方式是在每个需要登录的 Servlet 开头加一段检查:从 session 取 reader,如果为 null 就重定向到登录页。更好的做法是写一个LoginFilter,用过滤器统一拦截所有需要登录的请求,避免在每个 Servlet 里重复写检查逻辑。

// LoginFilter 核心逻辑 public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(); Object reader = session.getAttribute("reader"); if (reader == null) { // 未登录,跳回登录页 response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); // 已登录,放行 }

过滤器配置在web.xml里,url-pattern设成/*拦截所有请求,但要在过滤器内部排除登录页和登录接口本身,否则会死循环。

5. 在源码基础上做二次开发:三个能写进简历的改造方向

5.1 加一个借阅排行榜:从 SQL 到页面的完整链路

原始系统只有基础的借还功能,想让它更有亮点,加一个「热门图书排行榜」是最容易上手的改造。核心是一条聚合查询:

SELECT b.title, b.author, COUNT(*) AS borrow_count FROM borrow_record r JOIN book b ON r.book_id = b.id GROUP BY r.book_id ORDER BY borrow_count DESC LIMIT 10;

这条 SQL 按借阅次数倒序取前十本。在 Servlet 里调用这个查询,把结果集转成 List 塞进 request,转发到rank.jsp用 JSTL 的c:forEach渲染成表格。改造量不大,但涉及多表关联、聚合函数、结果排序,面试时能讲清楚这条 SQL 的执行逻辑,比单纯说「我做过一个图书管理系统」有说服力得多。

5.2 逾期提醒:用定时任务扫描借阅记录

另一个实用改造是逾期提醒。在borrow_record表里,due_date已经记录了应还日期,status为 0 表示在借。写一个定时任务,每天扫描一次status = 0 AND due_date < NOW()的记录,把对应的读者和图书信息查出来,在页面上用红色标记显示。

定时任务可以用ServletContextListener配合ScheduledExecutorService实现,在 Web 应用启动时开启一个后台线程,每隔 24 小时执行一次扫描。注意线程要设成守护线程,否则 Tomcat 关闭时任务线程不退出会导致进程挂住。这个改造涉及并发和时间处理,是很好的练手点。

5.3 从 JDBC 到连接池:Druid 替换手写工具类

原始源码里通常是自己写DBUtil用DriverManager.getConnection()获取连接,每次请求都新建和关闭连接,性能很差。改成 Druid 连接池只需要三步:引入 jar 包、写配置文件、改DBUtil的获取连接方法。

# druid.properties driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=Asia/Shanghai username=root password=你的密码 initialSize=5 maxActive=20 maxWait=3000

initialSize是启动时创建的连接数,maxActive是最大连接数,maxWait是获取连接的超时毫秒数。改完之后DBUtil.getConnection()从DruidDataSource拿连接,close()方法变成把连接归还池子而不是真正关闭。这个改造能让你理解连接池的工作原理,也是实际项目里的标配。

我自己的习惯是拿到任何一份源码,先不急着改功能,而是把数据库连接、事务边界、异常处理这三块过一遍。这三块稳了,后面加什么功能都不会出大问题。上面这几个改造方向,挑一个做透,比每个都浅尝辄止强。希望帮到你。

本文还有配套的精品资源,点击获取