拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

JavaWeb图书管理系统:手把手搭建最小闭环实践模板

JavaWeb图书管理系统:手把手搭建最小闭环实践模板

简介:本资源是一份面向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驱动。按此顺序检查:

  1. 打开Project Structure→Artifacts→+→Web Application: Archive
    → 命名为BookSystem.war
    → 点击右侧Output Layout标签

  2. 在Available Elements中展开你的模块
    → 将src下的编译输出(通常是BookSystem/classes)拖到WEB-INF/classes/下
    → 将web/WEB-INF/lib/下的所有JAR拖到WEB-INF/lib/下(不能拖整个lib文件夹,必须逐个拖JAR)

  3. 关键一步:右键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.javarequest.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倍。

希望帮到你。

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

返回列表