ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

JSP图书管理系统源码实战:环境搭建、事务处理与避坑指南

JSP图书管理系统源码实战:环境搭建、事务处理与避坑指南 简介这份资源是一套基于JSPMySQLServlet技术栈实现的图书馆图书管理系统完整源码面向正在学习Java Web开发、需要课程设计或毕业设计参考的学生与初级开发者。系统按角色划分权限管理员可完成登录、用户管理、图书管理与罚款缴纳学生可登录、借阅归还图书、查看借阅记录并修改密码游客则能浏览图书列表与详情覆盖了图书流通业务的主要环节。压缩包共112个文件约1.76MB其中45个Java文件承载Servlet、DAO与过滤器等后端逻辑24个JSP页面负责视图展示另有37个PNG与2个GIF图片资源、2个Markdown说明及1个PDF文档结构清晰、便于二次开发。目前已有263人学习下载。读者可借此理解JSPServletMySQL的经典MVC分层写法掌握登录过滤、借阅记录管理等常见功能的实现思路并直接用于课程作业或项目练手。1. 从一份 JSP 图书管理系统源码说起它到底能跑出什么很多人第一次接触 Java Web都是从一份「JSP 图书馆图书管理系统源码」开始的。它不像 Spring Boot 那样一上来就堆一堆注解和自动配置而是把 Servlet、JSP、JDBC、MySQL 这几样东西赤裸裸地摆在你面前浏览器发一个请求Tomcat 把请求交给某个 ServletServlet 查完 MySQL 再把数据塞进 request最后转发给一个 JSP 页面渲染成 HTML。整条链路短、透明出问题也容易定位。这份源码能解决的核心诉求很具体图书的增删改查、借阅与归还、读者和图书分类管理以及一个能登录的后台。它适合两类人——一类是刚学完 Servlet 和 JSP、想找个完整项目把知识点串起来的学生另一类是要交课程设计、需要一套能改能跑的底子而不是从零搭环境的人。但「能跑」和「跑得好」是两回事下面我按实际落地的顺序把环境、数据库、Servlet 分层、JSP 页面和踩坑一条条讲清楚。2. 环境与数据库先落地Tomcat、JDK 和 MySQL 的最小可用组合2.1 版本怎么选别一上来就追新JSP Servlet 这套技术栈对版本其实挺挑。JDK 建议用 8 或 11Tomcat 用 8.5 或 9.0MySQL 用 5.7 或 8.0。原因很实在老源码里的web.xml大多还是 Servlet 3.0/3.1 的写法Tomcat 10 之后把javax.servlet换成了jakarta.servlet包名一变源码里所有import javax.servlet.*全部报红新手很容易卡在这里以为是自己环境装错了。MySQL 8.0 和 5.7 的差别主要在驱动和连接串。8.0 的驱动类是com.mysql.cj.jdbc.Driver连接串要带时区和 SSL 参数5.7 用com.mysql.jdbc.Driver就行。如果你拿到的源码里写的是 5.7 的驱动直接连 8.0 会报Unknown system variable或者时区错误。提示先看源码lib目录里 mysql-connector 的版本号再决定装哪个 MySQL比反过来省事得多。2.2 建库建表图书管理系统最少要哪几张表一份典型的图书管理系统核心表就那么几张。下面是我一般会先跑通的建表脚本字段名和常见源码基本对得上你可以按自己源码里的实体类微调。-- 图书表存书的基本信息和库存 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, -- 书名 author VARCHAR(50), -- 作者 price DECIMAL(10,2) DEFAULT 0.00, -- 价格 category_id INT, -- 分类外键 stock INT DEFAULT 0, -- 库存借阅时减一 status TINYINT DEFAULT 1 -- 1在架 0下架 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 读者表 CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_name VARCHAR(50) NOT NULL, phone VARCHAR(20), card_no VARCHAR(30) UNIQUE -- 借书证号唯一 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表谁在什么时候借了哪本书 CREATE TABLE borrow ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATETIME DEFAULT CURRENT_TIMESTAMP, return_date DATETIME, -- 为空表示还没还 is_return TINYINT DEFAULT 0 -- 0未还 1已还 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字符集一定用utf8mb4不然书名里出现生僻字或者 emoji 会直接插入失败。stock字段是借阅逻辑的关键借书时stock stock - 1还书时加回来这个加减必须在同一个事务里做否则并发借同一本书会出现库存变负数。2.3 连接串和驱动三个最容易写错的参数数据库建好后源码里通常有一个DBUtil或者db.properties。以 MySQL 8.0 为例连接串长这样# db.properties drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue usernameroot password你的密码serverTimezone不写8.0 驱动会报时区异常useSSLfalse不写控制台会一直刷 SSL 警告虽然不影响运行但看着烦allowPublicKeyRetrievaltrue在 MySQL 8 默认加密方式下不加会连不上。这三个参数是血泪经验新手十有八九要在这里翻一次车。3. Servlet 分层与请求流转把增删改查拆成能维护的结构3.1 为什么不能把所有逻辑塞进一个 Servlet很多入门源码图省事一个BookServlet里用if(action.equals(add))判断到底干什么几百行堆在一起。能跑但改起来痛苦。我一般会按职责拆成三层Servlet 只负责收参数和转发Service 处理业务规则比如借书前先查库存DAO 只管和数据库打交道。这样你改一个 SQL 不会碰到页面逻辑加一个功能也不用翻遍整个文件。以图书列表为例DAO 层大概是这样// BookDao.java public ListBook findAll() throws SQLException { ListBook list new ArrayList(); String sql SELECT id, book_name, author, price, stock FROM book WHERE status 1 ORDER BY id DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setBookName(rs.getString(book_name)); b.setAuthor(rs.getString(author)); b.setPrice(rs.getBigDecimal(price)); b.setStock(rs.getInt(stock)); list.add(b); } } return list; }这里用PreparedStatement而不是拼字符串是为了防 SQL 注入——书名里带个单引号就能把拼接的 SQL 搞崩甚至被注入。try-with-resources保证连接和结果集自动关闭不然连接池很快就被耗光。3.2 借书这个动作事务边界画在哪借书不是一条 SQL 能搞定的要先查库存够不够够的话插一条借阅记录再把库存减一。这三步必须在一个事务里任何一步失败都要回滚。Service 层这么写// BorrowService.java public boolean borrow(int bookId, int readerId) throws SQLException { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查库存加行锁防止并发超借 String checkSql SELECT stock FROM book WHERE id ? FOR UPDATE; PreparedStatement check conn.prepareStatement(checkSql); check.setInt(1, bookId); ResultSet rs check.executeQuery(); if (!rs.next() || rs.getInt(stock) 0) { conn.rollback(); return false; // 库存不足 } // 2. 插入借阅记录 PreparedStatement ins conn.prepareStatement( INSERT INTO borrow(book_id, reader_id) VALUES(?, ?)); ins.setInt(1, bookId); ins.setInt(2, readerId); ins.executeUpdate(); // 3. 扣减库存 PreparedStatement dec conn.prepareStatement( UPDATE book SET stock stock - 1 WHERE id ?); dec.setInt(1, bookId); dec.executeUpdate(); conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { if (conn ! null) conn.rollback(); // 出错回滚 throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }FOR UPDATE是关键它给这一行加排他锁两个用户同时借最后一本书时后一个会等前一个提交后再读到库存 0从而被拦下。不加这个锁两个线程可能都读到 stock1最后库存变成 -1。这是图书管理系统里最典型的并发坑。3.3 中文乱码POST 和 GET 要分开治乱码是 JSP 项目的老朋友。POST 请求在 Servlet 里第一行加request.setCharacterEncoding(UTF-8)就能解决GET 请求的参数编码由 Tomcat 的server.xml决定要在 Connector 上加URIEncodingUTF-8。JSP 页面顶部则统一写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %。三处都对齐 UTF-8乱码基本就绝迹了。数据库连接串里的characterEncodingutf8也算一环缺一个都可能出问题。4. JSP 页面与前端交互列表、分页和表单的常见写法4.1 用 JSTL 渲染图书列表别写 Java 脚本片段老源码里常见% for(...) { %这种脚本片段能跑但难维护。现在更推荐 JSTL 标签。Servlet 把 list 放进 requestJSP 里这样渲染% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table border1 trthID/thth书名/thth作者/thth库存/thth操作/th/tr c:forEach items${bookList} varb tr td${b.id}/td td${b.bookName}/td td${b.author}/td td${b.stock}/td tda hrefborrow?bookId${b.id}借阅/a/td /tr /c:forEach /table${b.bookName}会自动调用实体的 getter前提是实体类属性名和 EL 表达式对得上。如果页面显示空白先检查实体类有没有写 getter或者属性名大小写是否一致——EL 对大小写敏感bookname和bookName是两个东西。4.2 分页limit 的两个参数怎么算图书多了必须分页。核心 SQL 是LIMIT offset, sizeoffset是跳过的行数size是每页条数。假设每页 10 条第 page 页从 1 开始的 offset 就是(page - 1) * 10。// BookDao.java 分页查询 public ListBook findByPage(int page, int size) throws SQLException { ListBook list new ArrayList(); String sql SELECT * FROM book ORDER BY id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * size); // offset ps.setInt(2, size); // 每页条数 ResultSet rs ps.executeQuery(); while (rs.next()) { // 封装 Book 对象略 } } return list; }总页数要单独查一次SELECT COUNT(*) FROM book再除以 size 向上取整。常见错误是把 offset 和 size 写反结果第一页就查出全部数据或者一条都没有。另外LIMIT在 MySQL 里是从 0 开始算 offset 的别用页码直接当 offset。4.3 表单提交与刷新一个容易被忽略的体验问题新增图书的表单提交后如果直接转发回列表页用户按 F5 会重复提交。正确做法是提交成功后用response.sendRedirect(bookList)重定向而不是forward。重定向会让浏览器重新发一次 GET 请求刷新就不会重复插入。这个细节很多源码没处理测试时反复点提交就多出好几条重复数据。5. 避坑与排查这份源码跑不起来时先看这几条5.1 报 404路径和 web.xml 对不上现象是访问某个功能提示 404。原因通常是 Servlet 的WebServlet(/bookList)注解路径和 JSP 里表单 action 写的路径不一致或者web.xml里url-pattern配错。解决方法是先在浏览器地址栏直接敲 Servlet 的映射路径确认 Servlet 本身能通再回头查页面里的链接。另外注意项目部署的 context path如果部署成了/library那访问地址要带这个前缀。5.2 报 500 且堆栈指向 ClassNotFound驱动没进 lib现象是启动或连接数据库时抛ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因是 mysql-connector 的 jar 没放进WEB-INF/lib目录或者放进去但没重新部署。解决方法是把对应版本的 jar 拷进WEB-INF/lib在 IDE 里刷新项目并重新部署。注意别只加到项目的 build path 里Tomcat 运行时只认WEB-INF/lib下的包。5.3 中文变问号三处编码没对齐现象是页面显示「????」或者数据库里存进去就是乱码。原因是 JSP 的 pageEncoding、Servlet 的 setCharacterEncoding、数据库连接串的 characterEncoding、以及 MySQL 表的字符集这四处只要有一处不是 UTF-8 就会出问题。解决方法是逐处检查建表时显式写DEFAULT CHARSETutf8mb4连接串加characterEncodingutf8Servlet 里 POST 请求先设编码再取参数。5.4 借阅后库存不变或变负事务和锁没做对现象是借书成功但库存没减或者并发下库存变成负数。原因是扣库存和插记录不在一个事务里或者查库存时没加FOR UPDATE。解决方法是按 3.2 的写法把三步包进一个事务查库存时加行锁。如果只是单机测试不容易复现可以用两个浏览器同时点借阅来验证。5.5 页面样式全丢静态资源路径写成了相对路径现象是 JSP 页面能打开但 CSS、图片全没了。原因是静态资源用了相对路径而 Servlet 转发后浏览器地址栏的路径变了相对路径就找不到了。解决方法是用${pageContext.request.contextPath}/css/style.css这种带 context path 的绝对路径或者用c:url标签生成。这个坑在转发和重定向混用时特别常见。6. 从能跑到好用连接池、参数校验和一次压测的收尾技巧把功能跑通只是第一步真正让这套系统「能用」还得补几样东西。第一是连接池源码里如果每次请求都DriverManager.getConnection并发一上来数据库连接数就爆了。换成 Druid 或 HikariCP配置initialSize、maxActive、maxWait三个参数就够用maxActive一般设 20 到 50maxWait设 3000 毫秒超时直接抛异常而不是无限等。第二是参数校验。前端表单的required靠不住Servlet 里必须再校验一遍书名不能为空、价格不能为负、借书证号格式要对。校验不通过就带着错误信息转发回表单页别直接抛 500 给用户看。第三是验证方法。改完代码别只点几下页面就算完我一般会做两件事一是用SELECT * FROM borrow WHERE is_return 0核对借阅记录和库存是否对得上二是用 JMeter 或者简单的多线程脚本模拟 50 个并发借同一本书看库存最终是不是 0 而不是负数。这一步能验证事务和锁到底有没有生效。# 用 ab 做一次简单并发测试模拟 50 个并发、共 200 次借阅请求 ab -n 200 -c 50 http://localhost:8080/library/borrow?bookId1readerId1跑完看数据库里book表的 stock 和borrow表的记录数是否一致如果 stock 变成负数说明FOR UPDATE没生效或者事务没包住。这个测试比看代码靠谱得多。最后说个习惯我拿到任何一份 JSP 源码第一件事不是急着运行而是先看WEB-INF/lib里有哪些 jar、web.xml的版本、以及数据库连接配置写在哪。这三样决定了后面 80% 的坑长什么样。把环境对齐、事务写对、编码统一这套 JSP 图书管理系统就能从「勉强能跑」变成「敢拿去交作业甚至改造成自己的项目」。希望帮到你。本文还有配套的精品资源点击获取
返回列表