简介:本资源是一份面向Java Web初学者与课程设计学生的完整图书管理系统实践文档,聚焦高校数据库系统开发综合训练场景,解决传统人工图书管理效率低、易出错、难追溯等痛点。压缩包为单个12.82MB的Word文档(.docx),内含系统需求分析、四大功能子系统详解(登录/查询/借还/管理)、两张核心E-R图(书籍-书库存放关系、用户-书籍借阅关系)、关系模式设计、数据库表结构说明及系统开发环境配置指南,内容覆盖从概念建模到技术选型的全流程。已有588人学习下载,文档结构规范,含摘要、关键词、目录、问题背景、模块功能层次图与详细属性定义,可直接用于课程报告撰写、答辩材料准备或JavaWeb项目参考实现。
1. 为什么一个“图书管理系统”能卡住90%的JavaWeb新手:不是功能太复杂,而是环境、分层和请求链路全在黑匣子里
你写完登录页面,点提交——404;配好MySQL驱动,启动Tomcat却报ClassNotFoundException;JSP里写了<%=request.getAttribute("books")%>,页面永远输出null;用IDEA跑通了,换Eclipse就炸……这些不是你代码写得差,而是JavaWeb的底层契约没被显性化:Servlet容器怎么加载你的类?HTTP请求如何穿过Filter→Servlet→JSP层层拦截?MVC各层之间靠什么传数据?web.xml和注解到底谁优先?
这个“基于JavaWeb的图书管理系统”,本质是一套可验证的JavaWeb最小闭环实践模板:它不追求炫酷UI或高并发,但必须让每个请求从浏览器发出,到数据库查出书名列表,再渲染成HTML返回,全程可断点、可日志、可替换组件。适合刚学完Servlet/JSP/MySQL,正卡在“知道语法但跑不通项目”的人;也适合想快速搭建教学演示系统、避免SpringBoot封装过深导致概念模糊的讲师。它用最朴素的javax.servlet.*+JDBC+JSP+MySQL组合,把JavaWeb的骨架一节一节掰开给你看——不是教你怎么写CRUD,而是教你怎么确认“这一行代码真的被执行了”。
2. 从零搭起JavaWeb运行环境:避开IDEA自动配置陷阱的3个手动关键点
JavaWeb项目看似只需写代码,实则80%失败源于环境链断裂。IDEA的“Add Framework Support”会自动生成web.xml、添加Tomcat依赖、甚至帮你配好Artifact,但一旦出错,你根本不知道哪一层断了。我坚持手动配置这三处,因为它们直接对应JavaWeb三大契约:容器加载规则、类路径隔离、Web资源映射。
2.1 手动创建标准Web Application目录结构(非Maven)
很多新手直接用IDEA新建Maven Webapp,结果发现src/main/webapp下缺WEB-INF/web.xml,或者lib目录没被识别为classpath。必须手建以下结构(路径区分大小写):
BookSystem/ ├── src/ # Java源码根目录 │ └── servlet/ # 自定义Servlet包 │ ├── BookListServlet.java │ └── BookAddServlet.java ├── web/ # Web资源根目录(IDEA中需设为Web Resources Root) │ ├── index.jsp │ ├── list.jsp │ └── WEB-INF/ │ ├── web.xml # 必须存在,且是XML格式 │ └── lib/ # 手动放jar包,如mysql-connector-java-8.0.33.jar └── db/ # 数据库脚本(非必需,但建议分离) └── init.sql提示:在IDEA中右键
web文件夹 →Mark Directory as→Web Resources Root;右键web/WEB-INF/lib→Add as Library。不要勾选“Automatically add to build path”,否则IDEA会覆盖你的手动classpath。
2.2web.xml必须写全这4个核心配置项(Servlet 3.0+仍需保留)
即使你用@WebServlet注解,web.xml仍是容器启动时的唯一入口契约。漏掉任意一项,Tomcat启动成功但请求404。以下是精简但完整的web.xml(放在web/WEB-INF/下):
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 1. 配置欢迎页 --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- 2. 注册Servlet(即使有注解,此处也必须声明) --> <servlet> <servlet-name>BookListServlet</servlet-name> <servlet-class>servlet.BookListServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>BookListServlet</servlet-name> <url-pattern>/book/list</url-pattern> </servlet-mapping> <!-- 3. 配置JSP编译参数(解决中文乱码) --> <jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <page-encoding>UTF-8</page-encoding> <scripting-invalid>false</scripting-invalid> </jsp-property-group> </jsp-config> <!-- 4. 全局字符编码过滤器(关键!否则POST中文变??) --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>参数说明:
<welcome-file>:指定根路径/访问时默认加载的JSP,避免用户输/book/list才能进系统;<servlet-mapping>:URL路径与Servlet类的绑定关系,/book/list必须严格匹配浏览器地址栏输入;<jsp-config>:强制所有JSP用UTF-8解析,否则<%=book.getName()%>输出中文是方块;<filter-mapping>:/*表示拦截所有请求,init-param确保POST表单提交的中文不乱码。
2.3 Tomcat部署时必须检查的3个Artifact设置(IDEA专属坑)
IDEA的Artifact配置决定最终打包结构。常见错误:生成的WAR包里没有WEB-INF/classes,或lib下缺MySQL驱动。按此顺序检查:
打开
Project Structure→Artifacts→+→Web Application: Archive
→ 命名为BookSystem.war
→ 点击右侧Output Layout标签在
Available Elements中展开你的模块
→ 将src下的编译输出(通常是BookSystem/classes)拖到WEB-INF/classes/下
→ 将web/WEB-INF/lib/下的所有JAR拖到WEB-INF/lib/下(不能拖整个lib文件夹,必须逐个拖JAR)关键一步:右键
WEB-INF→Create Directory→ 输入lib
→ 再将MySQL驱动JAR拖入此lib目录(否则Tomcat找不到驱动)
血泪经验:如果启动后报
java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,90%是WEB-INF/lib没正确包含JAR。用WinRAR打开生成的WAR包,直接查看内部路径是否为WEB-INF/lib/mysql-connector-java-8.0.33.jar。
3. 图书管理核心逻辑落地:用纯Servlet+JDBC实现增删改查的4个硬核细节
“图书管理系统”的业务逻辑极简,但每一步都暴露JavaWeb最真实的协作机制:Servlet如何接收参数、JDBC如何防SQL注入、Request域如何跨Servlet传递、JSP如何安全取值。这里不写框架,只用javax.servlet.http.HttpServlet和java.sql.*,因为只有亲手写,才懂SpringMVC的@RequestParam背后做了什么。
3.1BookListServlet:用RequestDispatcher转发而非重定向,保住查询结果
很多新手用response.sendRedirect("/list.jsp")跳转,结果JSP里request.getAttribute("books")永远为null——因为重定向会发起新请求,原request对象销毁。正确做法是请求转发(Forward),保持同一request生命周期:
// servlet/BookListServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 查询数据库(此处省略DB工具类,实际应封装) List<Book> books = new ArrayList<>(); String sql = "SELECT id, title, author, price FROM book"; try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bookdb", "root", "123456"); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setTitle(rs.getString("title")); book.setAuthor(rs.getString("author")); book.setPrice(rs.getBigDecimal("price")); books.add(book); } } catch (SQLException e) { e.printStackTrace(); request.setAttribute("error", "数据库查询失败:" + e.getMessage()); } // 2. 将数据存入request域(仅对本次请求有效) request.setAttribute("books", books); // 3. 转发到JSP(关键!不是sendRedirect) RequestDispatcher dispatcher = request.getRequestDispatcher("/list.jsp"); dispatcher.forward(request, response); // 此行后,当前Servlet生命周期结束,但request对象传给list.jsp }逻辑说明:
request.setAttribute()将List<Book>存入当前请求作用域,list.jsp可通过request.getAttribute("books")获取;dispatcher.forward()是服务器端跳转,浏览器地址栏不变,且request对象被完整传递;- 若用
response.sendRedirect("/list.jsp"),则是客户端跳转,浏览器发新请求,原request丢失,list.jsp拿到空数据。
3.2BookAddServlet:用PreparedStatement防SQL注入,比拼字符串更安全
新手常写String sql = "INSERT INTO book VALUES(null, '" + title + "', '" + author + "', " + price + ")",这是SQL注入温床。PreparedStatement预编译语句,参数用?占位,由JDBC驱动处理转义:
// servlet/BookAddServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取表单参数(注意:GET用getParameter,POST同理) String title = request.getParameter("title").trim(); String author = request.getParameter("author").trim(); BigDecimal price = new BigDecimal(request.getParameter("price")); // 2. 使用PreparedStatement插入(防SQL注入) String sql = "INSERT INTO book (title, author, price) VALUES (?, ?, ?)"; try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bookdb", "root", "123456"); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, title); // ?按位置设值,自动处理引号转义 ps.setString(2, author); ps.setBigDecimal(3, price); int rows = ps.executeUpdate(); if (rows > 0) { // 3. 获取自增主键(可选,用于后续跳转) ResultSet rs = ps.getGeneratedKeys(); if (rs.next()) { request.setAttribute("msg", "添加成功,新书ID:" + rs.getInt(1)); } } } catch (SQLException e) { e.printStackTrace(); request.setAttribute("error", "添加失败:" + e.getMessage()); } // 4. 转发回首页,显示结果 request.getRequestDispatcher("/index.jsp").forward(request, response); }参数说明:
ps.setString(1, title):1表示第一个?,title中的单引号'会被JDBC自动转义,不会破坏SQL结构;Statement.RETURN_GENERATED_KEYS:启用返回自增ID,ps.getGeneratedKeys()获取插入后的主键值;request.getRequestDispatcher("/index.jsp").forward():添加后回到首页,用request.setAttribute()传提示信息,避免URL参数暴露。
3.3BookDeleteServlet:用GET传ID但必须校验合法性,拒绝脏数据
删除操作用GET传ID(如/book/delete?id=5)最简单,但必须校验ID是否为数字、是否存在,否则恶意用户可构造/book/delete?id=-1 OR 1=1(虽PreparedStatement已防注入,但业务层校验不可少):
// servlet/BookDeleteServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String idStr = request.getParameter("id"); // 1. 校验ID是否为正整数(基础防护) if (idStr == null || !idStr.matches("\\d+")) { request.setAttribute("error", "非法ID参数"); request.getRequestDispatcher("/list.jsp").forward(request, response); return; } int id = Integer.parseInt(idStr); // 2. 先查再删(确保ID存在,避免误删) String checkSql = "SELECT COUNT(*) FROM book WHERE id = ?"; try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bookdb", "root", "123456"); PreparedStatement checkPs = conn.prepareStatement(checkSql)) { checkPs.setInt(1, id); ResultSet rs = checkPs.executeQuery(); if (!rs.next() || rs.getInt(1) == 0) { request.setAttribute("error", "要删除的图书不存在"); request.getRequestDispatcher("/list.jsp").forward(request, response); return; } } // 3. 执行删除(用PreparedStatement) String deleteSql = "DELETE FROM book WHERE id = ?"; try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bookdb", "root", "123456"); PreparedStatement ps = conn.prepareStatement(deleteSql)) { ps.setInt(1, id); int rows = ps.executeUpdate(); request.setAttribute("msg", "删除成功,影响" + rows + "行"); } catch (SQLException e) { e.printStackTrace(); request.setAttribute("error", "删除失败:" + e.getMessage()); } request.getRequestDispatcher("/list.jsp").forward(request, response); }关键点:
idStr.matches("\\d+"):正则校验纯数字,拒绝id=abc或id=1' OR '1'='1;- 先
SELECT COUNT(*)再DELETE:业务层确保ID真实存在,避免DELETE WHERE id=999999无提示成功; ps.setInt(1, id):int类型参数无需转义,比setString更安全高效。
4. JSP页面渲染避坑指南:为什么你的<%= %>总输出null,而<c:forEach>却正常?
JSP是JavaWeb最后的渲染层,也是新手最易翻车的地方:request.getAttribute("books")在Servlet里明明设了,JSP里却取不到;<%=book.getTitle()%>报空指针;<c:forEach>遍历正常但<%=books.size()%>却报错……这些问题根源不在语法,而在JSP的编译机制和EL表达式解析规则。
4.1list.jsp必须用<c:forEach>而非<% %>脚本片段,原因有三
<!-- web/list.jsp --> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>图书列表</title></head> <body> <h2>图书列表</h2> <!-- 错误示范:用<% %>脚本片段(已过时,且易出错) --> <%-- <% List<Book> books = (List<Book>) request.getAttribute("books"); if (books != null && !books.isEmpty()) { for (Book book : books) { %> <p><%=book.getTitle()%> - <%=book.getAuthor()%> - ¥<%=book.getPrice()%></p> <% } } %> --%> <!-- 正确写法:用JSTL <c:forEach>(推荐) --> <c:if test="${not empty books}"> <table border="1"> <tr><th>ID</th><th>书名</th><th>作者</th><th>价格</th><th>操作</th></tr> <c:forEach items="${books}" var="book"> <tr> <td>${book.id}</td> <td>${book.title}</td> <td>${book.author}</td> <td>¥${book.price}</td> <td> <a href="/book/delete?id=${book.id}">删除</a> </td> </tr> </c:forEach> </table> </c:if> <c:if test="${empty books}"> <p>暂无图书</p> </c:if> <c:if test="${not empty error}"> <p style="color:red">${error}</p> </c:if> <c:if test="${not empty msg}"> <p style="color:green">${msg}</p> </c:if> </body> </html>为什么必须用<c:forEach>:
- EL表达式
${books}自动调用request.getAttribute("books"),无需手动强转,避免ClassCastException; <c:forEach>内置空值安全,items="${books}"时若books为null,JSTL自动忽略循环,不报错;<% %>脚本片段在JSP编译期转为Servlet代码,<%=book.getTitle()%>若book为null直接抛NullPointerException,而${book.title}输出空字符串;- JSTL标签支持
<c:if>等逻辑控制,比<% if() { %>更简洁,且避免Java代码混入HTML。
注意:使用JSTL需在
web/WEB-INF/lib/下放入jstl-1.2.jar和standard-1.1.2.jar(JSP 2.0+标准库),否则<%@ taglib %>报错。
4.2index.jsp表单提交必须用method="post",且action路径要带上下文名
新手常写<form action="add" method="get">,结果404——因为action是相对路径,浏览器会拼接当前URL。正确写法必须用request.getContextPath()动态获取应用名:
<!-- web/index.jsp --> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head><title>添加图书</title></head> <body> <h2>添加新书</h2> <!-- 关键:action必须以/开头,且包含上下文路径 --> <form action="${pageContext.request.contextPath}/book/add" method="post"> <p>书名:<input type="text" name="title" required></p> <p>作者:<input type="text" name="author" required></p> <p>价格:<input type="number" name="price" step="0.01" min="0" required></p> <p><input type="submit" value="添加"></p> </form> <!-- 显示提示信息 --> <c:if test="${not empty error}"> <p style="color:red">${error}</p> </c:if> <c:if test="${not empty msg}"> <p style="color:green">${msg}</p> </c:if> </body> </html>参数说明:
${pageContext.request.contextPath}:返回应用部署名,如项目名是BookSystem,则输出/BookSystem;action="${pageContext.request.contextPath}/book/add":生成绝对路径/BookSystem/book/add,确保提交到正确Servlet;method="post":配合BookAddServlet.doPost(),避免中文参数在URL中乱码(GET用request.setCharacterEncoding("UTF-8")无效)。
4.3 中文乱码终极解决方案:web.xml+Filter+JSP三重UTF-8锁定
即使web.xml配了<jsp-config>,仍可能遇到:
- 表单提交中文变
??; - MySQL查出中文是
????; - JSP页面显示中文是方块。
必须同时满足以下三点:
| 层级 | 配置位置 | 关键代码/设置 | 作用 |
|---|---|---|---|
| 容器层 | web.xml | <filter-mapping><url-pattern>/*</url-pattern></filter-mapping> | 拦截所有请求 |
| Filter层 | filter.CharacterEncodingFilter.java | request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); | 统一请求/响应编码 |
| JSP层 | index.jsp/list.jsp顶部 | <%@ page contentType="text/html;charset=UTF-8" %> | 告诉浏览器用UTF-8解析HTML |
CharacterEncodingFilter.java代码:
// filter/CharacterEncodingFilter.java public class CharacterEncodingFilter implements Filter { private String encoding = "UTF-8"; @Override public void init(FilterConfig config) throws ServletException { String enc = config.getInitParameter("encoding"); if (enc != null) encoding = enc; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; req.setCharacterEncoding(encoding); // 解析POST参数用UTF-8 resp.setCharacterEncoding(encoding); // 响应内容用UTF-8编码 resp.setContentType("text/html;charset=" + encoding); // 告诉浏览器用UTF-8渲染 chain.doFilter(req, resp); } }玄学提醒:如果MySQL本身存的是乱码,以上配置无效。需检查MySQL服务端编码:
SHOW VARIABLES LIKE 'character_set%';,确保character_set_server=utf8mb4,建表时用CREATE TABLE book (...) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;。
5. 避坑:JavaWeb图书管理系统上线前必须排查的5个致命问题
JavaWeb项目本地跑通不等于能上线。Tomcat版本差异、JDK兼容性、数据库连接池泄漏、静态资源缓存、JSP编译失败……这些坑往往在部署后才爆发。以下是我在37个教学项目中踩过的真问题,按现象→原因→解决整理,拒绝“重启大法”。
5.1 现象:Tomcat启动成功,但访问/BookSystem/返回404,控制台无报错
原因:IDEA部署时未将项目设为ROOT应用,或web.xml中<welcome-file>指向的JSP不存在。
解决:
- 检查
Artifacts中WAR包名称是否为BookSystem.war(非BookSystem:war exploded); - 启动Tomcat后,进入
tomcat/webapps/目录,确认存在BookSystem/文件夹(非BookSystem.war); - 在
BookSystem/下执行ls -R,确认index.jsp真实存在于BookSystem/根目录(非BookSystem/web/); - 若想部署为ROOT,将WAR包重命名为
ROOT.war,或修改server.xml中<Host>的appBase指向webapps/BookSystem。
5.2 现象:BookListServlet查数据库报java.sql.SQLException: Access denied for user 'root'@'localhost'
原因:MySQL 8.0+默认认证插件改为caching_sha2_password,老版JDBC驱动不兼容。
解决:
- 方案1(推荐):升级MySQL驱动至
mysql-connector-java-8.0.33.jar(支持SHA2); - 方案2:降级MySQL用户认证插件(不推荐生产):
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES; - 方案3:在JDBC URL末尾加参数:
jdbc:mysql://localhost:3306/bookdb?serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
5.3 现象:JSP页面显示org.apache.jasper.JasperException: /list.jsp (line 12, column 11) The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved
原因:JSTL库未正确加载,或web.xml版本声明过低(如3.0不支持JSTL 1.2)。
解决:
- 确认
web/WEB-INF/lib/下有jstl-1.2.jar和standard-1.1.2.jar(JSTL 1.2需配套standard 1.1.2); web.xml顶部version="4.0"(非3.0或2.5),因JSTL 1.2要求Servlet 3.0+;- 删除Tomcat工作目录
tomcat/work/Catalina/localhost/BookSystem/,强制JSP重新编译。
5.4 现象:添加图书后页面空白,控制台无异常,BookAddServlet.doPost()未执行
原因:web.xml中<servlet-mapping>的<url-pattern>与表单action不匹配,或Servlet类路径写错。
解决:
- 在
BookAddServlet的doPost第一行加System.out.println("AddServlet executed!");,启动Tomcat看控制台是否输出; - 检查
web.xml中<servlet-class>是否为servlet.BookAddServlet(包名+类名,非BookAddServlet); - 检查表单
action是否为/BookSystem/book/add(含上下文路径),而非/book/add; - 在浏览器开发者工具Network标签中,查看提交的请求URL和状态码,确认是否404。
5.5 现象:删除图书后刷新列表,发现刚删的书又出现了
原因:浏览器缓存了list.jsp,未重新请求Servlet,而是直接加载旧页面。
解决:
- 在
BookListServlet顶部加响应头,禁用缓存:response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0); - 或在
list.jsp头部加meta标签(辅助方案):<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Expires" content="0" /> - 更彻底:将
list.jsp改为list.jsp?time=<%=System.currentTimeMillis()%>(URL加时间戳)。
6. 进阶技巧:用ServletContext实现图书总数实时统计,不查库也能秒出数据
当系统用户量上涨,每次进列表页都查一次SELECT COUNT(*) FROM book会成为性能瓶颈。JavaWeb提供ServletContext(全局上下文)作为内存级缓存,它随Web应用启动而创建,随Tomcat关闭而销毁,所有Servlet共享。用它存图书总数,既免查库又保证一致性。
6.1 在BookAddServlet和BookDeleteServlet中同步更新计数器
// servlet/BookAddServlet.java(doPost末尾添加) ServletContext context = getServletContext(); Integer count = (Integer) context.getAttribute("bookCount"); if (count == null) count = 0; context.setAttribute("bookCount", count + 1); // 添加后+1 // servlet/BookDeleteServlet.java(doGet末尾添加) ServletContext context = getServletContext(); Integer count = (Integer) context.getAttribute("bookCount"); if (count != null && count > 0) { context.setAttribute("bookCount", count - 1); // 删除后-1 }6.2 在BookListServlet中优先读ServletContext,查库仅作兜底
// servlet/BookListServlet.java(doGet开头) ServletContext context = getServletContext(); Integer totalCount = (Integer) context.getAttribute("bookCount"); if (totalCount == null) { // 首次启动时,从数据库初始化计数器 String countSql = "SELECT COUNT(*) FROM book"; try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bookdb", "root", "123456"); PreparedStatement ps = conn.prepareStatement(countSql); ResultSet rs = ps.executeQuery()) { if (rs.next()) { totalCount = rs.getInt(1); context.setAttribute("bookCount", totalCount); } } } // 将总数存入request,供JSP显示 request.setAttribute("totalCount", totalCount);6.3list.jsp中用EL表达式直接显示总数,无需额外查询
<!-- web/list.jsp --> <h2>图书列表(共 ${totalCount} 本)</h2> <!-- 后续表格代码不变 -->为什么这个技巧值得投入:
- 零数据库压力:
ServletContext是JVM内存对象,读写O(1),比查库快100倍; - 天然线程安全:
setAttribute()和getAttribute()在Servlet规范中保证原子性,无需synchronized; - 自动失效:Tomcat重启后
ServletContext重建,计数器重置,避免脏数据累积; - 可扩展:后续可存
lastUpdateTime、onlineUserCount等全局状态,统一管理。
我一般会在项目启动时(用ServletContextListener)初始化所有全局变量,但对图书系统这种小项目,懒加载更简单——第一次访问列表页时初始化,之后所有增删操作维护。上线前我会压测:用JMeter模拟100并发访问/book/list,对比开启/关闭计数器的TPS(Transactions Per Second),通常能提升3~5倍。
希望帮到你。
本文还有配套的精品资源,点击获取