从零实现图书管理系统:Servlet+JSP+JDBC+MySQL完整实战指南
2026/9/2 19:28:04 网站建设 项目流程

简介:这是一份完整的 JavaWeb 图书管理系统项目材料,面向正在学习 JSP/Servlet、JDBC、DAO 与 MVC 分层开发的入门开发者,尤其适合课程设计与实训参考。项目通过 HTML、CSS、JavaScript 搭建设计页面,JSP 负责动态处理请求,Servlet 作为控制器调用服务,JavaBean 封装图书字段,并用 DAO 模式统一管理数据库操作,完整展现了 JavaWeb 经典分层开发流程。压缩包共 3 个文件,包含 SQL 数据库脚本、Word 设计报告和源码压缩包,整体约 4.52MB;SQL 脚本可快速还原图书信息表,设计报告覆盖需求分析、系统架构、数据库设计及功能模块实现等内容,可辅助理解项目全貌。目前已有 1.9 万余人学习下载,是 JavaWeb 学习中认可度较高的综合案例。通过这份材料,读者既能掌握项目结构搭建,也能对照源码理解前后端交互、数据库访问与分层解耦的完整实现。

1. 为什么图书管理系统是 JavaWeb 入门的“黄金项目”

如果你在技术社区搜过“JavaWeb 项目完整案例”,十个结果里至少有七八个是图书管理系统。我刚入行那会儿也觉得这题目太老套,直到自己带过几个新人之后才明白,这个项目能一直霸榜是有道理的:它几乎覆盖了 JavaWeb 开发的所有核心环节——前端页面交互、Servlet 控制层编写、JSP 模板渲染、JDBC 操作 MySQL、Session 会话管理、增删改查业务逻辑,一个不落。

更重要的是,图书管理系统的业务模型足够简单,没有电商那种复杂的订单状态机,也不需要像社交产品那样考虑高并发,但它又不是那种“一看就会、一写就废”的纯 Demo。借书、还书、超期计算、库存扣减这些场景,天然就带着事务和状态变更的味道,能让你认真思考“一行数据从页面到数据库,再回到页面,中间到底经历了什么”。

这篇文章我打算从一个完整的项目搭建视角来写,从技术选型、数据库设计、核心功能实现,到前端页面、常见部署坑,尽量把每一步的设计理由和踩坑记录都交代清楚。你如果是正在做课程设计的学生,或者想巩固 Servlet/JSP/JDBC 基础的自学者,这篇文章可以直接当作一份手把手实现指南来参考。

2. 技术选型与整体设计思路

2.1 为什么选 Servlet + JSP + MySQL 而不是 Spring 全家桶

现在搜索 JavaWeb 项目,很多人上来就推荐 Spring Boot + MyBatis Plus,这当然没错,但我不建议零基础的人从框架入手。Servlet 和 JSP 是 JavaWeb 的底层规范,你理解了 HttpServletRequest 是怎么把表单数据带进来的,理解了为什么 JSP 最终会被编译成 Servlet 执行,再去看 Spring MVC 里的 DispatcherServlet 和 @RequestMapping 才有代入感,否则你永远只是在“调 API”,而不是在“写代码”。

图书管理系统用原生 Servlet + JSP 来实现,正好踩中 JavaWeb 学习的黄金区间:能感受到 HTTP 请求响应的完整生命周期,又不会被繁琐的配置淹没。组件版本上我建议统一使用 Servlet 4.0 + JSP 2.3 + JSTL 1.2 + MySQL 8.0,对应 Tomcat 9.x,这一套是兼容性最稳的组合。Tomcat 10 以后 Jakarta EE 改名导致包名从 javax.* 变成 jakarta.*,网上大量老教程的代码直接跑不起来,新手踩这个坑非常打击信心。

数据访问层用原生 JDBC,不要一上来就上 MyBatis。JDBC 那套“注册驱动 -> 获取连接 -> 预编译语句 -> 执行查询 -> 处理结果集 -> 关闭资源”的流程虽然啰嗦,但它把数据库操作的本质全暴露给你了,后面你用任何 ORM 框架都是在封装这套流程。

2.2 三层架构与包结构规划

项目不能把代码全塞在一个包里,我习惯用经典的三层架构来组织,Controller 层负责接收请求和跳转页面,Service 层处理业务逻辑,Dao 层(数据访问层)只做数据库的增删改查。好处是后面如果要换数据库、改业务规则,不用推倒重来。

实际的包结构参考如下:

com.library ├── controller // Servlet 类 ├── service // 业务逻辑接口 + 实现类 ├── dao // 数据访问接口 + 实现类 ├── entity // 实体类(Book, User, BorrowRecord) ├── util // 工具类(DBUtil, StringUtil) └── filter // 过滤器(编码过滤、登录校验过滤)

WebContent 目录下按功能分文件夹:admin 放后台管理页面,user 放读者端页面,static 放 css/js/images,WEB-INF 下放 JSP 模板。强调一下,WEB-INF 目录下的页面不能通过浏览器地址栏直接访问,只能通过 Servlet 服务端转发进入,这个特性可以用来做登录拦截,防止用户绕过登录直接访问后台页面。

三层结构写起来会显得“类很多”,但这是好事。图书管理系统的业务复杂度不高,如果你在这个阶段就习惯按职责拆分代码,后面遇到订单、支付、库存这类复杂业务时,才不会把数据库操作混在页面跳转逻辑里,搞得一团糟。

2.3 开发工具与环境准备

开发工具我推荐 IntelliJ IDEA,社区版就够用,不需要破解旗舰版。如果电脑配置一般,也可以用 Eclipse IDE for Enterprise Java and Web Developers,注意别下成普通 Java SE 版本,否则没有 Dynamic Web Project 选项。这里针对热词里的“vscode 创建 javaweb 项目”多说一句:VSCode 可以通过安装 Extension Pack for Java + Tomcat for Java 插件来开发 JavaWeb 项目,但不建议新手用,因为 VSCode 对 Web 项目结构、部署路径、依赖管理的支持都是“能用但不顺手”,出了问题排查成本很高。

环境版本对照表我整理了一下:

组件推荐版本说明
JDK1.8 或 11不要用 17+,部分老依赖和 Tomcat 9 不兼容
Tomcat9.0.x对应 javax.* 命名空间
MySQL8.0.x5.7 也可以,注意驱动和连接 URL 差异
mysql-connector-java8.0.x驱动类名是 com.mysql.cj.jdbc.Driver
IDEA2023+ 社区版或 Eclipse for JavaEE
Maven3.8+强烈建议用 Maven 管理 jar 包

为什么强烈建议 Maven?因为传统方式是把 jar 包手动复制到 WebContent/WEB-INF/lib 目录里,一旦依赖多了,版本冲突、漏包问题能把人折磨疯。Maven 在 pom.xml 里声明依赖后自动下载统一管理,这不仅是工程化习惯,也是后面入职项目的基本要求。

3. 数据库设计与核心表结构

3.1 四张核心表的关系梳理

图书管理系统的数据库设计,核心在于搞清楚“书”和“读者”之间是多对多的借阅关系。至少需要四张表:用户表(包含管理员和普通读者)、图书表、图书分类表、借阅记录表。下面把每张表的字段设计关键点讲透。

用户表设计:

CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, `real_name` VARCHAR(50), `role` TINYINT DEFAULT 0 COMMENT '0-普通用户 1-管理员', `phone` VARCHAR(20), `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个设计要点,我逐个说。username 一定要加唯一约束,这是业务上的硬要求,同时唯一索引也能加速登录查询。password 字段长度至少 100,因为你大概率会存 MD5 加密后的 32 位字符串,如果未来想升级成 BCrypt,长度不够就得改表结构。role 字段用 TINYINT 而不是字符串,省空间且判断效率高。

图书表的核心字段要特别注意一个容易忽略的点:总库存 total_stock 和当前可借数量 available_stock 要分开设计,不能只用一个库存字段。每次借书扣减 available_stock,还书时加回来,total_stock 保持不变用于统计总馆藏。如果只用一个字段,你得在借阅记录里去倒推“这本书到底有多少本”,完全没法做。

3.2 借阅记录表的状态设计与外键策略

借阅记录是系统里业务逻辑最复杂的表,因为它关联用户和图书两张表,同时还要记录借书时间、应还时间、实际归还时间、状态等关键信息。

CREATE TABLE `borrow_record` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `book_id` INT NOT NULL, `borrow_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `due_time` DATETIME NOT NULL, `return_time` DATETIME DEFAULT NULL, `status` TINYINT DEFAULT 0 COMMENT '0-借出 1-已还 2-超期未还', CONSTRAINT fk_borrow_user FOREIGN KEY (user_id) REFERENCES `user`(id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

status 字段我用了 0/1/2 三种状态:0 表示借出未还,1 表示已归还,2 表示超期未还。这里有个很容易犯的错误:很多人把“超期”设计成一个单独的表或者单独的字段,其实超期完全可以通过计算 due_time 和当前时间的差值得到,不需要真实存储。但保留一个状态值可以简化查询,比如首页统计“有多少本超期未还”,直接WHERE status = 2就能查出来,不用每次都在代码里做日期比对。

外键到底加不加,网上争议很大。图书管理系统这种规模,我建议加外键约束,因为它能保证数据完整性——比如你不能删除一本仍有借阅记录的图书。但要注意,加了外键之后删除顺序有讲究:先删借阅记录,再删图书,否则会报外键约束错误。生产环境大表不加外键是因为性能和高并发下锁竞争的问题,小项目根本遇不到这个瓶颈,别被“高并发经验”误导。

3.3 分类表与初始化数据

图书分类表结构简单,id、name、description 三个字段就够。但要注意插入初始化数据时,必须把默认管理员账号一起加进去,否则系统启动后你连后台都进不去。

INSERT INTO `user` (username, password, real_name, role) VALUES ('admin', MD5('admin123'), '系统管理员', 1);

一个小提示:JDBC 连接 MySQL 8.0 时,URL 需要加上 useSSL=false 和 serverTimezone=Asia/Shanghai,否则会报 SSL 连接警告和时区错误。这个不是代码问题,是 MySQL 8.0 的配置变更,遇到别慌。

4. 核心功能模块的设计与实现

4.1 登录与会话管理:Cookie/Session 的实战理解

登录模块是所有 JavaWeb 项目都躲不开的第一关,但别看它简单,里面值得讲的细节不少。登录流程是:前端表单提交用户名密码 -> 后端接收并做非空校验 -> 查库匹配 -> 成功后把用户信息放进 Session -> 跳转首页。

密码加密存储是必须的,明文存储一旦数据库泄露,所有用户账号全裸奔。图书管理系统我用 MD5 加密就够,因为它的安全级别不像支付系统那么高,MD5 虽然理论上可碰撞,但在这个场景下足够了。如果以后做商业项目,至少要用 SHA-256 加盐或者 BCrypt。

登录成功后不要把密码放进 Session,只存用户 id、用户名、角色这几个字段。Session 默认 30 分钟过期,这个时间可以在 web.xml 里通过 session-config 配置。如果用户选了“记住我”,可以用 Cookie 存一个加密后的用户名标识,但不要存明文密码,这是底线。

登录拦截用 Filter 实现,在一个自定义的 LoginFilter 里检查 Session 中是否包含登录标记,没有就重定向到登录页,有就放行。注意要放行登录页本身和静态资源,否则会出现“明明点了登录,却一直被弹回登录页”的诡异问题。

4.2 图书管理:列表分页与条件检索的组合查询

图书列表页是系统的门面,功能诉求是“能看、能搜、能翻页”。分页查询是这里的技术难点,很多新手一开始是把所有图书查出来,然后用 Java 代码在内存里 slice,图书量小的时候感觉不到问题,量一上来页面会越翻越卡。

正确的做法是在 SQL 层分页,MySQL 的 LIMIT 语法:

String sql = "SELECT * FROM book WHERE name LIKE ? LIMIT ?, ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, "%" + keyword + "%"); ps.setInt(2, (currentPage - 1) * pageSize); ps.setInt(3, pageSize);

分页的核心参数是 currentPage(当前页码)和 pageSize(每页条数),数据库偏移量 = (currentPage - 1) * pageSize。举个例子:第 1 页每页 5 条,偏移量是 0,查询前 5 条;第 2 页偏移量是 5,从第 6 条开始取 5 条。这套计算公式所有分页组件都绕不开。

模糊查询用 LIKE 配合 %keyword%,注意两个细节:一是 SQL 注入问题,必须用 PreparedStatement 预编译绑定参数,禁止字符串拼接 SQL;二是 LIKE 查询性能问题,%keyword% 这种写法不会走索引,但图书表几千条数据无需担心,等到了几十万条再考虑全文索引。

分页查询还隐含两个必要的统计查询:总记录数 totalCount 和总页数 totalPages。totalCount 用 COUNT(*) 获取,totalPages 的计算公式是:

int totalPages = (totalCount % pageSize == 0) ? (totalCount / pageSize) : (totalCount / pageSize + 1);

这个公式用“是否能整除”来做向上取整,比 Math.ceil 更直观,也避免浮点数精度问题。

4.3 借书还书:事务处理的状态流转

借书和还书是系统性最强的一块,因为涉及两张表的数据变更:借书要更新借阅记录表,同时扣减图书表的 available_stock;还书要更新借阅记录的 status/return_time,同时加回库存。这两步操作必须在一个数据库事务里完成,否则借书成功但库存没减,会造成数据不一致。

JDBC 事务的标准写法是:

Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 1. 插入借阅记录 // 2. 扣减图书库存 // 3. 更新图书状态 conn.commit(); // 全部成功,提交事务 } catch (SQLException e) { conn.rollback(); // 任何一步失败,回滚所有操作 throw e; } finally { conn.setAutoCommit(true); conn.close(); }

这段代码每次看起来都很“例行公事”,但它是事务的完整骨架:关闭自动提交 -> 执行多条 SQL -> 全部成功 commit -> 任一失败 rollback -> 恢复自动提交并关闭连接。

借书时还要做几个前置校验:这本书是否存在、当前可借库存是否大于 0、该读者是否已经借了同一本书且未归还。第三个校验很多人漏掉,结果就是读者能无限次借同一本书,库存被疯狂扣减。校验的 SQL 就是在 borrow_record 里查是否存在 user_id + book_id + status=0 的记录。

还书操作比较简单:根据借阅记录 id 把 status 更新为 1,set return_time = NOW(),同时把图书的 available_stock + 1。超期判断不用单独处理,JSP 页面展示时比较 due_time 和当前时间,超了就显示红色“已超期”,管理员可以按状态 2 筛选超期记录。

4.4 JSP 页面渲染与 JSTL 标签的使用

JSP 页面不建议直接在 HTML 里写 <% %> 脚本片段,那样一方面页面代码臃肿,另一方面项目大了很难维护。正确做法是 JSP + EL 表达式 + JSTL 标签三件套,EL 负责取值,JSTL 负责逻辑控制,HTML 负责布局展示。

举个例子,图书列表页的核心渲染部分:

<c:forEach items="${pageBean.list}" var="book"> <tr> <td>${book.id}</td> <td>${book.name}</td> <td>${book.author}</td> <td>${book.availableStock}</td> <td> <c:if test="${book.availableStock > 0}"> <a href="borrowBook?bookId=${book.id}">借阅</a> </c:if> <c:if test="${book.availableStock <= 0}"> <span class="text-muted">已借完</span> </c:if> </td> </tr> </c:forEach>

注意 c:if 标签没有 else 分支,需要反向条件用第二个 c:if 实现。这是 JSTL 和普通编程语言的差异点,习惯了 else 的开发者容易踩这个坑。另外,EL 表达式${book.name}实际上调用的是 Book 对象的 getName() 方法,所以实体类的属性必须有规范 getter/setter。

5. 前端交互与页面细节

5.1 用 AJAX 实现借阅操作局部刷新

图书列表页如果每次点击“借阅”都整页刷新,体验非常生硬,而且翻页后当前位置还会丢失。我在这个项目里给借阅操作加了 AJAX 异步请求,返回结果是 JSON 格式,前端根据返回的 code 判断成功失败,弹出提示框后用 JS 局部更新那一行的库存数据。

Servlet 返回 JSON 的简化写法:

response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); out.print("{\"code\":0,\"msg\":\"借阅成功\"}");

前端用原生 JS 的 fetch 就够了,不需要引入 axios:

fetch('borrowBook?bookId=' + bookId) .then(resp => resp.json()) .then(data => { if (data.code === 0) { alert(data.msg); location.reload(); } else { alert(data.msg); } });

这里有个小坑:Servlet 输出 JSON 必须设置 application/json 的 ContentType,并指定 UTF-8 编码。如果漏了 charset,浏览器解析中文返回信息会乱码。另外,AJAX 请求同样会被 LoginFilter 拦截,未登录时返回的是重定向的 HTML 页面而不是 JSON,前端解析会报错。解决方法是前端先判断响应头或内容类型,或者在后端 Filter 里针对 AJAX 请求返回 JSON 的未登录提示。

5.2 Bootstrap 快速构建后台界面

图书管理系统不需要你从零写 CSS。我推荐用 Bootstrap 4.x,直接用它现成的表格、按钮、表单、导航栏组件,半小时就能拼出一个干净的后台界面。虽然 Bootstrap 已经出到 5.x,但 4.x 的中文教程最多,遇到问题排查方便。

界面布局上,后台管理页面用“左侧导航栏 + 右侧内容区”的经典结构,左侧放“图书管理、借阅管理、分类管理、用户管理”等入口,右侧根据菜单动态展示对应内容。顶部导航栏放系统名称、当前登录用户、退出登录按钮。

注意 JSP 页面里引入 Bootstrap 有两种方式:本地下载到 static 目录,或者用 CDN 链接。课程设计交作业场景建议本地引入,因为评委老师现场演示时可能没网,CDN 加载不出来整个页面就“裸奔”了,影响观感。

5.3 表单校验:前端体验与后端安全的配合

表单校验是前后端各做一层的前端校验可以极端一点:字段必填、邮箱格式、手机号位数这些用 HTML5 的 required 和 pattern 属性就能实现,错误信息即时反馈,提升用户体验。但前端校验随时可以被绕过,真正承重的校验必须在后端做。

以新增图书为例,后端 Service 层至少要校验:书名不能为空、ISBN 不能重复、出版日期格式必须正确。在 Servlet 里把校验结果放进 request 域,然后转发回新增页面回显错误信息:

if (bookName == null || bookName.trim().isEmpty()) { request.setAttribute("error", "书名不能为空"); request.getRequestDispatcher("/admin/bookAdd.jsp").forward(request, response); return; }

前端校验更像是“防呆”,后端校验才是“保安”。两者职责不同,不能互相替代。

6. 部署调试与常见问题排查

6.1 用 vscode/IDEA 配置 Tomcat 的完整步骤

IDEA 社区版不像旗舰版自带 Tomcat 集成,但完全可以通过 Smart Tomcat 插件来运行项目。装好插件后配置一次:把 Tomcat 安装路径指到你的 Tomcat 9 目录,设置 Deployment 指向当前 Web 项目,Context Path 填 /library,就能在 IDEA 里一键启动 Tomcat 了。不用 Maven 的 Web 项目,Community 版还需要把依赖 jar 放进 WEB-INF/lib,这个和 Eclipse 里的做法一样。

如果你坚持用 VSCode,流程是:安装 Tomcat for Java 插件 -> 在 VSCode 侧边栏找到 Tomcat Servers 面板 -> 添加本地 Tomcat 路径 -> 右键项目选择 Run on Tomcat。但这套方案在项目结构不对时会频繁报 404 或类找不到,排查起来比 IDEA 麻烦不少,还是建议优先用 IDEA。

启动成功后在浏览器访问http://localhost:8080/library/,默认端口 8080 如果被占用,去 Tomcat 的 conf/server.xml 里改 Connector 的 port 属性,改成 8081 或 9090 都可以。

6.2 高频问题排查速查表

根据我多年的经验,JavaWeb 项目跑不起来的坑,90% 集中在那几个点上。我整理成速查表,直接对着排查,比自己瞎猜快得多:

问题现象可能原因解决方案
启动 Tomcat 报端口被占用8080 被其他程序占用修改 server.xml 端口或杀掉占用进程
JDBC 连接报 ClassNotFoundExceptionmysql 驱动 jar 没放到 WEB-INF/lib检查 jar 包是否编译进项目,用 Maven 的检查 dependency
中文乱码请求/响应编码不一致统一设置 request.setCharacterEncoding("utf-8") 和 response.setContentType("text/html;charset=utf-8")
页面 404Servlet 没配置 @WebServlet 或没配置 web.xml 映射检查注解路径和表单 action 路径是否一致
列表页不显示数据数据库连接 URL 写错或表名大小写问题检查 URL 参数,MySQL 在 Linux 下表名区分大小写
数据库中文存进去乱码数据库连接 URL 没指定编码URL 加 useUnicode=true&characterEncoding=utf8
修改代码后不生效JSP 文件被服务器缓存Clean Tomcat 缓存或重启服务

6.3 我踩过印象最深的三个坑

第一个坑是 MySQL 8.0 驱动类名变化。5.x 版本用 com.mysql.jdbc.Driver,8.0 改成了 com.mysql.cj.jdbc.Driver,如果沿用老驱动类名会直接 ClassNotFoundException。这个错误信息很直接,但新手可能懵很久,因为代码看着没问题。

第二个坑是 filter 拦截了静态资源。我一开始写 LoginFilter 时没排除 css/js/image 路径,结果登录页面加载出来纯文本,样式全丢了,排查半天发现是 Filter 的 url-pattern 写了 /*,把静态资源的请求也拦截重定向了。Filter 里要对静态资源路径做白名单判断,或者把静态资源放在单独路径下针对性排除。

第三个坑是 JSP 修改不生效。Tomcat 默认对 JSP 有热部署机制,但有时因为本地缓存或者编译目录残留,改动就是不刷新。遇到这种情况,把 Tomcat 的 work/Catalina 目录删掉重启,一切就正常了。这个操作等同于“重启大法”,但胜过花半小时怀疑人生。

7. 项目扩展方向与个人实操感受

图书管理系统完成基础功能之后,我建议你别急着“毕业”,顺着下面几个方向扩展一下,项目含金量会明显提升:

  • 借阅超期自动计算:用 Quartz 定时任务每天刷新超期状态,这是从“手动操作”到“系统自动化”的质变。
  • 借阅排行榜:写一条带 GROUP BY 和 COUNT 的 SQL,统计热门书籍 Top10,这是 SQL 进阶训练。
  • 验证码登录:用 Kaptcha 组件生成图片验证码,理解 Session 在“防止机器操作”场景的另一个用途。
  • 读者借阅历史详情页:把列表和详情联动起来,练一下主从表查询和页面数据回显。
  • 接口化改造:把 Servlet 改造成返回 JSON 的 REST 风格接口,配合 Vue 前端重写一遍,那基本就是企业级前后端分离的思路了。

我在带新人做这类项目时的个人体会是:图书管理系统最大的价值不在于它本身有多新潮,而在于它把 JavaWeb 开发的核心知识编织成了一个完整的闭环。你写登录模块时会理解会话保持,写借阅模块时会理解事务边界,写列表页时会理解分页和条件查询,这些东西在框架和微服务大行其道的今天,依然是底层的地基。地基打得越扎实,后面学 Spring Boot、MyBatis Plus 甚至微服务,都只是在这个闭环上做工程化封装而已。

最后分享一个小建议:做这个项目时,把每一段代码都当“作品”而不是“作业”来写,每一步都问一句为什么这么写。做完之后把心得总结成博客发出来,你会发现自己对这个项目的理解会再上一个台阶。

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

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

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

立即咨询