☰
Java Web入门:手写Servlet+JSP+MySQL购物网站实战
2026/10/7 19:34:57 网站建设 项目流程

简介:这是一套基于Java Web经典技术栈(Servlet+JSP+MySQL)实现的购物网站系统,专为计算机专业学生课程设计、期末大作业及项目实战练习打造,已获98分高分评价,具备完整业务流程与工程规范性。资源包共69个文件,包含15个核心Java源码、13个JSP页面、16个编译后Class文件、11个GIF和6个JPG等静态资源,辅以SQL建表脚本、CSS样式、JS交互逻辑及README说明文档,整体压缩后仅292KB,轻量易部署。已有234人下载学习,适合零基础入门Web开发或巩固MVC分层结构的学习者。读者可直接运行调试,完整掌握用户注册登录、商品浏览、购物车管理、订单提交等核心模块实现逻辑,并通过清晰的目录结构(如src/com/、web/shopping4.jsp、WEB-INF/index.jsp等)理解前后端协作机制与典型Java Web项目组织方式。

1. 为什么一个用 Java + Servlet + JSP + MySQL 搭建的购物网站,至今仍是 Java Web 入门最硬核的“通关副本”?

不是因为技术多新——Servlet 3.1 是 2013 年定稿的规范,JSP 在 Spring Boot 主流化后早已退居二线;恰恰相反,正因为它“老”,才暴露出真实工程里最扎手的筋骨:HTTP 请求怎么一层层穿过容器、过滤器、Servlet、JSP 编译器,最终拼出 HTML;数据库连接怎么在无框架下手动管理生命周期,避免连接泄漏;表单提交、会话保持、中文乱码、路径跳转这些看似 trivial 的环节,稍一疏忽就让页面白屏、数据错乱、登录态丢失。这不是玩具 Demo,而是把 Java Web 底层契约全摊开给你看的“裸机训练场”。适合三类人:刚学完 Java 基础想落地 Web 开发的新手、面试前急需补全传统 Web 技术栈的求职者、以及需要快速搭建轻量后台验证业务逻辑的嵌入式/硬件团队。它不追求高并发或微服务,但要求你亲手写web.xml、手写 JDBC 连接池、手写购物车 Session 管理——这些动作本身,就是对 Java Web 运行时最扎实的肌肉记忆。


2. 从零启动:用 Tomcat 9 + JDK 8 搭建可运行的最小购物系统骨架

这个项目不是靠 Spring Boot 自动装配出来的“黑匣子”,它的每一层都必须你亲手拧紧螺丝。我们不装 IDE 插件、不依赖 Maven 模板、不抄 starter,只用最原始的 WAR 包结构和标准 Servlet 容器行为来构建。目标是:部署后能访问首页、展示商品列表、完成用户登录、添加商品到购物车——四个核心链路全部跑通,且每个环节的代码都能在 Tomcat 日志里找到对应输出。

2.1 创建标准 Web 应用目录结构(不依赖任何构建工具)

Java Web 传统项目的生命线是WEB-INF/web.xml和WEB-INF/classes下的字节码。我们手动创建以下结构(路径以shop-web为根):

shop-web/ ├── index.jsp ├── login.jsp ├── product-list.jsp ├── WEB-INF/ │ ├── web.xml │ ├── classes/ │ │ ├── servlet/ │ │ │ ├── LoginServlet.class │ │ │ ├── ProductListServlet.class │ │ │ └── CartAddServlet.class │ │ └── dao/ │ │ ├── ProductDao.class │ │ └── UserDao.class │ └── lib/ │ └── mysql-connector-java-8.0.33.jar └── static/ ├── css/ │ └── style.css └── images/ └── logo.png

提示:WEB-INF是 Tomcat 的安全边界,其下所有资源(包括web.xml、classes、lib)无法被浏览器直接访问,这是 Servlet 规范强制要求。所有业务逻辑必须通过 Servlet 路由,不能把.java源码放进去,只能放编译后的.class。

2.2 配置web.xml:定义 Servlet 映射与初始化参数

WEB-INF/web.xml是整个应用的“宪法”,它声明了哪些类是 Servlet、它们响应什么 URL、是否需要初始化、会话超时时间等。以下是精简但完整的配置(Servlet 3.1+ 兼容):

<?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_3_1.xsd" version="3.1"> <!-- 应用名称 --> <display-name>Shopping Web App</display-name> <!-- 会话超时:30分钟 --> <session-config> <session-timeout>30</session-timeout> </session-config> <!-- 字符编码过滤器:解决 POST 中文乱码 --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>javax.servlet.Filter</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> <!-- 登录 Servlet --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> <!-- 商品列表 Servlet --> <servlet> <servlet-name>ProductListServlet</servlet-name> <servlet-class>servlet.ProductListServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ProductListServlet</servlet-name> <url-pattern>/products</url-pattern> </servlet-mapping> <!-- 购物车添加 Servlet --> <servlet> <servlet-name>CartAddServlet</servlet-name> <servlet-class>servlet.CartAddServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>CartAddServlet</servlet-name> <url-pattern>/cart/add</url-pattern> </servlet-mapping> </web-app>

关键点说明:

  • <session-config>设置会话超时时间,单位为分钟。Tomcat 默认是 30 分钟,但显式声明更可靠,尤其在负载均衡场景下需与反向代理同步。
  • <filter>是解决中文乱码的唯一可靠方式。很多新手在 JSP 里写<%@ page contentType="text/html;charset=UTF-8"%>,但这只影响响应头,对 POST 请求体无效。必须用 Filter 在请求进入 Servlet 前调用request.setCharacterEncoding("UTF-8")。
  • <servlet-class>必须是完整类名(含包路径),且该类必须继承HttpServlet并实现doGet()或doPost()。Tomcat 启动时会扫描WEB-INF/classes下的 class 文件并加载。

2.3 编写第一个 Servlet:LoginServlet实现表单接收与重定向

LoginServlet不做密码加密(生产环境必须加 BCrypt),但必须体现 Servlet 核心契约:获取请求参数、调用 DAO、设置会话属性、转发或重定向。

// src/servlet/LoginServlet.java package servlet; import dao.UserDao; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取表单参数(注意:GET 请求用 getParameter,POST 同样适用) String username = request.getParameter("username"); String password = request.getParameter("password"); // 2. 调用 DAO 验证(此处简化为硬编码校验,实际应查 DB) boolean isValid = new UserDao().validate(username, password); if (isValid) { // 3. 登录成功:将用户信息存入 Session(不是 Cookie!) HttpSession session = request.getSession(true); session.setAttribute("user", username); session.setMaxInactiveInterval(1800); // 30分钟,与 web.xml 一致 // 4. 重定向到首页(避免刷新重复提交) response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { // 5. 登录失败:回传错误信息到 login.jsp request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // GET 请求直接转到登录页 request.getRequestDispatcher("/login.jsp").forward(request, response); } }

逻辑说明与参数说明:

  • request.getSession(true):true表示如果当前没有 Session,则新建一个;false表示只获取已有 Session,不存在则返回 null。登录场景必须确保 Session 存在。
  • response.sendRedirect(...)是客户端重定向,浏览器地址栏会变,且是 302 响应。它比RequestDispatcher.forward()更安全,因为后者是服务器端跳转,URL 不变,用户刷新会导致表单重复提交。
  • request.setAttribute(...)设置的属性仅在本次请求生命周期内有效,用于forward传递数据给 JSP;而session.setAttribute(...)是跨请求的,用于保持登录态。
  • request.getContextPath()返回应用上下文路径(如/shop-web),避免硬编码路径,提升部署灵活性。

编译该类并放入WEB-INF/classes/servlet/LoginServlet.class后,即可通过http://localhost:8080/shop-web/login访问(假设部署名为shop-web)。


3. 数据持久层:手写 JDBC 连接池与商品/用户 DAO 实现

不用 MyBatis、不靠 Hibernate,只用原生 JDBC + DBCP2(Apache Commons DBCP 2.9.0)实现连接复用。这不是复古情怀,而是为了看清:连接怎么申请、怎么归还、事务怎么控制、SQL 注入怎么防御。MySQL 8.0.33 驱动必须用com.mysql.cj.jdbc.Driver,旧版com.mysql.jdbc.Driver已废弃。

3.1 配置DBCP2连接池(WEB-INF/classes/db.properties)

不写死在代码里,用外部配置文件解耦。内容如下:

# db.properties driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/shop_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username=root password=your_password initialSize=5 maxTotal=20 maxIdle=10 minIdle=5 maxWaitMillis=5000 timeBetweenEvictionRunsMillis=30000 minEvictableIdleTimeMillis=60000 testOnBorrow=true validationQuery=SELECT 1

参数说明:

  • useSSL=false:本地开发可关闭 SSL(生产环境必须开启并配置证书);
  • serverTimezone=Asia/Shanghai:必须显式指定,否则 MySQL 8+ 会因时区不匹配抛java.sql.SQLException: The server time zone value '...' is unrecognized;
  • allowPublicKeyRetrieval=true:MySQL 8.0.21+ 默认禁用公钥检索,开启才能连接;
  • initialSize=5:Tomcat 启动时预创建 5 个连接,避免首请求慢;
  • testOnBorrow=true+validationQuery=SELECT 1:每次从池中借连接前执行SELECT 1检测连通性,防止拿到失效连接。

3.2 编写JDBCUtil:封装连接获取与释放逻辑

// src/util/JDBCUtil.java package util; import org.apache.commons.dbcp2.BasicDataSource; import java.io.InputStream; import java.util.Properties; public class JDBCUtil { private static BasicDataSource dataSource; static { try { // 1. 加载配置文件 InputStream is = JDBCUtil.class.getClassLoader() .getResourceAsStream("db.properties"); Properties props = new Properties(); props.load(is); // 2. 初始化 DBCP2 数据源 dataSource = new BasicDataSource(); dataSource.setDriverClassName(props.getProperty("driverClassName")); dataSource.setUrl(props.getProperty("url")); dataSource.setUsername(props.getProperty("username")); dataSource.setPassword(props.getProperty("password")); // 3. 设置连接池参数(与 properties 中一致) dataSource.setInitialSize(Integer.parseInt(props.getProperty("initialSize"))); dataSource.setMaxTotal(Integer.parseInt(props.getProperty("maxTotal"))); dataSource.setMaxIdle(Integer.parseInt(props.getProperty("maxIdle"))); dataSource.setMinIdle(Integer.parseInt(props.getProperty("minIdle"))); dataSource.setMaxWaitMillis(Long.parseLong(props.getProperty("maxWaitMillis"))); dataSource.setTimeBetweenEvictionRunsMillis( Long.parseLong(props.getProperty("timeBetweenEvictionRunsMillis"))); dataSource.setMinEvictableIdleTimeMillis( Long.parseLong(props.getProperty("minEvictableIdleTimeMillis"))); dataSource.setTestOnBorrow(Boolean.parseBoolean(props.getProperty("testOnBorrow"))); dataSource.setValidationQuery(props.getProperty("validationQuery")); } catch (Exception e) { throw new RuntimeException("初始化数据库连接池失败", e); } } public static java.sql.Connection getConnection() throws java.sql.SQLException { return dataSource.getConnection(); } public static void close(java.sql.Connection conn, java.sql.PreparedStatement ps, java.sql.ResultSet rs) { if (rs != null) { try { rs.close(); } catch (java.sql.SQLException e) { /* ignore */ } } if (ps != null) { try { ps.close(); } catch (java.sql.SQLException e) { /* ignore */ } } if (conn != null) { try { conn.close(); } catch (java.sql.SQLException e) { /* ignore */ } } } }

关键设计点:

  • static块保证类加载时只初始化一次dataSource,符合单例模式;
  • close(...)方法必须按ResultSet → PreparedStatement → Connection顺序关闭,且每个都try-catch忽略异常(JDBC 关闭异常不影响主流程);
  • getConnection()返回的是池中连接,调用方无需关心创建逻辑,但必须调用close()归还连接,否则池会耗尽。

3.3 实现ProductDao:参数化查询与结果映射

// src/dao/ProductDao.java package dao; import model.Product; import util.JDBCUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class ProductDao { public List<Product> findAll() throws SQLException { String sql = "SELECT id, name, price, stock FROM product ORDER BY id DESC"; List<Product> list = new ArrayList<>(); try (Connection conn = JDBCUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Product p = new Product(); p.setId(rs.getLong("id")); p.setName(rs.getString("name")); p.setPrice(rs.getBigDecimal("price")); p.setStock(rs.getInt("stock")); list.add(p); } } return list; } public Product findById(Long id) throws SQLException { String sql = "SELECT id, name, price, stock FROM product WHERE id = ?"; Product p = null; try (Connection conn = JDBCUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setLong(1, id); // 防 SQL 注入:用 ? 占位符 + setXXX() try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { p = new Product(); p.setId(rs.getLong("id")); p.setName(rs.getString("name")); p.setPrice(rs.getBigDecimal("price")); p.setStock(rs.getInt("stock")); } } } return p; } }

避坑重点:

  • 所有PreparedStatement必须用setXXX()设置参数,绝不能字符串拼接 SQL(如"WHERE id = " + id),这是 SQL 注入的温床;
  • try-with-resources自动关闭ResultSet、PreparedStatement,但Connection仍需显式close()(已在JDBCUtil.close()中处理);
  • getBigDecimal("price")比getDouble()更安全,避免浮点精度丢失(价格必须用 BigDecimal)。

4. JSP 页面渲染:从 MVC 分离到 EL 表达式与自定义标签实战

JSP 不是“写 HTML + 嵌 Java 代码”,而是 MVC 中的 View 层。它的职责是展示数据、不处理业务、不操作数据库。我们用 EL 表达式替代<% %>脚本,用 JSTL 替代循环判断,用自定义标签封装重复逻辑(如分页、图片路径拼接)。

4.1product-list.jsp:用 JSTL<c:forEach>渲染商品列表

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>商品列表</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/style.css"> </head> <body> <div class="container"> <h1>全部商品</h1> <!-- 判断是否有商品 --> <c:if test="${empty products}"> <p class="no-data">暂无商品</p> </c:if> <!-- 商品卡片列表 --> <div class="product-grid"> <c:forEach items="${products}" var="p"> <div class="product-card"> <img src="${pageContext.request.contextPath}/static/images/product-${p.id}.jpg" alt="${p.name}" width="150" height="150"> <h3>${p.name}</h3> <p class="price"><fmt:formatNumber value="${p.price}" pattern="¥#,##0.00"/></p> <p>库存:<c:out value="${p.stock}"/> 件</p> <form action="${pageContext.request.contextPath}/cart/add" method="post"> <input type="hidden" name="productId" value="${p.id}"> <button type="submit">加入购物车</button> </form> </div> </c:forEach> </div> </div> </body> </html>

关键细节说明:

  • <c:if test="${empty products}">:EL 表达式${empty xxx}是判断集合为空的最简写法,比products == null || products.size() == 0更安全(避免 NPE);
  • ${pageContext.request.contextPath}:动态获取应用上下文路径(如/shop-web),确保静态资源路径在任意部署路径下都正确;
  • <fmt:formatNumber>:JSTL 格式化标签,pattern="¥#,##0.00"保证价格显示为 “¥1,299.00”,避免BigDecimal.toString()输出科学计数法;
  • <c:out value="${p.stock}"/>:自动 HTML 转义,防止 XSS(如stock字段含<script>会被转成&lt;script&gt;)。

4.2 自定义标签:img-url标签统一处理图片路径

JSP 中反复写src="${pageContext.request.contextPath}/static/images/xxx.jpg"易出错。我们写一个自定义标签封装:

  1. 创建WEB-INF/tags/img-url.tag:
<%-- WEB-INF/tags/img-url.tag --%> <%@ tag description="生成图片绝对路径" pageEncoding="UTF-8"%> <%@ attribute name="name" required="true" type="java.lang.String" %> <% String contextPath = pageContext.getRequest().getContextPath(); String url = contextPath + "/static/images/" + pageContext.getAttribute("name"); pageContext.setAttribute("imgUrl", url); %> ${imgUrl}
  1. 在 JSP 中使用:
<%@ taglib prefix="my" tagdir="/WEB-INF/tags" %> <!-- ... --> <img src="<my:img-url name='product-${p.id}.jpg'/>" alt="${p.name}">

优势:路径逻辑集中维护,修改图片目录只需改 tag 文件;标签名my:img-url语义清晰,比硬编码可读性强十倍。

4.3login.jsp:表单验证与错误提示的 JSP 实践

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>用户登录</title> </head> <body> <div class="login-box"> <h2>用户登录</h2> <!-- 显示错误信息 --> <c:if test="${not empty error}"> <div class="error-message">${error}</div> </c:if> <form action="${pageContext.request.contextPath}/login" method="post"> <label>用户名:</label> <input type="text" name="username" required><br><br> <label>密码:</label> <input type="password" name="password" required><br><br> <button type="submit">登录</button> </form> </div> </body> </html>

注意:<input required>是 HTML5 前端校验,不能替代后端校验。它只是用户体验优化,真正校验必须在LoginServlet中进行(如空值判断、长度限制)。


5. 避坑指南:这 4 个血泪问题,90% 的新手在部署时当场翻车

部署阶段不是“复制 WAR 包到 webapps 就完事”,而是暴露底层机制的照妖镜。以下问题我带过 17 届学生实训,几乎人人踩过,列出来就是救命清单。

5.1 现象:访问http://localhost:8080/shop-web/显示 404,但http://localhost:8080/shop-web/index.jsp能打开

原因:web.xml中未配置 welcome-file-list,Tomcat 不知道默认首页是哪个文件。JSP 文件虽能直访,但违背 MVC 路由原则(首页应由 Servlet 控制)。
解决:在web.xml<web-app>标签下添加:

<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>

提示:welcome-file-list是 Servlet 规范的一部分,Tomcat 会按顺序查找index.jsp、index.html等。若写成index.html,则index.jsp不会被命中。

5.2 现象:登录后跳转到index.jsp,但页面顶部显示java.lang.ClassNotFoundException: servlet.ProductListServlet

原因:ProductListServlet.class编译后未放入WEB-INF/classes/servlet/目录,或包名与目录结构不匹配(如类声明package servlet;,但 class 文件放在WEB-INF/classes/根目录下)。
解决:确认编译命令为javac -d WEB-INF/classes -cp "WEB-INF/lib/mysql-connector-java-8.0.33.jar:WEB-INF/lib/commons-dbcp2-2.9.0.jar:WEB-INF/lib/commons-pool2-2.11.1.jar" src/servlet/ProductListServlet.java,且-d参数指向WEB-INF/classes。

5.3 现象:商品列表页面中文显示为??,数据库里明明是 UTF-8

原因:MySQL 连接 URL 缺少characterEncoding=utf8mb4参数,且web.xml的字符编码 Filter 未生效(常见于 Filter mapping 的url-pattern写成/而非/*)。
解决:

  1. 修改db.properties的url行:
    url=jdbc:mysql://localhost:3306/shop_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8mb4
  2. 确保web.xml中 Filter mapping 的url-pattern是/*(不是/或/login);
  3. 在 MySQL 命令行执行ALTER DATABASE shop_db CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;。

5.4 现象:添加商品到购物车后,刷新页面购物车清空

原因:购物车数据存在HttpSession中,但未调用session.setAttribute("cart", cartList),或cartList是局部变量未存入 session,或session.setMaxInactiveInterval()设置过短。
解决:在CartAddServlet中明确写:

HttpSession session = request.getSession(true); List<Product> cart = (List<Product>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<>(); } cart.add(product); // product 从 DAO 查询得到 session.setAttribute("cart", cart); // 必须重新 set,否则引用未更新

注意:session.setAttribute()是值传递,不是引用传递。cart是 ArrayList 引用,cart.add()修改的是堆内存对象,所以session.setAttribute("cart", cart)可省略?错!session.setAttribute()会触发内部序列化/反序列化机制,必须显式调用才能确保 session 容器感知变更。


6. 进阶技巧:用 Filter 实现登录拦截与请求日志,让系统真正“活”起来

一个能上线的购物系统,不能只满足功能跑通。它得有安全底线(未登录不能访问商品页)、有可观测性(知道谁在什么时候做了什么)、有可维护性(日志能定位问题)。这些都不靠框架,靠两个 Filter 就搞定。

6.1 登录拦截 Filter:保护/products、/cart/*等敏感路径

// src/filter/LoginCheckFilter.java package filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行登录页、静态资源、登录接口 if (uri.contains("/login.jsp") || uri.contains("/login") || uri.contains("/static/") || uri.contains("/favicon.ico")) { chain.doFilter(req, resp); return; } // 检查 Session 中是否有 user 属性 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { // 未登录,重定向到登录页 response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 已登录,放行 chain.doFilter(req, resp); } }

注册到web.xml:

<filter> <filter-name>LoginCheckFilter</filter-name> <filter-class>filter.LoginCheckFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginCheckFilter</filter-name> <url-pattern>/products</url-pattern> <url-pattern>/cart/*</url-pattern> </filter-mapping>

效果:访问/products时自动跳转/login.jsp,登录后再回来,这才是真实业务逻辑。

6.2 请求日志 Filter:记录 IP、URL、耗时,不依赖 Log4j

// src/filter/LogFilter.java package filter; import javax.servlet.*; import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class LogFilter implements Filter { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; long start = System.currentTimeMillis(); // 执行后续链 chain.doFilter(req, resp); // 记录日志(打印到控制台,生产环境应写入文件) long end = System.currentTimeMillis(); String ip = getClientIP(request); String uri = request.getRequestURI(); String method = request.getMethod(); int status = ((HttpServletResponse) resp).getStatus(); System.out.printf("[%s] %s %s %s %dms %d%n", LocalDateTime.now().format(FORMATTER), ip, method, uri, end - start, status); } private String getClientIP(HttpServletRequest request) { String xff = request.getHeader("X-Forwarded-For"); if (xff != null && !xff.isEmpty()) { return xff.split(",")[0].trim(); } return request.getRemoteAddr(); } }

注册到web.xml(放在LoginCheckFilter之前,确保日志记录所有请求):

<filter> <filter-name>LogFilter</filter-name> <filter-class>filter.LogFilter</filter-class> </filter> <filter-mapping> <filter-name>LogFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

日志样例:

[2024-06-15 14:22:31] 127.0.0.1 GET /shop-web/products 127ms 200 [2024-06-15 14:22:35] 127.0.0.1 POST /shop-web/cart/add 8ms 302

价值:不用引入任何日志框架,就能获得请求链路基础指标。当用户说“点击加入购物车没反应”,你一眼看到302状态码就知道重定向成功,问题不在后端而在前端跳转逻辑。

6.3 最后一个习惯:每次修改web.xml或添加新 Servlet,务必重启 Tomcat

这是新手最容易忽略的玄学点。Tomcat 对web.xml和WEB-INF/classes下的 class 文件是热加载的,但不是实时生效。尤其是 Servlet 映射、Filter 配置、Listener 注册,必须重启才能确保容器重新解析配置。我见过太多人改完web.xml不重启,然后花两小时 debug “为什么 Filter 不生效”,最后发现是缓存问题。我的做法是:写完配置,关掉 Tomcat 进程,再双击startup.bat(Windows)或./startup.sh(Linux),看着控制台输出Server startup in [xxx] ms才开始测试。这 30 秒,省下 3 小时。

希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询