news 2026/9/9 23:46:46

Servlet+JSP图书管理系统开发:从分工设计到连接池、事务与JSP调试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Servlet+JSP图书管理系统开发:从分工设计到连接池、事务与JSP调试

简介:这是一套基于Servlet与JSP的图书管理系统完整项目源码,面向Java Web初学者及课程设计/毕业设计人群,通过实际案例展示图书信息增删改查等核心管理功能的实现方式。代码结构清晰,涵盖Servlet请求处理、JSP界面展示、JavaScript与jQuery前端交互以及SQL数据库脚本。资源包为RAR格式,共包含419个文件,压缩包大小约14.91MB,主要文件类型包括24个Java源文件、48个class编译文件、14个JSP页面、18个CSS样式、6个JavaScript脚本、SQL脚本、XML配置及JAR依赖库,另附说明文档便于部署;项目基于IntelliJ IDEA开发,可配合Tomcat直接运行。目前已有2216人学习/下载,适合作为学习Servlet、JSP、jQuery协同开发的实战范例。利用这些文件可快速掌握Java Web应用从后端逻辑到前端页面的完整流程,并能在现有基础上二次扩展,是课程设计与毕业设计的实用参考。 看了下这个页面,我的思路是:

如果你是为了交作业或者应付毕业设计,那我劝你换个方向想问题——你真正需要的不是一份能提交的源码,而是搞清楚Servlet和JSP在一个真实项目里到底怎么分工、怎么配合。图书馆管理系统之所以能被各路教程讲了十几年,是因为它的业务逻辑足够简单清楚:一个图书实体、一个读者实体、中间再来张借阅记录表,增删改查加个状态流转,正好把JavaWeb最核心的那套请求-处理-响应链条完整覆盖了。

我先给你泼盆冷水,也是这些年带新人总结出来的经验:这类项目最大的坑不是写不出来,而是开局就错。很多人一上来就打开IDEA新建项目,然后对着空荡荡的src目录发愁,不知道该建几个包、写几个类、页面文件放哪里。等你纠结完这些,半天已经过去了。

这篇文章我会按自己的实际操作习惯,把这个项目的开发顺序和关键技术点重新捋一遍。顺序很重要——教科书喜欢从环境搭建讲起,但实际开发里应该先从"数据长什么样"入手,把表和功能边界定清楚,再去想代码怎么组织。另外我也会把两个高频问题单独拿出来说透:一个是数据访问层的连接池和事务处理,另一个是"JSP改了不生效"的完整排查思路。这两个问题在毕设答辩和面试里被问到的概率极高,而且是真正能拉开差距的地方。

1. 先把Servlet和JSP的分工想明白

很多教材把Servlet和JSP并列着讲,好像它们是两个竞争关系的东西,这是最大的误解。它们其实是一个团队里的两个角色,配合干活。

Servlet的角色是"后台调度员"。它接收浏览器发来的请求,解析参数,调用业务方法处理数据,最后决定把结果交给哪个页面去展示。它的强项是Java代码,适合写逻辑,但不适合写HTML——你想想,用out.println(" " + bookName + " ")去拼一个表格,不但写着难受,后期改样式更是噩梦。

JSP的角色恰恰相反,它是"前端展示页"。它的强项是把Java代码嵌在HTML里,适合做页面渲染。但它不适合写复杂逻辑——如果你发现哪段代码超过十行,赶紧挪到Servlet或者JavaBean里去。

用图书管理系统打个比方。用户在前端页面点了一下"查询图书",流程是这样的:

  • 浏览器把请求发给Servlet(比如BookServlet,带个action=query的参数)
  • Servlet收到请求后,调用BookDAO的queryBooks()方法,从数据库查出图书列表
  • Servlet把查到的List 塞进request对象,转发到bookList.jsp
  • JSP负责用for循环把List里的每本书渲染成一个表格行

所以你在开发的时候应该有一个清晰的判断,当前写的这行代码属于哪一层:是接收参数做判断(Servlet)、是操作数据库查数据(DAO)、还是把数据展示成页面(JSP)。脑子里有了这个分层,代码结构自然就清晰了,后面加功能、查Bug都会顺手很多。

顺便说一句,现在主流的Spring Boot项目里,Servlet这个角色被Controller替代了,JSP也基本被Thymeleaf或者前后端分离的JSON接口替代了。但"接收请求→处理数据→响应页面"这个核心链路一点没变。所以别觉得学这套老技术是浪费时间,它其实是理解现代框架的一把钥匙。

2. 动手之前:先把表和功能边界划清楚

我见过太多人上来就写代码,写到一半发现借书状态不知道存在哪、逾期天数算不出来、想加个搜索功能发现SQL全是坑。这些问题的根源都一样——没在设计阶段把数据模型想清楚。

图书管理系统最核心的就是三张表,一张都不能少:

图书表(book)记录书本身的静态信息。图书编号是主键,书名、作者、出版社、ISBN、库存总量、当前可借数量这些字段按需加上。有个细节容易忽略:如果你想做图书封面或者封面图上传,最好预留一个cover_path字段存图片路径,而不是直接把图片二进制塞进数据库——后者会让数据库变得臃肿,查询效率也受影响。

读者表(reader)存借书的人。读者编号主键、姓名、联系方式、借书证号。如果你要做登录功能,这张表还要加上用户名和密码字段。很多毕业设计把管理员和读者分开建表,我的建议是如果权限逻辑不复杂,用一张表加个role字段区分就行,省去大量联表查询的麻烦。

借阅记录表(borrow_record)是整个系统的业务核心。它至少要包含这些字段:记录ID、图书ID、读者ID、借书日期、应还日期、实际归还日期、状态。这里要特别强调状态字段的设计,很多新手只存一个"已借出/已归还",导致逾期判断根本没法做。我的做法是让状态字段跟实际归还日期联动:实际归还日期为空且应还日期小于今天的,就是逾期未还;实际归还日期不为空的,就是已归还。不额外设置冗余状态,逻辑简单还不容易出错。

功能边界上,如果你做的是标准版的图书管理系统,先把这四个功能跑通:

  • 图书管理:新增、修改、删除、按书名或作者模糊查询、分页列表
  • 读者管理:新增、修改、删除、列表查询
  • 借书/还书:借书时校验图书可借数量并扣减,还书时更新记录状态并回补库存
  • 统计展示:首页展示馆藏总数、借出数量、逾期记录

借书和还书是最能体现你业务逻辑设计能力的部分。想清楚借书时涉及几张表的更新——借阅记录插一条,图书表的可借数量减一;还书时反向操作,还书日期写上,可借数量加一。教学版本的图书管理系统如果只做简单的增删改查,很难拿到高分,但加上借还书这个带状态流转的闭环,整个系统的复杂度就上来了,分数也上来了。

最后说一句关于表结构设计的建议:字段命名统一用下划线风格(如book_name),Java代码里对应属性再用驼峰风格(bookName),配合MyBatis或者手动映射都方便。别一会儿下划线一会儿驼峰混着来,后期会疯。

3. 页面流转与Servlet分工:这个项目的骨架逻辑

表设计好了,接下来才是代码组织。我先把自己的推荐结构说出来,再解释为什么这样分。

src/main/java ├── com.example.bookms │ ├── entity // Book、Reader、BorrowRecord实体类 │ ├── dao // BookDAO、ReaderDAO、BorrowDAO │ ├── service // BookService、BorrowService │ ├── servlet // BookServlet、ReaderServlet、BorrowServlet、LoginServlet │ └── util // DBUtil、DateUtil、PageUtil webapp ├── WEB-INF │ ├── web.xml // Servlet映射配置 │ └── jsp // 所有JSP页面放在这里 ├── css ├── js └── index.jsp // 入口页

这个结构对应的是经典的MVC模式:JSP是视图,Servlet是控制器,Service和DAO是模型层。为什么不要把所有Servlet都写成一个?因为图书、读者、借还书三个业务模块的接口数量一多,一个Servlet里要塞几十个if-else判断,维护起来非常痛苦。按业务模块分多个Servlet,每个Servlet的职责清晰,代码量也适中。

再看web.xml里是怎么配置的。下面这段是标准的Servlet映射配置,在Servlet 3.0之前只能这么写,现在也可以用注解@WebServlet替代。我建议你在做这个项目的时候用web.xml的方式写一遍,因为面试经常会问这个流程,手写一遍印象会深很多:

<servlet> <servlet-name>BookServlet</servlet-name> <servlet-class>com.example.bookms.servlet.BookServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>BookServlet</servlet-name> <url-pattern>/book</url-pattern> </servlet-mapping> <servlet> <servlet-name>BorrowServlet</servlet-name> <servlet-class>com.example.bookms.servlet.BorrowServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>BorrowServlet</servlet-name> <url-pattern>/borrow</url-pattern> </servlet-mapping>

配置好之后,浏览器访问http://localhost:8080/项目名/book?action=add,Tomcat就会根据这个url-pattern找到BookServlet,然后调用它的doGet或者doPost方法。这个映射逻辑是整个JavaWeb开发的基石,你后面的所有跳转路径都是围绕它设计的。

Servlet的内部逻辑,我习惯用统一的action参数做分发。比如BookServlet的doPost方法里:

String action = request.getParameter("action"); if ("add".equals(action)) { // 调用新增逻辑 } else if ("update".equals(action)) { // 调用修改逻辑 } else if ("delete".equals(action)) { // 调用删除逻辑 } else if ("query".equals(action)) { // 调用查询逻辑 }

这样做的好处是同一个模块的接口都收敛在同一个Servlet里,路径好记,代码也好找。坏处是如果一个action分支里的逻辑太多,方法会被撑得很长。解决办法是每个分支都单独抽一个方法,比如doAdd(HttpServletRequest request, HttpServletResponse response),这样主方法看着清爽,子方法也都短小精悍。

最后说说转发和重定向的区别,这是必考知识点,也是实际写代码天天要用的。新增图书成功后要跳转到列表页,如果你用forward转发,浏览器地址栏不会变,用户按F5刷新就会重复提交表单,数据库里多出好几条一模一样的记录。这种问题在答辩现场出现过很多次,急救方案是改用重定向:

response.sendRedirect(request.getContextPath() + "/book?action=list");

这个request.getContextPath()是获取项目根路径,拼上后面的路径才是完整的URL。很多新手忘了加这个前缀,结果跳转404,这也是一个经典坑。

4. 数据访问这一层:连接池、DAO封装和事务边界

数据访问层是整个系统最容易出事的地方,也是最值得花时间优化的地方。我见过不少同学写的代码,直接在Servlet里用Class.forName加载驱动,然后创建Connection、执行SQL、关闭连接,一条龙写下来。跑通是能跑通,但存在两个严重问题。

第一是性能问题。每次请求都要重新加载驱动、建立物理连接,数据库连接的开销其实是很大的。高并发场景下,这种写法能直接把数据库拖垮。第二是连接泄漏的隐患。很多人写SQL查询,忘了在finally块里关闭Connection,或者忘记关闭ResultSet和Statement。一次两次没关系,时间长了连接池耗尽,页面就卡死不动了。

我的建议是引入连接池,比如Druid或者C3P0。拿Druid来说,它的配置其实很简洁,放在druid.properties文件里:

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/bookms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username=root password=你的密码 initialSize=5 maxActive=20 maxWait=30000

然后在工具类里读取配置,初始化连接池:

public class DBUtil { private static DruidDataSource dataSource; static { Properties props = new Properties(); try (InputStream is = DBUtil.class.getClassLoader() .getResourceAsStream("druid.properties")) { props.load(is); dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }

要注意url里的characterEncoding=utf8这个参数。如果不设置,中文字符存进去再查出来可能会变成问号。这个参数配合页面端的request.setCharacterEncoding("UTF-8")和JSP页面顶部的pageEncoding="UTF-8",三层下来才能保证中文不乱码。缺任何一层,乱码问题都会以各种奇怪的形式出现。

再说DAO封装。每个表对应一个DAO类,里面写增删改查的方法,返回值要么是实体对象、要么是List、要么是int受影响行数。一个典型的查询方法的写法是这样的:

public List<Book> queryBooks(String keyword) throws SQLException { List<Book> list = new ArrayList<>(); String sql = "SELECT * FROM book WHERE book_name LIKE ? OR author LIKE ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setBookName(rs.getString("book_name")); book.setAuthor(rs.getString("author")); book.setPublisher(rs.getString("publisher")); book.setTotalStock(rs.getInt("total_stock")); book.setAvailableStock(rs.getInt("available_stock")); list.add(book); } } } return list; }

关键细节我都写在这个示例里了,说几个重点:

  • 用PreparedStatement而不是Statement,既能防止SQL注入,又方便设置参数
  • 用try-with-resources语法自动关闭连接,不用手动finally关闭,少写很多样板代码
  • 查询条件是模糊匹配,所以SQL里用了LIKE,参数里就要手动拼上百分号

关于事务,借书这个动作是有事务性的,不能插了借阅记录却忘了扣库存。如果用框架,加个@Transactional注解就完事了。但原生Servlet里你必须自己管理事务:

Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 插入借阅记录 // 扣减图书库存 conn.commit(); // 两个操作都成功才提交 } catch (Exception e) { if (conn != null) conn.rollback(); // 任何一个失败都回滚 throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } }

这个模式下要注意一点:你在DAO层不能用try-with-resources直接关闭连接,否则事务就断了。正确的做法是把事务控制抽到Service层,DAO层只负责执行SQL,连接由Service层统一管理,操作结束后再关闭。这一步体现的就是Service层的价值所在。

5. JSP改了不生效排查实录:三层原因逐个击破

"JSP改了不生效"大概是JavaWeb开发里最让人抓狂的问题了。我在这上面帮人排查过很多次,也自己踩过。大部分情况不是代码写错了,而是改动没有被真正加载。我这里把排查链路完整写出来,遇到这个问题可以照着顺序逐一检查。

第一层检查:IDEA的target目录有没有同步。IDEA里的项目是Multi-Module结构的,源码改了之后需要经过编译、复制资源文件,最终部署到Tomcat里的是target目录(或者out目录)那一份,不是你改的源文件那一份。如果你改了JSP页面,但target目录里的同名文件还是旧内容,那说明IDEA没有触发自动编译。解决办法很简单:Build菜单下选择Rebuild Project,强制重新构建一次。这个操作应该成为你的肌肉记忆,每次遇到诡异问题时第一个先想到它。

第二层检查:Tomcat部署的是war包还是exploded war。war包方式部署,Tomcat会在工作目录里解压一份副本,你改完重新打包war才能生效,直接改源文件当然没用。密码就是改用exploded war方式部署,也就是IDEA里说的"部署为解压目录",这样每次构建后Tomcat直接加载最新文件,不用重复打war包。如果你用的是较新的Tomcat版本,官方现在也推荐用exploded方式开发调试,因为热部署更友好。

第三层检查:浏览器缓存。这个最容易被人忽略。JSP页面在浏览器端可能有缓存,尤其当你用浏览器自带的"后退"按钮返回页面时,看到的很可能是历史快照,而不是服务器返回的新页面。排查方法很直接:按F12打开开发者工具,切到Network面板,勾选"Disable cache",然后强制刷新。如果这样能看到最新效果,那就是浏览器缓存的问题。平时开发时建议直接用无痕窗口调试,可以省去大量缓存相关的困惑。

除了这三层,还有一个隐藏环节容易踩坑:Tomcat的conf/web.xml和conf/context.xml。比如你配置了全局字符编码过滤器,但位置或者类名写错了,页面上的中文就会出现乱码或者显示异常。这种问题排查起来最难,因为报错信息不直观,有时候只是某些页面上出现方框符号。我的建议是:检查一遍Tomcat的conf目录下的xml是否有语法错误,尤其是server.xml里有没有被误改过的Connector端口配置。

再补充一个跟JSP直接相关的小细节。很多人写JSP喜欢用默认的page指令,但如果你要在页面上用forEach之类的JSTL标签,必须引入:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

同时记得在pom.xml或者lib目录里引入jstl和standard的jar包。不然你在页面上用了<c:forEach>,运行时报错说找不到标签,你还会一头雾水。这个坑属于那种"教程里写过但你没注意"的类型,报错的时候很折磨人,建议提前备好。

6. 进阶一点:分页查询的两种实现,别被面试官问倒

图书管理系统的列表页,图书量一大就要做分页。这里有两种主流实现方案,在项目里用哪种都行,但原理最好都懂。

第一种是传统分页,依靠SQL的LIMIT语句。Java端接收两个参数:当前页码page和每页条数pageSize,然后计算offset = (page - 1) * pageSize,查数据库时LIMIT offset, pageSize。同时另写一个count查询获取总条数,用总条数除以每页条数算出总页数。这种方式在数据量不大时性能很好,实现也简单,是目前的主流做法。

第二种是物理分页配合SQL_CALC_FOUND_ROWS。MySQL特有的一种写法,查询时用SELECT SQL_CALC_FOUND_ROWS * FROM book LIMIT offset, pageSize,紧接着执行SELECT FOUND_ROWS()就能拿到符合条件的总条数,省去一次count查询。在数据量特别大的时候性能比第一种略好,但存在一些边界问题,一般不建议新手使用,记住有这个概念就行。

做分页还有一个容易忽略的点:当用户在第3页搜索了一个关键词,结果很少只有1页时,页码会显示异常。这个处理不复杂,就是每次查询完都重新计算总页数,如果当前页码大于总页数,就把当前页码重置为1重新查一次。属于典型的边界处理,测试时一定要覆盖到。

分页导航的JSP页面可以用JSTL标签来渲染,配合JSP的c:forEach和c:if等标签,写起来很简洁。如果你没有引入JSTL,也可以用Scriptlet写Java代码循环生成页码链接,但不推荐,因为页面里嵌入大段Java代码本来就是JSP最被诟病的点。

7. 从Servlet+JSP到Spring Boot:学完老项目怎么往前走

如果你不是因为毕业设计才看这篇文章,而是想为以后工作打基础,那学完这个项目之后,下一步该怎么走就要想清楚了。

Servlet+JSP这套技术栈,在今天的企业开发里已经被Spring Boot全面替代了。Spring Boot的MVC架构,本质上就是对Servlet的封装和增强。你在图书管理系统里写过的Controller,在Spring Boot里长这样:

@Controller @RequestMapping("/book") public class BookController { @Autowired private BookService bookService; @RequestMapping("/list") public String list(Model model) { List<Book> books = bookService.queryAllBooks(); model.addAttribute("books", books); return "bookList"; } }

对比一下传统的BookServlet,你会发现思路完全一样:都是接收请求、调用Service、返回视图。区别在于Spring Boot用注解替代了web.xml配置,用自动装配替代了手动new对象,用内置Tomcat替代了外置容器。你之前学的那些Servlet和JSP层面的东西,只是换了个壳,内核还是那套"请求-处理-响应"模型。

所以我的建议是:不要觉得Servlet+JSP白学了。相反,你在做图书管理系统时打下的这些基本功——连接池怎么配、事务怎么控制、路径怎么跳转、乱码怎么排查——在Spring Boot项目里依然是绕不开的知识点。框架解决的是开发效率和工程化的问题,但底层的数据库访问、HTTP协议、状态管理这些基础,永远需要你自己掌握。

写到这里,回到开头说的那句话:这个项目的价值不在于"技术上多先进",而在于它能帮你把JavaWeb的知识体系完整串起来。如果你正在做这个项目,我的建议是不要只停留在"跑通就行"的层面,试着给它加上登录验证、分页查询、防重复提交、用户权限这些真实系统里必不可少的模块。每多踩一个坑,你积累的经验就多一分。等你把这些问题都处理过一遍,再回头看Spring Boot、MyBatis这些框架,你会发现一切都顺理成章。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 23:46:34

Python操作剪映关键帧:JSON解析与批量自动化实战

简介&#xff1a;这是一份基于Python开发的剪映关键帧自动化工具桌面版源码&#xff0c;面向需要批量、高效处理视频关键帧的剪辑爱好者与开发者。工具可自动识别视频关键帧&#xff0c;并依据用户设定条件筛选最适合的剪辑点&#xff0c;也支持按需调节参数&#xff0c;实现个…

作者头像 李华
网站建设 2026/9/9 23:45:11

NDIS 6.0 Filter驱动实战:收发数据包与MAC地址查询实现

简介&#xff1a;一份基于Windows 10 x64平台的NDIS 6.0 Filter驱动示例&#xff0c;主要面向具有C/C和Windows驱动基础的开发者&#xff0c;演示在KMDF框架下实现网络数据包处理功能&#xff1a;支持发送OID请求&#xff0c;能构造并发送ICMP自定义数据包&#xff0c;也可实时…

作者头像 李华
网站建设 2026/9/9 23:45:03

基于PyTorch的软PINN求解二维对流传热温度场实现与调参指南

前段时间一直在折腾物理信息神经网络&#xff08;PINN&#xff09;在传热问题里的实际落地。手头有个场景是两块平行平板之间的二维稳态对流传热&#xff0c;要预测温度场。传统做法是画网格跑CFD&#xff0c;但临时搭个求解器实在费劲&#xff0c;于是我从零用Python和PyTorch…

作者头像 李华
网站建设 2026/9/9 23:44:41

2026年8月台式装机配置指南:三套预算方案从3000到15000元

1. 2026年8月这个时间点&#xff0c;装机前先看这几件事 先说结论&#xff1a;8月一直是装机的好时候&#xff0c;但2026年8月有几个特殊背景&#xff0c;值得在挑配置之前先花两分钟搞清楚&#xff0c;否则很容易买贵或者买错。 第一个背景是平台换代处于中后段。目前无论是I…

作者头像 李华