Servlet+JSP图书管理系统课设全解析:从技术选型到避坑实战
发布时间:2026/9/9 23:44:06 作者:尧图编辑部 阅读量:1,286

简介基于ServletJSP的图书管理系统是一套完整的Java Web课程设计/毕业设计参考项目面向正在学习Java服务端开发的学生与开发者演示图书信息增删改查、借还管理及前端交互等典型业务场景。包内共419个文件约14.91MB涵盖Java源码、JSP页面、Servlet类、CSS/JS/jQuery前端脚本、SQL脚本、XML配置、JAR依赖库及PDF说明文档结构清晰便于对照学习。已有2216人浏览学习项目集成IntelliJ IDEA与Tomcat环境readME中提供数据库初始化与部署指引。通过阅读源码可重点掌握Servlet请求处理流程、JSPELJSTL页面渲染、jQuery实现Ajax异步刷新等核心知识点同时包内还包含运行截图与数据库建表语句适合作为课程设计参考或入门Java Web开发的练手实例。1. 项目整体设计与技术选型思路1.1 为什么在 2025 年还选 Servlet JSP 做图书管理系统先别急着说“这技术栈太老了”。图书管理系统这个题目在高校课程设计和毕业设计里出现的频率高到几乎可以称之为“Java Web 入门必修课”。我见过太多人一上来就选 Spring Boot Vue MySQL 全家桶结果评审老师一问Servlet生命周期答不上来一问Session底层原理直接卡壳——这其实就失去了做课设最重要的意义。Servlet JSP 这套组合本质上是 Java Web 最底层的“裸奔”方案。它没有 Spring MVC 帮你封装请求路由没有 MyBatis 帮你屏蔽 JDBC 细节更没有前端框架帮你自动渲染数据。你写的每一个doGet、doPost、每一条PreparedStatement都是在直面 HTTP 协议和数据库交互的本质。把这套东西吃透了后面再学 Spring Boot 那些“高级封装”你会发现自己是在看懂了电路图之后再去用电器而不是只会按开关。那为什么偏偏是图书管理系统因为这个业务场景足够经典用户登录注册、图书信息的增删改查、借阅还书流程、分类管理这几个模块恰好覆盖了 Java Web 开发几乎所有的核心知识点请求响应模型前端页面提交表单后端接收参数并响应状态管理登录状态用 Session 保存记住用户信息用 CookieJDBC 操作连接数据库、执行 SQL、处理结果集MVC 思想JSP 做视图Servlet 做控制器JavaBean / DAO 做模型换句话说你做完这一个系统相当于把 Java Web 的基础能力整体过了一遍。这也是为什么即使 Spring Boot 已经火到不行高校课设题库里依然给 Servlet JSP 留着一席之地的原因。1.2 技术栈选型与配套工具的搭配建议既然确定了 Servlet JSP 作为核心技术栈那周边工具和环境的搭配就有讲究了。我结合自己带过项目的经验给出一套比较稳的推荐配置组件推荐选型说明JDK1.8 或 11别追求太新的版本Tomcat 兼容性和课设验收环境是个变量IDEEclipse 或 IDEAIDEA 社区版够用但传统课设环境 Eclipse 更“原汁原味”服务器Tomcat 8.5 / 9.0经典配置网上资料最多出问题好排查数据库MySQL 5.7 / 8.05.7 更稳8.0 需要注意驱动包差异前端JSP JSTL EL 表达式传统搭配JSTL 能省掉大量 Java 脚本片段JDBC 驱动mysql-connector-java 5.1.49 / 8.0.x版本必须和数据库版本匹配这是最大的坑之一这里面有个细节我要单独拎出来说数据库驱动版本不匹配是新手碰到最多的拦路虎。如果你用 MySQL 8.0驱动类名是com.mysql.cj.jdbc.DriverURL 还需要带serverTimezoneAsia/Shanghai这样的时区参数如果你用 MySQL 5.7驱动类名是com.mysql.jdbc.DriverURL 不需要时区参数也能跑。这两套配置放在一起就够新手折腾一晚上的。1.3 功能模块划分边界清晰评分点全覆盖图书管理系统的功能模块往小了做就是“登录 一张表的增删改查”往大了做可以扯到“多角色权限 借阅逾期自动扣费 数据可视化大屏”。作为课设或毕设我的建议是做到“够用且有余量”——基础功能必须完整再加 1-2 个亮点功能做加分项。我常用的模块划分方式是用户模块注册、登录、注销、密码修改。注意区分管理员和普通用户角色图书管理模块图书信息的添加、删除、修改、查询。查询要支持模糊搜索这是最基本的分类管理模块图书分类的增删改查一本图书归属一个分类借阅模块借书、还书、借阅记录查询、借阅状态管理统计模块加分项图书总数、借出数量、热门图书排行这套划分方式的好处是每个模块的职责清晰代码量适中评审老师问起业务逻辑来你能对答如流。更重要的是模块之间的边界清晰意味着你写代码的时候不容易“拧成一团浆糊”。2. 核心细节解析与实操要点2.1 Servlet 的三种注册方式强烈建议掌握 web.xml 写法关于 Servlet 的注册方式现在网上主流推荐的是WebServlet注解确实简洁方便。但我要说的是如果你做的是课设建议掌握传统 web.xml 的写法。原因很简单——很多学校的上机验收环境版本偏老而且《Java Web》教材里讲 Servlet 部分时基本都是以 web.xml 为主你答辩的时候要能讲清楚两种方式的区别。这里给出一个基于 web.xml 方式编写 Servlet 的完整示例这是最能体现功底的写法?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-name图书管理系统/display-name !-- 配置 Servlet -- servlet servlet-nameBookServlet/servlet-name servlet-classcom.library.servlet.BookServlet/servlet-class !-- 可选启动时加载数字越小越优先 -- load-on-startup1/load-on-startup /servlet !-- 配置 Servlet 映射 -- servlet-mapping servlet-nameBookServlet/servlet-name url-pattern/book/url-pattern /servlet-mapping !-- 配置欢迎页 -- welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list !-- 配置全局编码过滤器解决中文乱码 -- filter filter-nameCharacterEncodingFilter/filter-name filter-classcom.library.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping /web-app这段配置里有两个点值得注意。第一是load-on-startup它决定 Servlet 是“第一次被请求时才加载”还是“服务器启动时就加载”。对于核心 Servlet 来说配置成启动加载可以避免首次访问时的延迟也有利于提前暴露配置问题。第二是编码过滤器这个我在后面专门讲中文乱码问题时会详细展开。2.2 JSP 页面设计EL 表达式 JSTL 才是正确姿势很多自学 JSP 的人最大的毛病就是在 JSP 页面里写大量的% ... %Java 脚本片段。这种写法虽然也能跑但页面会变得极其混乱而且容易引起评审老师的反感——因为这恰恰是 JSP 技术最被诟病的点。正确做法是用 EL 表达式${}和 JSTL 标签库来渲染页面。比如展示图书列表的核心代码是这样% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title图书列表/title link relstylesheet hrefcss/style.css /head body h2图书列表/h2 div classtoolbar form action${pageContext.request.contextPath}/book methodget input typehidden nameaction valuesearch input typetext namekeyword placeholder输入书名或作者 button typesubmit搜索/button /form a href${pageContext.request.contextPath}/book?actiontoAdd新增图书/a /div table border1 cellpadding8 cellspacing0 tr th编号/th th书名/th th作者/th th分类/th th价格/th th库存/th th操作/th /tr c:forEach items${bookList} varbook varStatusstatus tr td${status.count}/td td${book.title}/td td${book.author}/td td${book.categoryName}/td td${book.price}/td td${book.stock}/td td a href${pageContext.request.contextPath}/book?actiontoEditid${book.id}编辑/a a href${pageContext.request.contextPath}/book?actiondeleteid${book.id} onclickreturn confirm(确定要删除该图书吗)删除/a /td /tr /c:forEach /table c:if test${empty bookList} p stylecolor: gray;暂无数据/p /c:if /body /html${pageContext.request.contextPath}这个表达式建议背下来它取的是当前 Web 应用上下文路径这么写之后不管项目部署到哪个路径下链接都不会失效。这个细节在答辩时随口说出来印象分会加不少。2.3 Servlet 代码结构用一个 Servlet 处理一类业务Servlet 虽然可以直接在doGet或doPost里写逻辑但一个类只处理一件事会让代码更清晰。我在图书管理系统的 Servlet 里习惯用action参数来区分不同操作WebServlet(/book) public class BookServlet extends HttpServlet { private BookService bookService new BookService(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (list.equals(action)) { listBooks(request, response); } else if (toAdd.equals(action)) { request.getRequestDispatcher(/book_add.jsp).forward(request, response); } else if (toEdit.equals(action)) { String id request.getParameter(id); Book book bookService.getById(Integer.parseInt(id)); request.setAttribute(book, book); request.getRequestDispatcher(/book_edit.jsp).forward(request, response); } else if (delete.equals(action)) { deleteBook(request, response); } else { listBooks(request, response); } } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String action request.getParameter(action); if (add.equals(action)) { addBook(request, response); } else if (update.equals(action)) { updateBook(request, response); } } private void listBooks(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ListBook bookList bookService.getAllBooks(); request.setAttribute(bookList, bookList); request.getRequestDispatcher(/book_list.jsp).forward(request, response); } }这种action分发的好处是一个/book地址就能处理所有图书相关操作URL 简洁代码结构清晰。你只需要在 Servlet 里定义好action的枚举值前端页面通过这些参数来指定要执行的操作。不过要注意用这种方式时一定要对action为空的情况做兜底处理否则容易 NullPointerException。2.4 数据库设计五张表打天下数据库设计是整个系统的地基。图书管理系统的表结构我一般推荐五张表起步-- 用户表 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 VARCHAR(20) DEFAULT USER, -- ADMIN 或 USER create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 图书分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, description VARCHAR(200) ); -- 图书表 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), price DECIMAL(10,2), stock INT DEFAULT 0, category_id INT, publish_date DATE, isbn VARCHAR(50), cover_url VARCHAR(255), FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE SET NULL ); -- 借阅记录表 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, borrow_date DATETIME DEFAULT CURRENT_TIMESTAMP, return_date DATETIME, status VARCHAR(20) DEFAULT BORROWED, -- BORROWED / RETURNED / OVERDUE FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (book_id) REFERENCES book(id) );这里有个需要说明的外键策略图书表外键关联分类表时用ON DELETE SET NULL意思是分类删除后图书仍然保留只是分类置空。这样设计的好处是避免“删一个分类把所有书都删没了”这种惨案。这个细节值得在答辩时主动提说明你考虑到了数据完整性。3. 实操过程与核心环节实现3.1 项目搭建全流程从零开始跑通第一个页面完整的实操流程按这个顺序走基本不会卡壳创建数据库在 MySQL 中执行上述建表 SQL并插入几条测试数据新建动态 Web 项目IDEA 里选 Java Enterprise勾选 Web ApplicationEclipse 里选 Dynamic Web Project导入依赖 jar 包mysql-connector-java和jstl相关 jar 包放进WEB-INF/lib目录编写实体类User、Book、Category、BorrowRecord对应数据库表结构编写数据库工具类封装获取连接和释放资源的代码编写 DAO 层每个实体对应一个 DAO封装增删改查方法编写 Service 层业务逻辑比如借书时要判断库存是否充足编写 Servlet接收请求、调用 Service、转发到 JSP编写 JSP 页面列表页、编辑页、登录页、注册页配置 web.xml配置 Servlet 映射、编码过滤器部署到 Tomcat 测试3.2 JDBC 工具类封装最容易被忽略的核心基建JDBC 代码写得不优雅后面每个 DAO 都会重复大量样板代码。我封装工具类时习惯把连接获取和资源释放放在一起public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/library_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingUTF-8; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps ! null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这里有个非常关键的细节数据库 URL 里的characterEncodingUTF-8要和 Servlet 里的request.setCharacterEncoding(UTF-8)配合使用。这两处只要有一处缺失中文数据在存取过程中就会出现乱码。另外URL 中的useSSLfalse是为了避免 MySQL 8.0 在本地连接时弹出 SSL 警告属于实践中的常规操作。3.3 登录功能的完整实现涉及 Session 与拦截器思想登录是图书管理系统的门面功能也是状态管理的核心考点。流程上用户提交用户名密码后Servlet 查询数据库验证成功后把用户信息放入 Session再重定向到主页面失败则携带错误信息返回登录页。关键代码片段如下protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); User user userService.login(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(currentUser, user); response.sendRedirect(request.getContextPath() /book?actionlist); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } }提示登录成功后应使用sendRedirect重定向而不是forward转发。原因是重定向会发起新请求地址栏会变化用户刷新页面不会重复提交表单而直接转发的话刷新页面会出现重复提交表单的弹窗。至于“未登录用户不能访问系统页面”这种需求最朴素的做法是在需要保护的 Servlet 方法开头加 Session 判断HttpSession session request.getSession(); User currentUser (User) session.getAttribute(currentUser); if (currentUser null) { response.sendRedirect(request.getContextPath() /login.jsp); return; }当然如果你愿意写 Filter 来实现统一的登录验证会更优雅但课设阶段先在 Servlet 里做判断也完全够用量力而行。3.4 借书还书流程业务状态机最关键借书还书是这个系统里最有业务味道的功能。借书时系统要检查三件事图书是否存在、库存是否大于 0、用户是否已经借了这本书还没还。还书时要把库存加回去同时更新借阅记录的状态。用状态机来理解就是可借 —(借书)— 已借出 —(还书)— 已归还。这三个状态在数据库里就对应status字段的三种取值。实现时要注意一个细节借书和还书时都需要同时更新图书表的stock字段和借阅记录表的status字段这两个更新必须放在同一个事务里否则会出现“书借出去了但库存没减”的不一致问题。这里我给一个简单的事务控制模板public boolean borrowBook(int userId, int bookId) { Connection conn null; PreparedStatement ps1 null; PreparedStatement ps2 null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 操作一扣减库存 String sql1 UPDATE book SET stock stock - 1 WHERE id ? AND stock 0; ps1 conn.prepareStatement(sql1); ps1.setInt(1, bookId); int rows ps1.executeUpdate(); if (rows 0) { conn.rollback(); return false; // 库存不足或图书不存在 } // 操作二插入借阅记录 String sql2 INSERT INTO borrow_record(user_id, book_id, status) VALUES(?, ?, BORROWED); ps2 conn.prepareStatement(sql2); ps2.setInt(1, userId); ps2.setInt(2, bookId); ps2.executeUpdate(); conn.commit(); return true; } catch (Exception e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { DBUtil.close(conn, ps1, null); DBUtil.close(null, ps2, null); } }这个stock stock - 1 WHERE id ? AND stock 0的写法比“先查库存再更新”的方式好在哪它把“判断”和“更新”合并成了一条原子性操作。即使多个请求同时借同一本书数据库也能通过行锁保证不会出现超借的情况。这种写法体现了对并发场景的考虑和那些先查询再更新的做法拉开了一个档次。4. 常见问题与排查技巧实录4.1 中文乱码问题一次解决再也不用怕中文乱码是 Java Web 开发里出现频率最高的问题没有之一。很多人的处理方式很粗暴——各种地方乱设编码结果还是乱最后干脆放弃。乱码的本质是数据的编码和解码方式不一致。在图书管理系统里乱码可能出现在三个环节环节原因解决办法请求参数乱码浏览器提交数据时用 UTF-8但 Servlet 默认用 ISO-8859-1 解码request.setCharacterEncoding(UTF-8)必须在获取任何参数之前调用响应页面乱码JSP 页面编码和服务器输出编码不一致JSP 文件头部加% page contentTypetext/html;charsetUTF-8 %数据库存储乱码MySQL 连接 URL 未指定字符集JDBC URL 加characterEncodingUTF-8实用的检查手段是先确定“乱的是哪一层”。我的排查顺序是先在浏览器里右键查看源码看页面本身是否乱如果页面正常、数据乱那就是数据库连接或数据库表字符集的问题如果页面本身乱那就是 JSP 文件编码或响应头的问题。逐层排查不乱冲乱撞。4.2 JSP 修改了不生效八成是部署目录的问题“JSP 改完刷新页面还是旧的”——这个问题踩过的人都懂。大部分情况下是因为 IDE 没有自动把修改后的 JSP 文件同步到 Tomcat 的部署目录。IDEA 里表现为out/artifacts目录下的文件没更新Eclipse 里则是wtpwebapps目录下的文件没更新。最直接的解决办法是重新部署项目。在 IDEA 里选中 Tomcat 配置点“Redeploy”或直接重启 Tomcat。如果修改很频繁可以考虑开启 Tomcat 的自动加载功能在conf/context.xml里设置Context reloadabletrue这样 JSP 文件变化时会自动重编译加载。但要注意生产环境千万别开这个性能和安全性都会受影响。另外还有一种情况项目里存在多个同名 JSP 文件一个在src/main/webapp下另一个在web目录下IDE 部署的是其中某一个你改的是另一个。这属于项目结构混乱导致的问题建议一开始就统一好目录结构别在 IDEA 里同时乱建多个 Web 根目录。4.3 数据库连接失败驱动版本和时区是高频原因数据库连接失败报错信息通常有ClassNotFoundException、Communications link failure、Connection refused等。逐一来分析ClassNotFoundException是驱动 jar 包没导入或被漏掉了。检查WEB-INF/lib目录下有没有对应的mysql-connector-javajar 包如果 IDEA 项目里没有将 jar 包设置成 “Add as Library”运行时会找不到类。Communications link failure通常是数据库没启动、端口被占用、或防火墙拦截。先用命令行mysql -u root -p确认数据库能本地连接再看端口是不是 3306最后检查 MySQL 服务是否在运行。The server time zone value is unrecognized是 MySQL 8.0 特有的坑。解决办法是在 JDBC URL 里加serverTimezoneAsia/Shanghai。另外驱动类名也需要注意——MySQL 8.0 对应的驱动类是com.mysql.cj.jdbc.DriverMySQL 5.x 是com.mysql.jdbc.Driver写错也会报类找不到。4.4 端口被占用Tomcat 启动失败的标准处理姿势启动 Tomcat 时报Port 8080 was already in use意思就是 8080 端口被别的程序占了。最简单的处理方式有三种换端口修改conf/server.xml里的端口号比如改成 8081找到占用进程并结束它Windows 下用netstat -ano | findstr 8080查 PID然后taskkill /F /PID 进程号如果是你自己之前启动的 Tomcat 没关干净去任务管理器里把 Java 进程结束掉注意改端口时要同时改 HTTP 端口和可能用到的 AJP 端口只改一个会导致另一个端口依旧冲突。另外改完端口后访问路径也要跟着变比如原来是http://localhost:8080/book改成 8081 后就要访问http://localhost:8081/book。4.5 登录状态失效Session 超时和浏览器 Cookie 的关系登录后过一段时间再操作用户被弹回登录页这是因为 Session 默认超时时间是 30 分钟。Tomcat 的web.xml里可以配置全局 Session 超时时间session-config session-timeout60/session-timeout /session-config单位是分钟这里配置成 60 分钟。还有一种情况是“换了个浏览器标签一上来就变成未登录”这通常是因为 Tomcat 的 Session 依赖 Cookie而浏览器设置禁用了 Cookie。课设验收时如果遇到这个问题建议直接检查浏览器是否允许了该站点的 Cookie。最后再分享一个容易被坑的小细节如果我需要管理员和普通用户显示不同的菜单位可以在 JSP 里结合 EL 表达式判断sessionScope.currentUser.role的值不推荐在页面里写大段 Java 脚本逻辑简单且页面友好才是王道。这套基于 Servlet JSP 的图书管理系统做完你会发现后续再去学习 Spring MVC 里DispatcherServlet的工作原理、RequestMapping的路由映射都会有一种“原来如此”的通透感——因为你已经亲手把最底层的那套流程走了一遍。本文还有配套的精品资源点击获取