尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaWeb图书管理系统毕设:Servlet+JDBC分页与借阅事务

JavaWeb图书管理系统毕设:Servlet+JDBC分页与借阅事务 简介面向计算机专业毕业设计场景的Java Web图书管理系统论文文档适合正在选题、搭建论文框架或需要参考系统实现思路的本科生与指导教师。整份资料以1个docx文件交付压缩包约3.47MB内容按学位论文规范编排从摘要、前言一直延伸到结论、参考文献与致谢结构完整。文档围绕Java、MySQL、B/S结构与JSP等技术展开依次覆盖系统开发环境、需求分析、可行性分析、项目设计目标与原则、系统流程、架构设计、数据库实体与表设计以及登录、管理员、前台首页和用户功能模块的实现与测试。读者可据此了解图书借阅管理类课题的论述逻辑、章节组织方式、技术选型说明与功能模块拆分方法也能借鉴需求分析、数据库表设计、系统测试等部分的写作范式。目前已有93人学习下载适合作为毕业设计选题参考、论文模板与开发思路梳理材料。1. 选题落地JavaWeb 图书管理系统到底要交付什么很多同学拿到 JavaWeb 图书管理系统这个题目第一反应是打开 IDEA 新建工程结果三天过去还在和 Tomcat 404、驱动加载失败较劲。真正决定毕设能否顺利通过的不是堆了多少框架而是图书增删改查、借阅归还、分页查询这几条主链路能不能稳定跑通以及论文里的用例图、流程图、E-R 图和代码是否对得上。这个选题的交付物由三块拼成一份数据库脚本可复现、本地能启动的 JavaWeb 工程一套带借阅状态流转的核心业务代码一份把需求、设计、实现、测试串起来的论文。它适合学过 Servlet、JSP、JDBC 的本科与专科同学也适合想拿一个完整案例把 JavaWeb 知识点串一遍的自学者。下面按环境搭建、数据库设计、业务实现、并发验证与论文对齐推进每步都给到能直接抄的命令和配置。2. 用 IDEA Maven 把 JavaWeb 图书管理系统的骨架跑起来2.1 JDK、Tomcat、MySQL 版本组合与 IDEA 2026 创建 Web 项目版本组合是第一个分水岭选错会让后面所有教程都对不上。组件稳妥选择备选常见坑JDK8 或 1117JDK 17 配 Tomcat 8 会因模块化报反射错误Tomcat9.0.x8.5.xTomcat 10 起包名变成 jakarta.servlet网上 javax.servlet 教程全失效MySQL8.05.78.0 驱动类名是 com.mysql.cj.jdbc.DriverMaven3.83.6没配国内镜像会卡在依赖下载IDEA2023 以上2026新版要靠 Facet 识别 Web 模块旧项目导入常丢 Artifact核心矛盾在 Tomcat 9 与 10 的包名切换。Tomcat 9 用javax.servlet.*Tomcat 10 用jakarta.servlet.*如果你导入的是网上主流的javax.servlet-api却在 IDEA 里配了 Tomcat 10启动时会直接抛ClassNotFoundException: javax.servlet.http.HttpServlet。毕设求稳锁定 Tomcat 9.0.x。IDEA 2026 创建项目的入口是File → New → Project → Maven勾选Create from archetype选org.apache.maven.archetypes:maven-archetype-webapp。生成后如果src/main/java不显示蓝色源码根去Project Structure → Modules → Sources手动标记一次否则新建的 Servlet 不会被编译。2.2 pom.xml 依赖配置与标准目录结构Servlet、JSP 这两个 API 只在编译期需要运行时由 Tomcat 提供scope 必须锁成provided打成 war 时如果混进去会和容器自带的类冲突报NoSuchMethodError。dependencies !-- Servlet API编译期使用运行时由 Tomcat 提供 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API同样只在编译期需要 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- MySQL 8 驱动运行时必须打进 war -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Druid 连接池避免每次请求都新建物理连接 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies目录结构按职责拆开DAO 不放业务规则Service 不碰 SQLsrc/main/java/com/library/ ├── dao/ // 只负责 SQL 与 ResultSet 映射 ├── service/ // 业务规则、事务边界 ├── servlet/ // 接收请求、封装参数、转发视图 ├── filter/ // 登录与权限过滤 └── util/ // DBUtil、PageResult src/main/resources/ └── db.properties // 数据库连接参数 src/main/webapp/ ├── WEB-INF/views/ // JSP 放这里不能被直接访问 └── static/2.3 用 db.properties 和 DBUtil 打通 MySQL 连接连接参数硬编码在 Java 里换台机器答辩就要重新编译。放进resources/db.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的密码DBUtil用 Druid 读取配置并顺手验证连通性public class DBUtil { private static final DataSource DS; static { Properties props new Properties(); try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { props.load(in); } catch (IOException e) { throw new ExceptionInInitializerError(db.properties 读取失败: e.getMessage()); } DS DruidDataSourceFactory.createDataSource(props); } public static Connection getConnection() throws SQLException { return DS.getConnection(); } }serverTimezone不写会报时区错误characterEncodingutf8不写中文书名会乱码这两个参数是 MySQL 8 项目里出现频率最高的两个坑。写完先在main里调一次DBUtil.getConnection()能拿到连接再接 Tomcat能把问题范围缩小一半。配置 Tomcat 的位置在Run → Edit Configurations → → Tomcat Server → LocalApplication server 选 9.0 目录Deployment页签添加artifact时选war explodedApplication context 填/library启动后访问http://localhost:8080/library/hello。如果 404先看 Deployment 里是否真的加了 Artifact再看访问路径前缀对不对。3. 数据库设计从图书管理系统用例图到三张核心表的建表 SQL3.1 用例图与借阅流程图画完再动表能省掉一半返工用例图和流程图不是论文装饰。角色是读者还是管理员直接决定sys_user要不要 role 字段借阅流程图的分支决定borrow_record的状态枚举。常见做法是先画两张图用例图里读者有查询图书、借阅、归还、查看我的借阅管理员多出图书维护、用户管理、借阅审批借阅流程图画出「图书是否可借 → 是则扣库存写记录否则提示无库存」这条主分支。图一旦定下来表结构基本就锁死了角色两条 → role 字段借阅分支 → status 字段「查看我的借阅」→ borrow_record 上按 user_id 建索引。3.2 图书、用户、借阅三张表的建表 SQLCREATE DATABASE IF NOT EXISTS library DEFAULT CHARSET utf8mb4; CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(128) NOT NULL, author VARCHAR(64), publisher VARCHAR(128), total INT NOT NULL DEFAULT 0 COMMENT 馆藏总数, available INT NOT NULL DEFAULT 0 COMMENT 当前可借数量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 存摘要不存明文, real_name VARCHAR(32), role TINYINT NOT NULL DEFAULT 0 COMMENT 0读者 1管理员 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, borrow_time DATETIME NOT NULL, due_time DATETIME NOT NULL, return_time DATETIME, status TINYINT NOT NULL DEFAULT 0 COMMENT 0借出 1已还 2逾期, INDEX idx_user_status (user_id, status), INDEX idx_book_status (book_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;索引不是越多越好。idx_book_status服务于「这本书还在谁手上」类查询idx_user_status服务于「我的未还书目」两个覆盖了 90% 的借阅查询场景。isbn 上加 UNIQUE 是为了防止重复录入同一种书。3.3 available 冗余字段和状态字段的取舍available是可以用聚合查询算出来的为什么还要单独存因为借阅是高频写操作每次都去COUNT一遍 borrow_record 再判断库存命中一多就锁表。冗余一个字段读的时候快代价是必须保证它和借阅记录一致——这个一致性只能靠事务和行锁兜住第四章展开。状态字段用TINYINT而不是字符串理由有三点省空间、Java 侧switch比字符串比较快、论文 E-R 图里画起来干净。缺点是可读性差所以建表时必须写COMMENT答辩时被问到「0 和 1 分别代表什么」能立刻答上来。状态值含义可执行操作0已借出未归还归还、续借1已归还无2已逾期归还并计算罚金外键约束在这个场景下我一般不强加FOREIGN KEY因为删书时外键会阻止删除或级联删掉借阅历史把「下架」和「删除」搅在一起。改成逻辑删除加应用层校验E-R 图里照样画关系连线不影响论文表达。4. 核心业务图书 CRUD、借阅归还与分页查询的 Servlet JDBC 实现4.1 从 SQL LIMIT 到 PageResult 封装的分页查询图书列表是访问量最大的页面必须分页。SQL 层用LIMIT offset, sizeSELECT id, isbn, title, author, publisher, total, available FROM book WHERE title LIKE CONCAT(%, ?, %) OR author LIKE CONCAT(%, ?, %) ORDER BY id DESC LIMIT ?, ?;Java 侧统一封装结果前端 JSP 只认一个对象public class PageResultT { private ListT rows; // 当前页数据 private long total; // 总记录数 private int pageNum; // 当前页码从 1 开始 private int pageSize; // 每页条数 private int pages; // 总页数 ceil(total / pageSize) // getter / setter 省略 }Servlet 处理参数时要夹紧边界否则用户手改 URL 传pageNum0或负数offset会变负MySQL 直接报语法错误WebServlet(/book/list) public class BookListServlet extends HttpServlet { private final BookDao bookDao new BookDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int pageNum parsePositiveInt(req.getParameter(pageNum), 1); int pageSize parsePositiveInt(req.getParameter(pageSize), 10); pageSize Math.min(pageSize, 100); // 防止一次拉全表 String keyword req.getParameter(keyword); int offset (pageNum - 1) * pageSize; PageResultBook page bookDao.pageQuery(keyword, offset, pageSize); req.setAttribute(page, page); // JSP 放在 WEB-INF 下只能由 Servlet 转发进入 req.getRequestDispatcher(/WEB-INF/views/book_list.jsp).forward(req, resp); } private int parsePositiveInt(String v, int def) { try { int n Integer.parseInt(v); return n 0 ? n : def; } catch (Exception e) { return def; } } }MAX(pageSize)这行别省。答辩演示时如果被点到pageSize9999数据库会把整张表读进内存页面卡住很难看。LIKE的两个问号要分别 setString 两次不能复用同一个参数索引。4.2 借阅与归还的事务边界FOR UPDATE 加库存双保险借阅是整篇代码里唯一必须加锁的地方。要同时完成三件事占用这本书、扣减 available、写一条借阅记录。任何一步失败都要整体回滚否则会出现「记录写了但库存没扣」这种脏数据。public boolean borrow(long userId, long bookId) throws SQLException { String lockSql SELECT available FROM book WHERE id ? FOR UPDATE; String decrSql UPDATE book SET available available - 1 WHERE id ? AND available 0; String insertSql INSERT INTO borrow_record(user_id, book_id, borrow_time, due_time, status) VALUES(?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), 0); Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 行锁锁住这本图书记录其他事务此时排队 try (PreparedStatement ps conn.prepareStatement(lockSql)) { ps.setLong(1, bookId); try (ResultSet rs ps.executeQuery()) { if (!rs.next() || rs.getInt(available) 0) { conn.rollback(); return false; // 库存不足直接失败 } } } // 2. 扣减库存WHERE available 0 是第二道保险 try (PreparedStatement ps conn.prepareStatement(decrSql)) { ps.setLong(1, bookId); if (ps.executeUpdate() 0) { conn.rollback(); return false; } } // 3. 写借阅记录 try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setLong(1, userId); ps.setLong(2, bookId); ps.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还要在 UPDATE 里写available 0因为并不是所有借阅入口都走这条加锁路径可能还有后台批量导入的调用。WHERE available 0让数据库层承担最终兜底executeUpdate()返回 0 就说明实际没扣成。归还逻辑更简单但要防重复归还先按borrow_record.id查状态只有当status 0或2时才置为1并回补 available同一条 SQL 用status IN (0,2)作为条件靠影响行数判断是否成功。提示conn.setAutoCommit(true)一定要在close()前还原。连接池归还的连接如果还处于手动提交状态下一个请求拿到它就会出现莫名其妙的丢失更新。4.3 登录会话与权限过滤器别让读者改到后台数据登录成功后把用户对象放进 session过滤器负责拦人WebFilter(/admin/*) public class AdminFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); User login session null ? null : (User) session.getAttribute(loginUser); if (login null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } if (login.getRole() ! 1) { // 非管理员一律拒绝 response.sendError(403, 无权限访问); return; } chain.doFilter(req, resp); } }/admin/*覆盖所有后台 Servlet。真实项目里还要在登录 Servlet 上做密码校验明文入库是毕设被扣分的重灾区最省事的做法是注册时存MD5(username password salt)salt 每个用户随机一个字符存进表。虽然 MD5 已经不适合生产但答辩场景下能讲清「为什么不存明文」就够。5. 答辩前的并发验证与论文图表对齐5.1 用一段多线程测试证明 FOR UPDATE 真的生效借阅加锁写完了但肉眼看不出来。写一个 10 线程抢 1 本书的验证程序把book.available手动改成 1 再跑public static void main(String[] args) throws Exception { final long bookId 1L; ExecutorService pool Executors.newFixedThreadPool(10); CountDownLatch start new CountDownLatch(1); // 让 10 个线程同时起跑 AtomicInteger success new AtomicInteger(); for (int i 0; i 10; i) { final long userId 100 i; pool.submit(() - { try { start.await(); if (new BorrowService().borrow(userId, bookId)) { success.incrementAndGet(); } } catch (Exception ignored) { } }); } start.countDown(); pool.shutdown(); pool.awaitTermination(10, TimeUnit.SECONDS); System.out.println(成功借出次数 success.get() 期望 1); }结果必须是 1。如果打出 2 以上先查borrow里事务有没有真的开起来再确认表引擎是不是 InnoDB——MyISAM 不支持行锁FOR UPDATE完全失效。这个测试值得在论文的「系统测试」章节里留一张截图比单纯贴测试用例文字有说服力。5.2 论文图表与代码的对应关系答辩老师最爱问「你这个图在这段代码哪里实现的」。提前做一张对照表被问到直接翻代码位置比现场解释高效得多。论文图表对应代码位置高频追问用例图AdminFilter与各 Servlet 的 URL 映射读者和管理员权限边界在哪拦借阅流程图BorrowService.borrow库存为 0 时怎么处理E-R 图三张建表 SQLavailable 冗余后如何保持一致时序图Servlet → Service → DAO 调用链事务在哪一层开启、在哪一层提交状态图borrow_record.status 的 0/1/2 流转逾期状态怎么自动置位时序图最容易画错很多人把事务开在 DAO 里图上也这么标实际上一个完整借阅涉及两次写操作事务必须开在 Service 层才能覆盖住DAO 只负责执行单条 SQL。另外逾期状态建议加一个定时任务或登录时惰性判断UPDATE borrow_record SET status 2 WHERE status 0 AND due_time NOW()这句放在管理员进入借阅管理页时执行一次即可不必引入 Quartz。本文还有配套的精品资源点击获取
返回列表