JavaWeb图书管理系统毕业设计:Servlet鉴权与借阅库存事务实现

JavaWeb图书管理系统毕业设计:Servlet鉴权与借阅库存事务实现 简介围绕 Java Web 图书管理系统撰写的毕业设计论文文档面向计算机相关专业应届生、需要完成毕设选题与论文写作的本科或专科学生也可供 Java Web 入门者参考。文档完整覆盖前言、系统开发环境、需求分析、可行性分析、项目设计目标与原则、系统流程分析、架构设计、数据库实体与表设计、系统实现及系统测试等章节技术选型涉及 Java、MySQL、B/S 结构与 JSP并按登录、管理员、前台首页、用户功能等模块展开实现说明能帮助读者梳理毕设论文的目录结构、需求描述与数据库设计思路。资源包内共 1 个 docx 文件约 3.47MB内容为单篇论文正文便于直接查阅、修改与排版参考。目前已有 93 人学习适合作为图书管理系统类毕设的选题参考、开题素材与论文撰写范本对不熟悉论文结构的同学也能据此了解各章节篇幅安排与撰写重点。1. 一份 JavaWeb 图书管理系统的毕业设计卡住人的从来不是页面选到这个题目的人第一反应往往是去搜一套现成的前端模板把后台管理界面换个配色觉得这样就能交差。真正坐到答辩席上或者被导师要求现场改一个功能时问题全都出在另外几个地方tushujieyue表里的sfsh字段到底是干什么的、借阅时库存怎么扣、为什么管理员登进去还能看到用户菜单。这套毕业设计用的技术栈是 JSP Servlet JavaBean MySQLB/S 结构浏览器端只负责提交表单和渲染业务逻辑和权限判断全部放在服务器端完成。系统角色分成管理员、普通用户和前台首页三个入口。管理员管图书分类、热门图书、入库、借阅、归还、论坛和系统管理用户只看到借阅、归还和自己的收藏。模块看着很多底层其实是两条主线一条是账号与权限一条是图书从入库到借出、再回到库存的状态流转。把这两条线的数据库表和接口理顺剩下的页面基本都是重复的增删改查。这套东西适合正在做计算机方向毕业设计、需要一份能跑能讲的项目的人也适合想借这个题目把 Servlet 生命周期、Session、过滤器这些基础补回来的初学者。下面按技术栈选型、数据库设计、核心代码实现、部署排错的顺序往下拆。2. JSP Servlet MySQL 的 B/S 分层与工程骨架落地2.1 为什么这套技术栈在毕设场景里仍然是稳妥选择JSP 本质上是服务端脚本容器把它编译成 Servlet 再执行所以它天然能拿到 Java 的全部类库。这套系统里它承担的是视图层接收请求参数、循环渲染图书列表、根据 Session 里的角色决定菜单显示哪些项。JavaBean 负责在页面和数据库之间搬运数据典型的就是一个Tushu实体类配一整套getXxx/setXxx。Servlet 控制流程判断该查库还是该转发。三者拼起来就是最朴素的 MVC。B/S 结构的实际好处在于部署客户端不需要装任何东西一台装了浏览器的机器就能用升级只改服务器端。对毕业设计来说这个特点在答辩演示环节特别值钱——导师用自己电脑打开地址就能看到不用配环境。常见的替代方案是 SpringBoot MyBatis Vue。功能上没差别但答辩时被问「一个请求进来经过几个组件、Session 在哪一步写入」这种问题用原生 Servlet 的人答得出来用自动配置框架的人常常答不上来。如果时间只有两三周我不建议换栈把 JSP 这条线走透更划算。2.2 从 web.xml 到目录结构一个能跑起来的工程骨架先在 IDE 里建一个 Dynamic Web Project源码目录和网页目录分开Servlet 全部放在com.library.servlet包下避免后期找不到类。library-system/ ├── src/ │ └── com/library/ │ ├── dao/ # 数据库访问每个表一个 Dao │ ├── entity/ # 实体类字段与表字段一一对应 │ ├── servlet/ # 控制器处理表单提交 │ ├── filter/ # 登录鉴权、编码过滤 │ └── util/ # DBUtil 连接工具类 └── WebContent/ # IDEA 下叫 web ├── WEB-INF/ │ ├── web.xml │ └── lib/ # 放 mysql-connector、jstl 的 jar ├── admin/ # 管理员页面 ├── user/ # 普通用户页面 ├── login.jsp └── index.jsp老项目习惯在web.xml里逐个注册 Servlet 和过滤器照抄的时候注意url-pattern前要带斜杠servlet servlet-nameLoginServlet/servlet-name servlet-classcom.library.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping filter filter-nameAuthFilter/filter-name filter-classcom.library.filter.AuthFilter/filter-class /filter filter-mapping filter-nameAuthFilter/filter-name url-pattern/admin/*/url-pattern /filter-mappingServlet 3.0 以后更省事的写法是直接在类上加WebServlet(/login)和WebFilter(/admin/*)不用改配置文件。两种方式不能对同一个类重复用否则容器启动时会因为重复映射报错。2.3 开发环境与依赖版本怎么定版本错配是这套项目最容易翻车的地方下表是我一般会锁定的组合组件推荐版本说明JDK1.8高于 8 时老版本 Tomcat 编译 JSP 会报错Tomcat8.5 或 9.09.0 起包名是javax.servlet与 SpringBoot 2.x 之后的jakarta不同MySQL5.7 或 8.08.0 必须配时区参数mysql-connector-java5.1.47 / 8.0.33驱动类名和 URL 参数要跟 MySQL 版本对应JSTL1.2页面里用c:forEach遍历图书列表MySQL 8.0 的 JDBC URL 要写成这样否则启动时报时区错误jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse驱动类名也从com.mysql.jdbc.Driver换成了com.mysql.cj.jdbc.Driver。用 5.1 的驱动去连 8.0 的库症状是连接能建立但查询某些字段乱码排查半天找不到原因。提示把 jar 包放到WEB-INF/lib下而不是只在 IDE 的 Build Path 里加。IDE 里加只保证编译通过打包后 Tomcat 找不到类会抛ClassNotFoundException。3. 数据库表设计拼音命名、字段类型与 E-R 到建表的映射3.1 实体拆分与表命名约定这份设计里所有表名和字段名都用了拼音全拼rementushu、tushujieyue、tushuguihai、tushuruku账号表叫allusers。这种命名在正式项目里会被骂但在毕设里反而有好处答辩时口头描述和代码里看到的是同一个词不会出现「老师说的图书表到底是 book 还是 tushu」这种对不上的情况。实体拆分上对应四类对象。allusers存账号用cx字段区分角色管理员和普通用户共用一张表登录后靠cx的值决定跳哪个目录这是最省事的做法也解释了为什么管理员登录后可能看到用户菜单——判断条件写错了。rementushu是图书主表包含分类、图片路径、作者、出版社、发行日期、借阅价格、数量和详情。tushuruku记录入库流水tushujieyue和tushuguihai分别记录借出和归还。3.2 字段类型取舍原始设计里几乎所有字段都是varchar(50)包括数量、价格、借阅天数。这样写能跑但排序和统计会出问题shuliang存成字符串后按数量排序会得到 1、10、2 这种字典序结果。下面是我会做的调整字段原设计建议类型理由shuliangvarchar(50)int(11)需要做库存扣减和比较运算jieyuejiagevarchar(50)decimal(10,2)金额计算不能有浮点误差jieyuetianshuvarchar(50)int(11)用于计算应还日期zongjiagevarchar(50)decimal(10,2)单价乘天数必须可累加addtimevarchar(50)datetime时间范围查询和排序tupianvarchar(50)varchar(255)存相对路径50 位放不下sfsh和shhf这两个字段最容易被人忽略。sfsh是是否审核shhf是审核回复管理员在借阅管理里点通过或驳回时改的就是这两个值。如果不加这两个字段借阅申请提交后就直接生效没有审核环节业务上说不通。3.3 建表 SQL 与约束按调整后的类型重写核心两张表字符集统一用utf8mb4避免书名里出现生僻字或符号时插入失败CREATE TABLE allusers ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) DEFAULT NULL COMMENT 登录账号, pwd varchar(50) DEFAULT NULL COMMENT 密码毕设不做加密, cx varchar(50) DEFAULT NULL COMMENT 角色管理员/用户, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE tushujieyue ( id int(11) NOT NULL AUTO_INCREMENT, jieyuebianhao varchar(50) DEFAULT NULL COMMENT 借阅编号业务主键, tushumingcheng varchar(50) DEFAULT NULL, shuliang int(11) DEFAULT 1, jieyuetianshu int(11) DEFAULT 30, jieyuejiage decimal(10,2) DEFAULT 0.00, zongjiage decimal(10,2) DEFAULT 0.00, jieyueriqi datetime DEFAULT NULL, yonghuming varchar(50) DEFAULT NULL COMMENT 借阅人, sfsh varchar(50) DEFAULT 待审核 COMMENT 是否审核, shhf varchar(50) DEFAULT NULL COMMENT 审核回复, PRIMARY KEY (id), KEY idx_yonghuming (yonghuming), KEY idx_sfsh (sfsh) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_username保证账号不重复这个约束比在 Servlet 里手动查一次再插入可靠得多并发提交时也不会漏。idx_yonghuming和idx_sfsh是给「我的借阅」列表和管理员审核列表准备的不带索引时数据量上千后页面会明显变慢。zongjiage我是按单价乘数量算出来落库而不是每次查询时现算好处是历史订单不受图书调价影响。注意addtime保留原设计的话插入时记得用new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).format(new Date())转成字符串改成datetime后就可以交给 SQL 的NOW()。4. 登录鉴权与借阅归还链路的 Servlet 实现4.1 登录从表单提交到 Session 落地登录是所有模块的入口也是权限分流的唯一位置。核心逻辑是查到记录后把用户对象塞进 Session再按cx决定跳转目录WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String pwd req.getParameter(pwd); // 用 PreparedStatement禁止字符串拼接 SQL String sql SELECT id, username, pwd, cx FROM allusers WHERE username ? AND pwd ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, pwd); ResultSet rs ps.executeQuery(); if (rs.next()) { HttpSession session req.getSession(); session.setAttribute(uid, rs.getInt(id)); session.setAttribute(username, rs.getString(username)); session.setAttribute(cx, rs.getString(cx)); // 角色 // 按角色分流管理员进 /admin用户进 /user String target 管理员.equals(rs.getString(cx)) ? /admin/index.jsp : /user/index.jsp; resp.sendRedirect(req.getContextPath() target); } else { req.setAttribute(msg, 用户名或密码不正确); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } catch (SQLException e) { throw new ServletException(登录查询失败, e); } } }几个参数别写错。req.setCharacterEncoding(UTF-8)必须在第一次取参数之前调用写在getParameter之后就完全无效。密码这里直接明文比对毕设阶段够用真要严谨一点就在 Dao 层加一次 MD5 再做比较。sendRedirect会让浏览器重新发一次请求地址栏会变如果想保留地址用forward。登录成功后必须用重定向否则用户按 F5 会重复提交表单。4.2 用 Filter 统一拦截未登录请求如果只在每个 JSP 顶部写if (session.getAttribute(username) null)漏写一个页面就是漏洞。正确做法是把判断收敛到过滤器里WebFilter(urlPatterns {/admin/*, /user/*}) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); // 不主动创建新会话 if (session null || session.getAttribute(username) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; // 中断不放行到目标资源 } // 角色校验非管理员的账号访问 /admin 一律打回 String uri req.getRequestURI(); if (uri.contains(/admin/) !管理员.equals(session.getAttribute(cx))) { resp.sendRedirect(req.getContextPath() /user/index.jsp); return; } chain.doFilter(request, response); } }getSession(false)和getSession(true)的区别是关键前者在会话不存在时返回 null后者会顺手建一个新会话导致过滤器永远判断为「已登录」。chain.doFilter之前的所有return都表示拦截这也是为什么 4.1 里那次重定向之后不能继续往下走。4.3 借阅与归还库存扣减和状态流转借阅这一步同时动两张表必须放在一个事务里否则插入了借阅记录、扣库存失败用户就白借了一本public boolean borrow(String bianhao, int tushuId, String yonghuming, int days, BigDecimal price) throws SQLException { String checkSql SELECT shuliang FROM rementushu WHERE id ? FOR UPDATE; String insertSql INSERT INTO tushujieyue(jieyuebianhao, tushumingcheng, shuliang, jieyuetianshu, zongjiage, jieyueriqi, yonghuming, sfsh) VALUES(?, ?, 1, ?, ?, NOW(), ?, 待审核); String updateSql UPDATE rementushu SET shuliang shuliang - 1 WHERE id ? AND shuliang 0; Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 开启事务 try (PreparedStatement ps1 conn.prepareStatement(checkSql)) { ps1.setInt(1, tushuId); ResultSet rs ps1.executeQuery(); if (!rs.next() || rs.getInt(shuliang) 0) { conn.rollback(); return false; // 无库存直接回滚退出 } } try (PreparedStatement ps2 conn.prepareStatement(insertSql)) { ps2.setString(1, bianhao); ps2.setString(2, yonghuming); ps2.setInt(3, days); ps2.setBigDecimal(4, price.multiply(BigDecimal.valueOf(days))); ps2.executeUpdate(); } try (PreparedStatement ps3 conn.prepareStatement(updateSql)) { ps3.setInt(1, tushuId); if (ps3.executeUpdate() 0) { conn.rollback(); return false; } } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }SELECT ... FOR UPDATE会在这一行上加排他锁防止两个人同时借最后一本时都读到shuliang 1。UPDATE ... AND shuliang 0是第二道保险即便并发绕过了前面的检查这条语句的影响行数也会是 0直接触发回滚。归还的逻辑反过来往tushuguihai插一条记录把tushujieyue里对应记录的sfsh改成「已归还」再给rementushu的shuliang加一。4.4 JDBC 工具类与 SQL 注入把连接获取封装成一个静态方法五个 Dao 共用改数据库密码时只动一处public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/library ?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASS 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); // 8.0 驱动 } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL 驱动未加载); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASS); } }所有查询条件都必须走setString/setInt不要写成WHERE username username 。后者只要输入 or 11就能绕过登录是图书管理系统里最常见的低级漏洞答辩时被问到会很难看。5. 本地部署与联调排错404、乱码、事务不生效怎么定位5.1 部署后 404 的三层排查第一层看访问路径对不对。项目部署后的地址是http://localhost:8080/项目名/login项目名是 IDEA 里 Artifact 的 Context Path不是文件夹名。第二层看WEB-INF/lib里有没有mysql-connector-java和jstl的 jarTomcat 只在WEB-INF/lib和 Tomcat 自己的lib里找类。第三层看控制台有没有ClassNotFoundException或NoClassDefFoundError有的话基本就是第二层的问题。我在 IDEA 里一般会先在 Run/Debug Configurations 的 Deployment 页确认 Artifact 已经添加再检查 Output Layout 里依赖有没有被展开到WEB-INF/lib。5.2 中文乱码的三处设置乱码要分请求和响应两边处理。请求侧在 Filter 里统一设request.setCharacterEncoding(UTF-8)放在过滤器链的最前面。响应侧在每个 JSP 页面头部写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %或者统一定义response.setContentType(text/html;charsetUTF-8)。数据库侧确认建表时用的是utf8mb4JDBC URL 里带了characterEncodingutf8。三处缺一处就可能出现表单提交后书名变问号、或者页面显示正常但入库变乱码的情况。5.3 用日志和 EXPLAIN 定位借阅失败借阅操作返回失败但没报错时先确认事务里哪一步影响行数为 0。把ps3.executeUpdate()的返回值打到日志里是 0 就说明shuliang 0条件不成立也就是库存本来就不够。如果整段 SQL 执行很慢把语句拿出来单独跑一次EXPLAINEXPLAIN SELECT shuliang FROM rementushu WHERE id 3\G看type列是不是const或eq_refrows列是不是 1。如果显示ALL说明主键没有命中的条件检查id传进来的是不是字符串——JSP 表单提交的都是字符串忘了Integer.parseInt就会出现隐式类型转换索引失效。提示调试事务相关的问题时把 MySQL 的general_log打开能直接看到 JDBC 实际发过去的 SQL 长什么样比在 Java 代码里打日志快得多。最后一个技巧毕设答辩前把管理员、用户、前台三个入口各走一遍完整流程——管理员加一本库存为 1 的书用户借走用另一个浏览器窗口再借一次看是不是真的被拦住。这一步能验证FOR UPDATE加锁和shuliang 0条件是同时在起作用的比看代码可靠。本文还有配套的精品资源点击获取