
简介这是一套基于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带个actionquery的参数Servlet收到请求后调用BookDAO的queryBooks()方法从数据库查出图书列表Servlet把查到的List 塞进request对象转发到bookList.jspJSP负责用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_nameJava代码里对应属性再用驼峰风格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-nameBookServlet/servlet-name servlet-classcom.example.bookms.servlet.BookServlet/servlet-class /servlet servlet-mapping servlet-nameBookServlet/servlet-name url-pattern/book/url-pattern /servlet-mapping servlet servlet-nameBorrowServlet/servlet-name servlet-classcom.example.bookms.servlet.BorrowServlet/servlet-class /servlet servlet-mapping servlet-nameBorrowServlet/servlet-name url-pattern/borrow/url-pattern /servlet-mapping配置好之后浏览器访问http://localhost:8080/项目名/book?actionaddTomcat就会根据这个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?actionlist);这个request.getContextPath()是获取项目根路径拼上后面的路径才是完整的URL。很多新手忘了加这个前缀结果跳转404这也是一个经典坑。4. 数据访问这一层连接池、DAO封装和事务边界数据访问层是整个系统最容易出事的地方也是最值得花时间优化的地方。我见过不少同学写的代码直接在Servlet里用Class.forName加载驱动然后创建Connection、执行SQL、关闭连接一条龙写下来。跑通是能跑通但存在两个严重问题。第一是性能问题。每次请求都要重新加载驱动、建立物理连接数据库连接的开销其实是很大的。高并发场景下这种写法能直接把数据库拖垮。第二是连接泄漏的隐患。很多人写SQL查询忘了在finally块里关闭Connection或者忘记关闭ResultSet和Statement。一次两次没关系时间长了连接池耗尽页面就卡死不动了。我的建议是引入连接池比如Druid或者C3P0。拿Druid来说它的配置其实很简洁放在druid.properties文件里driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/bookms?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot password你的密码 initialSize5 maxActive20 maxWait30000然后在工具类里读取配置初始化连接池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里的characterEncodingutf8这个参数。如果不设置中文字符存进去再查出来可能会变成问号。这个参数配合页面端的request.setCharacterEncoding(UTF-8)和JSP页面顶部的pageEncodingUTF-8三层下来才能保证中文不乱码。缺任何一层乱码问题都会以各种奇怪的形式出现。再说DAO封装。每个表对应一个DAO类里面写增删改查的方法返回值要么是实体对象、要么是List、要么是int受影响行数。一个典型的查询方法的写法是这样的public ListBook queryBooks(String keyword) throws SQLException { ListBook 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 prefixc urihttp://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. 从ServletJSP到Spring Boot学完老项目怎么往前走如果你不是因为毕业设计才看这篇文章而是想为以后工作打基础那学完这个项目之后下一步该怎么走就要想清楚了。ServletJSP这套技术栈在今天的企业开发里已经被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) { ListBook books bookService.queryAllBooks(); model.addAttribute(books, books); return bookList; } }对比一下传统的BookServlet你会发现思路完全一样都是接收请求、调用Service、返回视图。区别在于Spring Boot用注解替代了web.xml配置用自动装配替代了手动new对象用内置Tomcat替代了外置容器。你之前学的那些Servlet和JSP层面的东西只是换了个壳内核还是那套请求-处理-响应模型。所以我的建议是不要觉得ServletJSP白学了。相反你在做图书管理系统时打下的这些基本功——连接池怎么配、事务怎么控制、路径怎么跳转、乱码怎么排查——在Spring Boot项目里依然是绕不开的知识点。框架解决的是开发效率和工程化的问题但底层的数据库访问、HTTP协议、状态管理这些基础永远需要你自己掌握。写到这里回到开头说的那句话这个项目的价值不在于技术上多先进而在于它能帮你把JavaWeb的知识体系完整串起来。如果你正在做这个项目我的建议是不要只停留在跑通就行的层面试着给它加上登录验证、分页查询、防重复提交、用户权限这些真实系统里必不可少的模块。每多踩一个坑你积累的经验就多一分。等你把这些问题都处理过一遍再回头看Spring Boot、MyBatis这些框架你会发现一切都顺理成章。本文还有配套的精品资源点击获取