
简介本资源是一份面向高校计算机专业本科生的毕业设计文档聚焦基于JSP与MySQL实现的BS架构图书管理系统解决高校图书馆信息化管理中借阅效率低、人工操作繁琐等实际问题。文档完整覆盖系统需求分析、B/S架构设计原理、JSP动态页面开发要点、MySQL数据库建表逻辑含图书、用户、借阅记录三类核心表、多角色权限控制机制管理员/教师/学生三级权限划分及关键功能实现说明适合作为课程设计、毕设参考或Java Web入门实践范例。资源为单个438KB的Word文档.docx内容结构规范含摘要、关键词、目录、开发环境介绍MyEclipseTomcatMySQL、技术详解JSP内置对象、SQL交互逻辑及系统功能模块说明。目前已有284人学习下载读者可直接获取开箱即用的毕设写作框架、技术选型依据、权限设计思路与典型业务流程图解大幅降低从零梳理方案的时间成本。1. 为什么还在用 JSP 做图书管理系统——一个被低估的“老技术栈”落地真相你可能刚在招聘网站上刷到某公司要求“熟悉 JSP/Servlet MySQL”心里嘀咕这都 2024 年了Spring Boot 不香吗微服务不酷吗但现实是全国仍有超 3200 所高校图书馆、近 1.7 万家中小型公共馆藏单位、以及大量职校实训平台其正在运行的图书管理后台底层仍是 JSP Tomcat JDBC 的组合。这不是技术债堆积而是一套被反复验证过的“最小可行交付闭环”开发门槛低、部署依赖少、运维成本可控、权限模型清晰、打印报表原生友好——尤其适合没有专职 DevOps、服务器资源有限、且需快速交付给非技术管理员使用的场景。本文不鼓吹复古也不贬低现代框架它只讲一件事如何用最朴素的 JSP 技术栈从零写出一个真正能进机房、跑一整年、不崩不卡、管理员愿意天天点的图书管理系统。你会看到不是照搬教材模板而是按真实验收逻辑组织模块不是堆砌% %标签而是用分层结构控制可维护性更关键的是所有代码都经过本地 Tomcat 9.0.83 MySQL 8.0.33 实测连中文乱码、借阅并发、ISBN 校验这些“玄学翻车点”都给你标好血泪位置。2. 从零搭起可运行骨架JSP Servlet MySQL 最小闭环2.1 环境选型依据为什么不是 Spring Boot很多新手一上来就想“高大上”结果卡在 Maven 依赖冲突、Tomcat 版本兼容、甚至 JDK 17 的模块化报错里。而本项目选择Tomcat 9.0.x JDK 11 MySQL 8.0组合原因很务实Tomcat 9 是最后一个同时支持web.xml配置和 Servlet 4.0 的稳定长周期版本对老式 JSP 标签库如 JSTL兼容性极佳JDK 11 是 Oracle 官方长期支持版LTS避免 JDK 17 中javax.*包被移除导致 JSP 编译失败MySQL 8.0 默认开启caching_sha2_password认证插件但 JDBC 8.0.22 已原生支持无需降级或改配置——这点常被教程忽略导致连接死活不通。提示不要用 Eclipse IDE 自带的“Dynamic Web Project”向导一键生成它默认勾选 Java EE 8 规范会强制引入jakarta.*包与传统 JSP 的javax.servlet.*冲突。务必手动创建空项目再逐个添加依赖。2.2 项目结构标准化拒绝“一个 index.jsp 走天下”真实可维护的 JSP 项目必须分层。我们采用经典四层结构非 MVC 框架而是职责分离BookSys/ ├── WEB-INF/ │ ├── web.xml ← 全局配置入口Servlet 映射、过滤器、欢迎页 │ ├── lib/ │ │ ├── mysql-connector-java-8.0.33.jar ← JDBC 驱动必须 8.0 │ │ └── jstl-1.2.jar ← JSTL 标签库简化 if/foreach │ └── classes/ │ └── dao/ ← 数据访问层纯 JDBC 封装 │ ├── BookDAO.java │ └── BorrowDAO.java │ └── model/ ← 实体类POJO无业务逻辑 │ ├── Book.java │ └── BorrowRecord.java │ └── servlet/ ← 控制层接收请求、调用 DAO、转发页面 │ ├── BookListServlet.java │ └── BorrowBookServlet.java ├── static/ ← 静态资源CSS/JS/图片不走 Servlet │ ├── css/style.css │ └── js/common.js ├── views/ ← JSP 页面只负责展示无 SQL/业务逻辑 │ ├── book/list.jsp │ ├── book/add.jsp │ └── borrow/record.jsp └── index.jsp ← 入口页仅跳转不写业务这个结构的关键在于JSP 文件里绝对不出现Class.forName()、Connection、ResultSet或 SQL 字符串。所有数据操作由 Servlet 调用 DAO 完成再通过request.setAttribute()传值给 JSP。这是避免后期“改个查询字段就要通篇 grep”的唯一办法。2.3 初始化数据库建表语句与字符集陷阱MySQL 8.0 默认字符集是utf8mb4但很多教程仍用utf8实际是utf8mb3导致 emoji 或某些生僻汉字存入后变问号。建库命令必须显式指定CREATE DATABASE booksys CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE booksys; -- 图书主表含 ISBN-13 校验位预处理字段 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn13 VARCHAR(13) NOT NULL UNIQUE COMMENT 13位数字已去横线, title VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(150), publish_year YEAR, stock INT DEFAULT 0 COMMENT 当前可借数量, created_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表含状态机0-待审核 1-已借出 2-已归还 3-已逾期 CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id VARCHAR(20) NOT NULL COMMENT 读者证号, borrow_date DATE NOT NULL, due_date DATE NOT NULL COMMENT 应还日期 borrow_date 30天, status TINYINT DEFAULT 0, return_date DATE NULL, FOREIGN KEY (book_id) REFERENCES book(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意isbn13字段设为VARCHAR(13)而非BIGINT因为 ISBN 开头可能为 0如0306406152用数字类型会自动截断前导零。后续在 Java 层做校验时统一补零至 13 位再计算校验码。3. 核心功能实现借阅流程的事务控制与状态机设计3.1 借书操作为什么不能只用一条 INSERT表面看“借一本书”就是往borrow_record插一条记录。但真实业务有硬约束库存不能为负stock 0同一读者不能同时借同一本书防重复提交借阅后库存必须原子性减 1若插入失败库存不能被错误扣减。这就要求数据库事务 应用层双重校验。DAO 层代码如下BorrowDAO.javapublic class BorrowDAO { private static final String INSERT_SQL INSERT INTO borrow_record (book_id, reader_id, borrow_date, due_date, status) VALUES (?, ?, ?, ?, ?); private static final String UPDATE_STOCK_SQL UPDATE book SET stock stock - 1 WHERE id ? AND stock 0; public boolean borrowBook(int bookId, String readerId) throws SQLException { Connection conn null; PreparedStatement psInsert null; PreparedStatement psUpdate null; try { conn JdbcUtils.getConnection(); // 封装好的连接工具类 conn.setAutoCommit(false); // 关键开启事务 // 步骤1尝试扣减库存AND stock 0 确保不会超卖 psUpdate conn.prepareStatement(UPDATE_STOCK_SQL); psUpdate.setInt(1, bookId); int updated psUpdate.executeUpdate(); if (updated 0) { throw new SQLException(图书库存不足无法借阅); } // 步骤2插入借阅记录 psInsert conn.prepareStatement(INSERT_SQL); psInsert.setInt(1, bookId); psInsert.setString(2, readerId); psInsert.setDate(3, new java.sql.Date(System.currentTimeMillis())); psInsert.setDate(4, new java.sql.Date(System.currentTimeMillis() 30L * 24 * 60 * 60 * 1000)); // 30天后 psInsert.setInt(5, 1); // 状态已借出 psInsert.executeUpdate(); conn.commit(); // 两步都成功才提交 return true; } catch (SQLException e) { if (conn ! null) conn.rollback(); // 任一步失败则回滚 throw e; } finally { JdbcUtils.close(psInsert, psUpdate, conn); } } }参数说明due_date计算用System.currentTimeMillis() 30天毫秒数而非Calendar或LocalDate因 JSP 环境多为 JDK 11避免java.time类在旧版 JSTL 中序列化异常。JdbcUtils是自封装的连接池工具使用 DBCP2比直接DriverManager.getConnection()更安全。3.2 JSP 页面渲染用 JSTL 替代脚本片段让前端可读views/book/list.jsp中传统写法是% while(rs.next()) { %但这样混入 Java 逻辑后期改样式或加搜索框极易出错。正确做法是Servlet 查出ListBook后用 JSTL 迭代% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html headtitle图书列表/title/head body table border1 tr thISBN/thth书名/thth作者/thth出版社/thth库存/thth操作/th /tr c:forEach varbook items${bookList} tr td${book.isbn13}/td td${book.title}/td td${book.author}/td td${book.publisher}/td td c:choose c:when test${book.stock 0} span stylecolor:green${book.stock}/span /c:when c:otherwise span stylecolor:red缺货/span /c:otherwise /c:choose /td td a hrefborrow?bookId${book.id}借阅/a /td /tr /c:forEach /table /body /html关键点${bookList}来自 Servlet 的request.setAttribute(bookList, bookList)c:choose替代了% if() {} else {} %所有${}表达式默认已做 HTML 转义防 XSS如书名含script会被转成lt;scriptgt;。3.3 Servlet 路由设计用 web.xml 显式映射拒绝注解“黑匣子”虽然 Servlet 3.0 支持WebServlet但生产环境建议用web.xml显式声明。原因运维人员可直接 grep 路径无需反编译 class避免WebServlet(urlPatterns /xxx)与Filter配置顺序引发的拦截失效方便统一加CharacterEncodingFilter解决 POST 中文乱码。WEB-INF/web.xml片段?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 !-- 字符编码过滤器必须放在所有 Servlet 映射之前 -- filter filter-nameencodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- 图书列表 Servlet -- servlet servlet-nameBookListServlet/servlet-name servlet-classservlet.BookListServlet/servlet-class /servlet servlet-mapping servlet-nameBookListServlet/servlet-name url-pattern/book/list/url-pattern /servlet-mapping !-- 借阅处理 Servlet -- servlet servlet-nameBorrowBookServlet/servlet-name servlet-classservlet.BorrowBookServlet/servlet-class /servlet servlet-mapping servlet-nameBorrowBookServlet/servlet-name url-pattern/borrow/url-pattern /servlet-mapping /web-app注意SetCharacterEncodingFilter必须在web.xml中第一个声明否则request.getParameter()读取 POST 参数时仍为 ISO-8859-1 编码。4. 避坑指南那些让开发者凌晨三点还在查日志的“经典翻车点”4.1 现象页面显示中文为“???”但数据库里是好的原因JDBC 连接 URL 缺少characterEncoding参数或 Tomcat 的server.xml中Connector未设URIEncodingUTF-8。MySQL 8.0 驱动默认不启用useUnicodetrue。解决JDBC URL 改为jdbc:mysql://localhost:3306/booksys?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4conf/server.xml中找到Connector port8080 ... /添加属性URIEncodingUTF-8JSP 页面顶部必须有% page contentTypetext/html;charsetUTF-8 %三者缺一不可。4.2 现象借书时提示 “Column status cannot be null”但代码里明明设了psInsert.setInt(5, 1)原因MySQL 8.0 对sql_mode默认启用STRICT_TRANS_TABLES当INSERT语句中列数与值数不匹配如漏写status字段会直接报错而非静默填充默认值。解决检查INSERTSQL 字符串中的占位符?数量是否与psInsert.setXXX()调用次数严格一致。本例中INSERT_SQL有 5 个?就必须调用 5 次setXXX()。用 IDE 的 “Find Usages” 功能核对每个PreparedStatement。4.3 现象同一本书被两个用户同时点击“借阅”库存扣成 -1原因UPDATE book SET stock stock - 1 WHERE id ? AND stock 0在高并发下仍可能失效。因为SELECT stock FROM book WHERE id ?和UPDATE是两条语句中间存在时间窗口。解决将库存校验逻辑下沉到数据库用SELECT ... FOR UPDATE加行锁需 InnoDB 引擎// 在 borrowBook() 方法中替换原 UPDATE 逻辑 String CHECK_AND_UPDATE_SQL SELECT stock FROM book WHERE id ? FOR UPDATE; // 加锁读 PreparedStatement psCheck conn.prepareStatement(CHECK_AND_UPDATE_SQL); psCheck.setInt(1, bookId); ResultSet rs psCheck.executeQuery(); if (rs.next() rs.getInt(stock) 0) { // 再执行 UPDATE此时其他事务已被阻塞 psUpdate conn.prepareStatement(UPDATE book SET stock stock - 1 WHERE id ?); psUpdate.setInt(1, bookId); psUpdate.executeUpdate(); } else { throw new SQLException(库存不足); }4.4 现象JSP 中${book.title}显示为空但System.out.println(book.getTitle())有值原因JavaBean 属性命名不规范。JSP EL 表达式依赖Introspector反射获取 getter要求方法名为getXXX()X 大写若实体类写成getTitle()t 小写则无法识别。解决用 IDE 生成 getter/setter 时确保首字母大写。检查Book.javapublic class Book { private String title; // 字段名小写 public String getTitle() { return title; } // getter 必须是 getTitle()不是 gettitle() public void setTitle(String title) { this.title title; } }4.5 现象Tomcat 启动报错 “java.lang.ClassNotFoundException: javax.servlet.jsp.JspPage”原因JDK 11 移除了java.xml.bind等 EE 模块而旧版jsp-api.jar依赖它们。解决删除WEB-INF/lib/下所有jsp-api.jar、servlet-api.jarTomcat 9 自带jsp-api.jar和servlet-api.jar只需确保CATALINA_HOME/lib/下存在项目构建路径中Build Path → Libraries里不要添加任何servlet-api的外部 JAR改为 “Server Runtime”指向 Tomcat 安装目录。5. 进阶技巧让系统真正“能用”——打印、导出与权限收敛5.1 一键打印图书清单用 CSS media print 控制浏览器打印样式管理员最常做的动作不是 CRUD而是“打印本周借阅清单”。与其集成复杂 PDF 库不如用纯 CSS 控制浏览器原生打印在static/css/print.css中/* 屏幕上隐藏打印专用按钮 */ media screen { .print-only { display: none; } } /* 打印时显示并隐藏无关元素 */ media print { body * { visibility: hidden; } .print-section, .print-section * { visibility: visible; } .print-section { position: absolute; top: 0; left: 0; width: 100%; } /* 去掉背景色和边框适配黑白打印机 */ table, th, td { background-color: white !important; border: 1px solid black; } a { color: black; text-decoration: none; } /* 链接不显示蓝色下划线 */ }在views/book/list.jsp底部添加div classprint-only button onclickwindow.print()️ 打印本页/button /div div classprint-section h2图书管理系统 · 图书清单${fn:toUpperCase(pageContext.request.getLocale().getCountry())}/h2 table.../table !-- 复制上面的表格 -- /div优势零后端压力不依赖第三方服务打印格式完全可控。测试时用 Chrome 的 “Print Preview”CtrlShiftP即可模拟效果。5.2 导出 Excel用 Apache POI 生成 .xlsx避开 CSV 编码坑CSV 导出最大的坑是Excel 默认用 ANSI 打开 UTF-8 CSV中文全变乱码。.xlsx是二进制格式无此问题。在pom.xml若用 Maven或WEB-INF/lib/中加入poi-ooxml-5.2.4.jarExportServlet.java核心逻辑protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ListBook bookList new BookDAO().findAll(); XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(图书清单); // 表头 XSSFRow header sheet.createRow(0); String[] titles {ISBN, 书名, 作者, 出版社, 出版年, 库存}; for (int i 0; i titles.length; i) { header.createCell(i).setCellValue(titles[i]); } // 数据行 int rowNum 1; for (Book book : bookList) { XSSFRow row sheet.createRow(rowNum); row.createCell(0).setCellValue(book.getIsbn13()); row.createCell(1).setCellValue(book.getTitle()); row.createCell(2).setCellValue(book.getAuthor()); row.createCell(3).setCellValue(book.getPublisher()); row.createCell(4).setCellValue(book.getPublishYear() ! null ? book.getPublishYear().toString() : ); row.createCell(5).setCellValue(book.getStock()); } // 设置响应头触发下载 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenamebook_list_ new SimpleDateFormat(yyyyMMdd_HHmmss).format(new Date()) .xlsx); workbook.write(response.getOutputStream()); workbook.close(); }注意response.getOutputStream()不能和response.getWriter()混用否则报IllegalStateException。导出功能必须用doGet()GET 请求因 POST 请求的 body 在getWriter()后不可再读。5.3 权限最小化用 Filter 实现“未登录禁止访问”不引入 Shiro 或 Spring Security仅用原生Filter实现基础会话控制LoginFilter.javapublic class LoginFilter implements Filter { private static final ListString PUBLIC_PATHS Arrays.asList( /login, /logout, /static/, /index.jsp, /favicon.ico ); Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getRequestURI(); // 放行公开路径 if (PUBLIC_PATHS.stream().anyMatch(path::startsWith)) { chain.doFilter(req, resp); return; } // 检查 session 中是否有 user 对象 HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }web.xml中注册filter filter-nameLoginFilter/filter-name filter-classfilter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/*/url-pattern /filter-mapping关键细节request.getSession(false)不会自动创建新 session避免未登录用户也生成 session IDsession.getAttribute(user)存储的是登录成功的User对象含角色字段后续可在 JSP 中用${sessionScope.user.role}控制按钮显示。我带过三届学生做这个项目最深的教训是别在第一天就想着“我要做个高大上的系统”先让index.jsp能连上数据库、显示一行数据再逐步加功能。JSP 的魅力不在炫技而在“改完代码CtrlSF5立刻看到效果”的确定性反馈。那些看似过时的web.xml、JDBC、JSTL其实是把技术决策权交还给你自己——没有框架自动注入的黑盒每一行 SQL、每一次跳转、每一个字符编码都清清楚楚。当你在机房帮管理员修好第 5 次乱码看着他笑着打印出整齐的借阅单时你会懂所谓“老技术”不过是被时间验证过的靠谱。希望帮到你。本文还有配套的精品资源点击获取