JavaWeb+MySQL图书馆管理系统源码部署与借阅事务实践

发布时间:2026/10/23 22:23:47
JavaWeb+MySQL图书馆管理系统源码部署与借阅事务实践

简介:这份 JavaWeb 图书馆管理系统源码面向高校课程设计、毕业设计与 JavaWeb 入门实践者,基于 Servlet、JSP、JavaBean 与 MVC 分层,配合 MySQL 实现图书信息、读者账户、借阅归还及管理员权限等核心业务,可帮助读者理解请求处理、业务逻辑与数据库读写的完整链路,适合需要熟悉传统 Web 项目结构、准备课程作业或进行二次开发的学生与开发者。压缩包共 209 个文件,约 1.91MB,内含 51 个 JSP 页面、32 个 Java 源文件和 32 个 class 文件、9 个 jar 依赖,并附带 SQL 脚本、properties/XML 配置及 MySQL 数据文件,覆盖视图、控制层、DAO、实体类等模块。已有 359 人学习。项目按图书、读者、借阅、管理员等业务拆分代码,读者可参考数据库表设计与建库脚本、登录鉴权与借还流程、JSP 与 Servlet 交互及分层目录组织,用于课设复现、功能扩展和代码对照。目录层次较清晰,便于按模块定位文件,对图书馆业务建模与权限控制也有较好参考价值。

1. 资料室那本借阅登记册,被一套 JavaWeb 图书馆管理系统接管了

某高校资料室的管理员拿着一本卷边的借阅登记册,谁借了什么书、哪天该还,全靠一支笔和记性。有人提议做个系统,预算为零,能用的只有一台装了 JDK 的旧机器。这种场景下,一份开箱即用的 JavaWeb 图书馆管理系统源码就是最省事的选择。这份 MySQL 版本的源码走的是很老派但很扎实的路线:Servlet 接请求、JSP 出页面、JDBC 直连数据库,没有 Spring 那层封装,也没有复杂的前端构建,解压、配库、丢进 Tomcat 就能跑。它适合两类人:一类是刚学完 Servlet 和 JSP、想找个字段齐全的项目把增删改查打通的人;另一类是要给小型图书室、班级书架、内部资料库做一套内网管理工具的人。它不追求高并发,追求的是每个功能都能在源码里找到对应的一行 SQL,改起来不用猜框架在背后替你做了什么。

2. 技术栈与目录结构:Servlet + JSP + JDBC 的三层怎么落地

2.1 一次请求从浏览器走到 MySQL 的完整链路

拿到源码先别急着点运行,先把请求链路在脑子里过一遍,后面任何报错都能定位到具体一层。这类项目的标准走向是:浏览器提交表单 → Tomcat 按web.xml或@WebServlet找到对应 Servlet → 前面的 Filter 先过一遍编码和登录校验 → Servlet 取参数、做基本校验 → 调 Service → 调 DAO → DAO 拼 SQL 走 JDBC 打到 MySQL → 结果集映射成实体类 → Servlet 塞进 request 域 → forward 到 JSP → JSTL 渲染成 HTML。

层次典型类/文件干的事出问题时的典型症状
视图层WebContent/jsp/*.jspJSTL 渲染,只做展示页面空白、${}原样打印
控制层servlet/BookServlet收参数、校验、转发404、参数永远是 null
业务层service/BorrowService借阅上限、库存规则借阅数量对不上
持久层dao/BookDaoSQL 与 ResultSet 映射SQLException、字段串位
工具层util/DBUtil读配置、拿连接项目启动就抛异常

链路里有两处最容易出问题。一是 Filter 的顺序,编码过滤器必须排在登录校验过滤器前面,否则校验失败跳转时带出去的中文提示会变乱码;二是 forward 和 redirect 的混用,登录成功后如果还用 forward,浏览器地址栏停在/login,用户一刷新就会重复提交表单。常见的写法是登录成功后sendRedirect到列表页,其余内部跳转统一用 forward。

2.2 目录树里每个包到底放什么

老项目没有 Maven 的标准目录,包名和路径全靠约定。解压之后先对着下面这棵树看一遍,心里就有数了:

library-system/ ├── src/ │ ├── com/library/bean/ # 实体类,和表字段一一对应 │ │ ├── Book.java │ │ ├── Reader.java │ │ └── BorrowRecord.java │ ├── com/library/dao/ # 只写 SQL,不掺业务判断 │ │ ├── BaseDao.java # 增删改查的公共封装 │ │ ├── BookDao.java │ │ └── BorrowDao.java │ ├── com/library/service/ # 业务规则:借阅上限、库存扣减 │ ├── com/library/servlet/ # 接参数、调 service、转发 JSP │ ├── com/library/filter/ # 编码过滤、登录校验 │ └── com/library/util/ # DBUtil、日期工具、分页工具 ├── WebContent/ # 部分 IDE 里叫 web 或 webapp │ ├── WEB-INF/ │ │ ├── web.xml # 早期 Servlet 在这里注册 │ │ └── lib/mysql-connector-java-8.0.28.jar │ ├── jsp/ # 业务页面,放在 WEB-INF 下更安全 │ ├── css/ js/ │ └── index.jsp # 登录入口 └── sql/library.sql # 建表 + 初始数据

判断一份源码分层是否干净,有个很土但很准的办法:打开servlet目录,如果里面有PreparedStatement或者ResultSet,说明业务逻辑漏到了控制层,后面加功能会越来越乱;反过来打开dao目录,如果里面有HttpServletRequest,那是另一方向的穿透,DAO 一旦依赖 Web 容器就没法写单元测试。合格的做法是 DAO 只认基本类型和实体类,参数进、实体出。

2.3 数据库表设计与建表脚本

图书管理这行的表结构几十年没变过,核心就四张:图书、读者、借阅记录、管理员,再加上一张分类做字典。这份 SQL 是整个项目的地基,字段类型定错,后面 Java 里再怎么写都别别扭扭。

-- 图书表:total 是馆藏总数,stock 是当前可借数,两者分开存 CREATE TABLE `book` ( `book_id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `isbn` varchar(20) NOT NULL COMMENT '业务唯一号', `book_name` varchar(100) NOT NULL COMMENT '书名', `author` varchar(50) DEFAULT NULL, `category_id` int DEFAULT NULL COMMENT '分类外键', `publisher` varchar(80) DEFAULT NULL, `price` decimal(8,2) DEFAULT '0.00', `total` int NOT NULL DEFAULT 0 COMMENT '馆藏总数', `stock` int NOT NULL DEFAULT 0 COMMENT '可借数量', PRIMARY KEY (`book_id`), UNIQUE KEY `uk_isbn` (`isbn`), KEY `idx_name` (`book_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表'; -- 借阅记录表:state 用一个字节表达归还状态和逾期状态 CREATE TABLE `borrow_record` ( `record_id` int NOT NULL AUTO_INCREMENT, `book_id` int NOT NULL, `reader_id` int NOT NULL, `borrow_time` datetime NOT NULL COMMENT '借出时间', `due_time` datetime NOT NULL COMMENT '应还时间', `return_time` datetime DEFAULT NULL COMMENT '实际归还时间', `state` tinyint NOT NULL DEFAULT 0 COMMENT '0在借 1已还 2逾期', PRIMARY KEY (`record_id`), KEY `idx_reader` (`reader_id`), KEY `idx_book` (`book_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';

total和stock分开存不是冗余,是刚需。图书馆会一次买五本同样的书,但对外只有 ISBN 一个号,用book_id做借阅外键、用stock表示实时可借数,比每本书建一条记录再按 ISBN 聚合要简单得多。字符集统一utf8mb4,别用utf8,后者在 MySQL 里其实是三字节的utf8mb3,遇到生僻字或者课程名里的特殊符号会直接插入失败。

提示:导入脚本前先确认自己的 MySQL 版本。8.0 默认字符集已经是utf8mb4,但 5.7 建库时如果不显式指定,表会跟着服务器默认走 latin1,中文写进去就是一堆问号。

3. 运行环境与启动流程:从改配置到登录首页

3.1 JDK、Tomcat、MySQL 的版本对齐

新手跑这套源码,八成的时间耗在版本上。这份代码用的是javax.servlet.*这套包名,这个细节决定了上限:

组件建议版本说明
JDK8 或 118 上最稳,源码里的语法不会踩到模块化限制
Tomcat8.5 或 9.0不要用 Tomcat 10,包名换成了jakarta.*,全项目编译不过
MySQL5.7 或 8.08.0 要换驱动类和 URL 参数
JDBC 驱动5.7 配 5.1.x,8.0 配 8.0.x驱动版本和服务器版本不匹配会连不上
IDE任意,需支持 Web 项目Eclipse 用 Dynamic Web Project,IDEA 用 Web Facet

Tomcat 10 这一条是血泪经验。如果你从官网首页直接下最新版,解压部署后控制台会刷一片ClassNotFoundException: javax.servlet.http.HttpServlet,代码一个字没错,纯粹是包名换代了。省事的做法是直接找 9.0.x 的压缩包,解压即用,不写注册表不装服务。

3.2 导入 SQL、改 jdbc.properties 与 web.xml

第一步,建库导数据。命令行里两句就够,注意库名要和配置文件里对上:

# 建库时显式指定字符集,避免继承服务器默认值 mysql -uroot -p -e "CREATE DATABASE library DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;" # 导入建表脚本和初始数据 mysql -uroot -p library < sql/library.sql

第二步,改数据库连接配置。这类项目通常把连接信息抽到src/jdbc.properties,如果源码里是硬编码在DBUtil里的,我一般顺手抽出来,后面换环境不用改代码:

# MySQL 8.0 的写法,注意时区和 SSL 参数 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/library?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你的密码 # MySQL 5.7 用下面这套,驱动类和 URL 都不一样 # jdbc.driver=com.mysql.jdbc.Driver # jdbc.url=jdbc:mysql://127.0.0.1:3306/library?useUnicode=true&characterEncoding=utf8

serverTimezone这个参数在 MySQL 8.0 的驱动上是硬要求,不写会抛The server time zone value '????' is unrecognized,因为驱动拿不到服务器时区就不知道怎么解析datetime。allowPublicKeyRetrieval=true是为了让 8.0 默认的caching_sha2_password认证方式在非 SSL 连接下能走通,本地开发开着无妨,上生产要慎重。

第三步,确认web.xml里的欢迎页指向登录页,并且WEB-INF/lib下确实放了驱动 jar。很多人把 jar 加进了 IDE 的 Build Path 却没放进lib,IDE 里跑得好好的,一导 war 包部署到独立 Tomcat 就报找不到驱动——因为 IDE 是拿自己的 classpath 顶上的。

3.3 部署与第一个能登录的页面

部署方式二选一。用 IDE 的话,Eclipse 里右键项目 → Run As → Run on Server,选好 Tomcat 9 即可;IDEA 里配一个 Tomcat 的 Run Configuration,Deployment 页签加 Artifact。想脱离 IDE 就直接把编译产物和WebContent打包成 war,扔进TOMCAT_HOME/webapps,启动后访问http://localhost:8080/项目名/。

启动成功的标志是控制台没有异常堆栈,并且能在浏览器里看到登录页。如果页面出来了但样式全丢,看 Network 面板里css/bootstrap.min.css是不是 404——通常是 JSP 里用了相对路径,而页面被 forward 到/jsp/子目录后基准路径变了。稳妥写法是在每个 JSP 头部加一句:

<%-- 用上下文路径拼绝对地址,forward 到任何子目录都不会丢样式 --%> <link rel="stylesheet" href="${pageContext.request.contextPath}/css/bootstrap.min.css"> <script src="${pageContext.request.contextPath}/js/jquery.min.js"></script>

${pageContext.request.contextPath}取的是应用上下文,部署成 ROOT 时是空串,部署成/library时是/library,两种情况都能拼对。这套源码里的 JSP 如果写的是href="css/xxx.css"这种裸相对路径,建议一次性全改掉,比一个个排查省事。默认账号一般在sql/library.sql的INSERT语句里,管理员表里躺着一条admin/123456之类的初始数据,登进去看到图书列表,环境就算通了。

4. 核心模块拆解:登录鉴权、图书分页、借阅事务

4.1 登录:从明文比对到 Session 与过滤器

登录是整个系统的入口,也是最容易被写歪的地方。这份源码里典型的写法是 Servlet 直接查库比对明文密码,逻辑短但漏洞也明显。

@WebServlet("/login") public class LoginServlet extends HttpServlet { private final AdminDao adminDao = new AdminDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String pwd = req.getParameter("password"); Admin admin = adminDao.findByUsername(username); // 用户名不存在和密码错误给同一句提示,避免被枚举出有效账号 if (admin == null || !admin.getPassword().equals(pwd)) { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/jsp/login.jsp").forward(req, resp); return; } // 登录态放 Session,后续所有页面从这儿取当前用户 req.getSession().setAttribute("loginAdmin", admin); // 重定向而不是转发,防止刷新重复提交表单 resp.sendRedirect(req.getContextPath() + "/book?action=list"); } }

光有登录还不够,用户绕过登录页直接敲/book?action=list照样能进。所以必须配一个登录校验过滤器,把需要保护的路径统一拦下来:

@WebFilter("/*") public class LoginFilter implements Filter { // 白名单:登录页、登录接口、静态资源放行 private static final String[] WHITE = {"/login", "/jsp/login.jsp", "/css/", "/js/"}; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); for (String w : WHITE) { if (uri.contains(w)) { chain.doFilter(request, response); // 白名单直接放行 return; } } if (req.getSession().getAttribute("loginAdmin") == null) { resp.sendRedirect(req.getContextPath() + "/jsp/login.jsp"); return; } chain.doFilter(request, response); // 已登录,继续往下走 } }

这里两个参数值得说清楚:@WebFilter("/*")的/*是前缀匹配,会拦下所有请求,包括图片和 CSS,所以白名单里必须放静态资源目录,否则登录页自己都加载不出样式。uri.contains(w)用包含判断而不是startsWith,是因为getRequestURI()返回的是带上下文路径的全路径,比如/library/login,直接startsWith("/login")永远匹配不上。另外,过滤器里跳转必须用sendRedirect,用 forward 的话浏览器地址栏不变,用户按 F5 会重新提交一次登录请求。

4.2 图书列表的分页与模糊搜索

图书列表是访问量最大的页面,也是最容易写出性能问题的地方。新手版本常见两种写法:SELECT * FROM book全量查出来在 Java 里subList,或者拼字符串"WHERE book_name LIKE '%" + keyword + "%'"。前者书一多就卡,后者直接是 SQL 注入的活靶子。

public List<Book> page(String keyword, int pageNo, int pageSize) { List<Book> list = new ArrayList<>(); // 用 ? 占位,LIKE 的通配符拼在参数值上而不是 SQL 里 String sql = "SELECT b.*, c.category_name FROM book b " + "LEFT JOIN category c ON b.category_id = c.category_id " + "WHERE b.book_name LIKE ? OR b.author LIKE ? " + "ORDER BY b.book_id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); ps.setInt(3, (pageNo - 1) * pageSize); // 偏移量从 0 开始 ps.setInt(4, pageSize); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); // 结果集到实体的映射单独抽方法 } } } catch (SQLException e) { throw new RuntimeException("分页查询图书失败", e); } return list; }

几个参数的门道:pageNo从 1 开始传,偏移量算成(pageNo - 1) * pageSize,如果业务代码里直接传 0 或者负数,MySQL 会抛语法错误而不是返回空列表。pageSize一般取 10 到 20,太大前端表格撑不住,太小翻页次数多。ORDER BY book_id DESC不能省,没有明确排序时 MySQL 返回顺序不保证稳定,翻页会出现同一本书在两页都出现或者干脆漏掉的情况。

分页还得配一个 count 查询拿总数,SELECT COUNT(*) FROM book WHERE book_name LIKE ? OR author LIKE ?,条件和列表查询严格保持一致,否则总页数算错,最后一页点进去是空的。封装一个PageBean把list、totalCount、pageNo、totalPage装一起传回 JSP,页面上的上一页下一页按钮直接用${page.totalPage}判断边界。

4.3 借书还书:必须手动控制的事务

借书这个动作背后是三条 SQL:扣库存、写借阅记录、改读者的已借数量。三条要么全成,要么全不成,中间断一条就会出现"库存扣了但没记录"或者"记录写了但书还在架上"这种对不上账的情况。JDBC 默认自动提交,每次executeUpdate都是一个独立事务,必须手动关掉。

public boolean borrow(int bookId, int readerId) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关键:关掉自动提交,三条 SQL 捆成一个原子操作 // 1. 带条件的库存扣减:stock > 0 才更新得动,天然挡住并发超借 try (PreparedStatement ps = conn.prepareStatement( "UPDATE book SET stock = stock - 1 WHERE book_id = ? AND stock > 0")) { ps.setInt(1, bookId); if (ps.executeUpdate() == 0) { conn.rollback(); return false; // 影响行数为 0 说明库存不足,直接结束 } } // 2. 写借阅记录,应还时间给 30 天 try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO borrow_record(book_id, reader_id, borrow_time, due_time, state) " + "VALUES(?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), 0)")) { ps.setInt(1, bookId); ps.setInt(2, readerId); ps.executeUpdate(); } // 3. 更新读者已借数量,超过上限就整体回滚 try (PreparedStatement ps = conn.prepareStatement( "UPDATE reader SET borrowed = borrowed + 1 " + "WHERE reader_id = ? AND borrowed < max_borrow")) { ps.setInt(1, readerId); if (ps.executeUpdate() == 0) { conn.rollback(); return false; // 达到借阅上限 } } conn.commit(); return true; } catch (SQLException e) { rollbackQuietly(conn); // 异常务必回滚,否则连接归还后脏数据还在 throw new RuntimeException("借书失败", e); } finally { closeQuietly(conn); // 关连接前先复位 autoCommit 由连接池负责 } }

这段代码的核心技巧是把判断条件塞进UPDATE的WHERE里,而不是先SELECT stock再判断再更新。后者在两个人同时借最后一本书时会双双读到stock = 1,然后都减成功,库存变 -1。把stock > 0写进WHERE,MySQL 的行锁会保证只有一个UPDATE影响行数为 1,另一个为 0,业务上就是"手慢了"。这个写法不需要额外加锁,也不需要SELECT ... FOR UPDATE,性价比最高。

还书是逆过程:把记录的state改成 1、填return_time、库存加 1、读者已借数减 1,同样一个事务包住。逾期判断不用定时任务,在查询时用due_time < NOW() AND state = 0动态算出来展示就行,真正要落状态再跑一个每日的批处理。

5. 避坑与排查:这套源码跑不起来时的五个高频现场

5.1 现象:控制台刷ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因基本是驱动 jar 没进WEB-INF/lib,只加到了 IDE 的构建路径里。IDE 内部跑用的是它自己的 classpath,打包部署到独立 Tomcat 时用的是lib目录,两边不是一回事。

解决办法是手动把mysql-connector-java-8.0.x.jar复制进WebContent/WEB-INF/lib,然后在 IDE 里刷新项目并重新部署。如果用的是 5.7 的服务器却配了 8.0 的驱动类名,报的也是这个错,反过来同理,改jdbc.properties里的jdbc.driver即可。判断依据很简单:URL 里带serverTimezone就用com.mysql.cj.jdbc.Driver,不带就用com.mysql.jdbc.Driver。

5.2 现象:页面中文全是问号,数据库里存进去的也是问号

三个地方的编码有一处没对齐就会这样:JSP 页面没写<%@ page contentType="text/html;charset=UTF-8" %>、请求体没设编码、数据库或表不是utf8mb4。乱码出现在"提交后"说明是请求侧,出现在"查询后"说明是响应侧或存储侧。

最省事的修法是加一个编码过滤器,并且保证它是过滤器链上的第一个:

@WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); // 只对 POST 请求体生效 resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }

注意setCharacterEncoding对 GET 请求的查询串无效,那个由 Tomcat 的server.xml里 Connector 的URIEncoding决定,8.0 之后默认就是 UTF-8,一般不用动。如果数据库层已经是乱码存量数据,改配置救不回来,只能重新导入脚本。

5.3 现象:登录成功跳到了列表页,一刷新又弹回登录页

原因通常是 Session 没存住。排查顺序是:先看登录 Servlet 里是不是用了req.getSession()又没setAttribute,再看过滤器是不是把重定向后的目标路径也拦了,最后看浏览器是不是禁了 Cookie——JSESSIONID 默认靠 Cookie 传,禁了就每次都是新会话。

还有一种隐蔽情况:项目部署在两台 Tomcat 上做负载,Session 不共享,用户每次请求落到不同实例就掉登录。单机部署不会遇到,但如果你照着这套源码往生产搬,这一点要提前想清楚,后面第 6 章会给改造方向。

5.4 现象:借完书库存变成负数,或者一本书被借了两次

原因是"先查再改"的写法没加事务,两个请求同时读到同样的库存值。也可能是事务开了但异常分支里忘了rollback,catch块只打了日志就return,连接被连接池回收时行为不可预期。

解决就是把判断条件写进UPDATE ... WHERE stock > 0,用影响行数判断成败,同时在catch里强制回滚。另外检查一下 MySQL 的存储引擎,如果建表脚本里写的是ENGINE=MyISAM,事务根本不生效,必须改成InnoDB。这个坑很隐蔽,代码写得再对也没用,因为引擎不支持。

5.5 现象:改了 JSP 或 Java 代码,重启后页面还是老样子

JSP 改完不生效,多半是 Tomcat 的work目录里缓存了编译后的 Servlet,清掉TOMCAT_HOME/work/Catalina/localhost/项目名再启动即可。Java 类改完不生效,是 IDE 没有重新编译或者部署包没更新,Eclipse 里检查 Project → Build Automatically 是否勾上,IDEA 里看 Artifact 是不是设成了 exploded war。

还有一种情况是浏览器缓存了 CSS 和 JS,页面结构变了样式没变,开无痕窗口验证一下就能区分。养成习惯:改完代码先 Ctrl+F5 强刷,再判断是不是服务端的问题,能省掉一半的自我怀疑。

6. 二次开发:把它从「能跑」改成「敢用」

这套源码跑通之后,最大的价值是它足够简单,任何一处改造都能立刻看到效果。但简单也意味着它离"能给别人用"还有距离,下面是我拿到这类项目后固定要动的几刀。

6.1 先堵密码这个最大的窟窿

明文存密码是这类教学项目最普遍的问题,改造成本却极低。库里加两个字段salt和password(长度给 64),注册或改密时生成随机盐,存SHA-256(salt + 明文);登录时用同样的规则算一遍再比对。

// 改密时:盐和摘要一起入库 String salt = UUID.randomUUID().toString().replace("-", ""); String hash = sha256(salt + plainPassword); // 登录时:取出该用户的盐,算完比对,数据库里永远不出现明文 if (sha256(admin.getSalt() + inputPwd).equals(admin.getPassword())) { /* 通过 */ }

顺手把权限也补上。现在多半只有一张admin表,加一个role字段区分超级管理员和普通管理员,普通管理员隐藏删除按钮、后台接口里再校验一次。前端隐藏只是体验,后端校验才是防线。

6.2 补一层 Service 与连接池

DAO 里每个方法都DriverManager.getConnection(),意味着每次查询都建一次 TCP 连接,压测下 QPS 上不去还容易把 MySQL 的连接数打满。换成 Druid 或 HikariCP,只改DBUtil.getConnection()的实现,上层代码一行不用动:

改造项改前改后收益
连接获取DriverManager每次新建连接池getConnection()单次查询省 10~30ms
事务边界散落在 Servlet收敛到 Service 方法异常回滚不漏
密码存储明文加盐摘要库被拖走也不至于全裸
分页总数每次COUNT(*)缓存或近似值大表翻页不卡

再往下就是结构性的改动:把 Servlet 里的业务判断抽到 Service,让 Servlet 只负责取参数和转发;把web.xml里的 Servlet 注册换成注解;把 JSP 里的 Java 小脚本<% %>全部换成 JSTL 和 EL。这几步做完,项目才算有了继续长大的骨架。

我自己的习惯是,每接一份这类源码,第一步不是改功能,而是先写一个能跑通"登录—新增图书—借书—还书"四条链路的冒烟脚本,把改造前后的结果对一遍。有一次我图快,直接改了库存扣减的 SQL 却没重跑还书流程,上线后才发现还书时库存加不回去,靠着借阅记录一条条手工对账才补平。从那以后我每次动到事务相关的代码,都强制把借书、还书、超期这三条路径手工走一遍再提交。希望帮到你。

本文还有配套的精品资源,点击获取