
简介JSP超市管理系统是一套基于Java Web技术、采用B/S架构的完整教学型项目主要面向正在学习JSP、Servlet与MySQL数据库开发的在校学生或初中级Java开发者适合用于课程设计、毕业设计或自学练手。系统覆盖人事、销售、进货、库存和客户管理五大功能模块其中职工和供货商信息可增删改查销售模块支持销售信息查询与商品盘点进货与库存模块提供完整的商品、库存信息维护客户管理支持添加、删除用户及修改密码逻辑清晰、模块划分明确。资源包为RAR格式容量约1.06MB内含源代码、数据库脚本jspsupermarket.sql及配置文件DBO.java部署时使用MyEclipse 8.5 Tomcat 7.0 MySQL 5.0即可运行管理员账号密码均为admin访问地址为http://127.0.0.1:8080/supermarket/login.jsp。目前已有1023人学习下载对于希望快速理解JSP项目结构、数据库连接方式及后台管理开发流程的读者是一个轻量而实用的参考样例。1. 为什么 2025 年还在用 JSP 做超市管理系统先说一个反直觉的结论JSP 技术本身早已不是主流但“JSP 超市管理系统”这类选题每年依然有大量开发者在做。原因不是 JSP 有多先进而是它恰好卡在“课程设计、毕业设计、中小企业内部系统”这三类需求的正中间。超市管理系统需要的是稳定的增删改查、库存联动、会员积分、订单流水这些业务用 JSP Servlet JDBC 这套经典组合完全能扛住而且部署成本极低——一个 Tomcat、一个 MySQL、一台能跑 Windows 的旧机器就够了。这套技术栈的真正价值在于它把 Web 开发的底层路径完整暴露出来了HTTP 请求怎么进来、Servlet 怎么接、JSP 怎么渲染、JDBC 怎么连库、连接池怎么管理。用 Spring Boot 写这些会被框架掩盖而用 JSP 写一遍整个 Web 结构就刻在脑子里了。本篇就顺着标题里的完整链路用超市管理系统作为载体把 myeclipse 环境配置、MySQL 库表设计、Web 分层结构、Java 代码落地这一条线全部走通。适合正在做课设、接手老旧维护项目、或想补 Java Web 底子的工程师。2. 用 MyEclipse 搭建 JSP MySQL 开发环境的最小方案环境搭建是这类项目里第一个劝退点。MyEclipse 版本、JDK 版本、Tomcat 版本、MySQL 驱动版本这四个东西只要一个不匹配后面全是坑。最常见的错误组合是MyEclipse 2014 JDK 1.8 Tomcat 9 mysql-connector-java 8.x这会导致 servlet-api 版本冲突和驱动类加载失败。稳定组合应该是MyEclipse 2017 CI 或更高版本 JDK 1.8 Tomcat 8.5 mysql-connector-java 5.1.49。Tomcat 8.5 同时兼容 JSP 2.3 和 Servlet 3.1对老项目和新代码都很友好。2.1 MySQL 免安装版配置与初始化MySQL 5.7 的免安装版zip 格式是这个项目里最可控的选择。相比安装版免安装版不受 Windows 服务权限和安装路径空格影响出了问题删掉重来也快。下载对应版本后解压到如D:\mysql-5.7.44-winx64然后以管理员身份打开 CMD执行下面这段初始化流程cd /d D:\mysql-5.7.44-winx64\bin mysqld --initialize-insecure --basedirD:\mysql-5.7.44-winx64 --datadirD:\mysql-5.7.44-winx64\data mysqld --install SuperMarketDB --basedirD:\mysql-5.7.44-winx64 --datadirD:\mysql-5.7.44-winx64\data net start SuperMarketDB mysql -u root --skip-password这段命令的逻辑非常直接--initialize-insecure会生成一个 data 目录并把 root 密码置空这是开发环境专用的初始化方式生产环境必须改用--initialize生成随机密码--install后面跟的SuperMarketDB是自定义的 Windows 服务名避免和系统里已有的 MySQL 服务撞名启动后用--skip-password直接进入是因为初始密码为空。进入 MySQL 命令行后立刻执行下面两条命令把 root 密码改掉并创建业务库ALTER USER rootlocalhost IDENTIFIED BY root123; CREATE DATABASE supermarket CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;这里强调一下字符集的选择。超市管理系统里会有商品名称、供应商地址、会员备注这类字段用 utf8mb4 而不是 utf8是因为 utf8mb4 支持完整的 Unicode 编码包括生僻字和 emoji 表情虽然本系统用不到 emoji但utf8mb4_general_ci的排序规则对中文拼音排序的支持也更自然。2.2 MyEclipse 中配置 Tomcat 与 JDK 的匹配关系MyEclipse 自带的 Tomcat 插件版本通常偏旧建议指向本地安装的独立 Tomcat。配置路径是 Window - Preferences - MyEclipse - Servers - Tomcat - Tomcat 8.x选择 Enable然后把 Tomcat home directory 指向解压目录。这里有个关键参数很多人忽略在 Tomcat 8.x 节点下找到 JDK 子选项把 JRE 改成已安装的 JDK 1.8而不是默认的 JRE。原因在于 JSP 编译成 Servlet 时javac需要完整 JDK 的 tools.jar只给 JRE 会导致编译失败或运行时报ClassNotFoundException: javax.servlet.jsp.JspFactory。JDK 版本用 1.8 不是随便选的。Tomcat 8.5 官方支持 JDK 7 及以上但 JDK 8 是 Java EE 7 规范最成熟的运行环境而且 MyEclipse 的编译器和调试器对 JDK 8 的字节码处理最稳定。如果本机已经装了 JDK 17不要直接切过来用Tomcat 8.5 在高版本 JDK 上运行会有模块访问限制问题需要额外加--add-opens参数老项目不值得为这个折腾。2.3 web.xml 中与 JSP 相关的三个必改参数创建 Web Project 时MyEclipse 会自动生成 web.xml但有三个参数必须手动确认。第一个是welcome-file-list默认是index.jsp如果项目入口是登录页改成login.jsp第二个是 session 超时时间session-config session-timeout30/session-timeout /session-configsession-timeout的单位是分钟超市收银场景下 30 分钟比较合理。收银员可能离开工位去补货回来时如果 session 已经过期购物车数据全部丢失这个坑在课设答辩现场尤其致命。第三个参数是 servlet 版本声明MyEclipse 2017 生成的 web.xml 头信息是 Servlet 3.1 规范不要在部署时把 web.xml 换成 2.5 的旧版本否则 JSP 里 EL 表达式的默认开启状态会不一致。提示把项目部署到 Tomcat 的 webapps 目录时注意项目名不要含有中文和空格否则 JSP 页面跳转时request.getContextPath()返回的路径可能包含被编码后的字符导致 URL 匹配失灵。3. 超市管理系统 MySQL 库表设计与 JDBC 连接层的落地数据库设计决定了这个系统后续的所有代码结构。超市管理系统最核心的实体不是商品而是“库存流水”。很多初学者把库存设计成商品表里的一个stock字段这在账面库存小于实际库存时根本查不出来原因。正确做法是把库存拆成“当前库存”和“出入库流水”两张表当前库存只做查询展示所有增减操作都写流水。3.1 五张核心业务表的建表 SQL下面这套结构覆盖了超市管理的最小业务闭环用户登录、供应商管理、商品入库、会员消费、订单出库。USE supermarket; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(20), role TINYINT DEFAULT 1 COMMENT 1-收银员 2-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE supplier ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, contact_person VARCHAR(20), phone VARCHAR(20), address VARCHAR(200) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, barcode VARCHAR(30) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, spec VARCHAR(50), unit VARCHAR(10), purchase_price DECIMAL(10,2), sale_price DECIMAL(10,2), stock INT DEFAULT 0, supplier_id INT, warn_stock INT DEFAULT 10, INDEX idx_supplier (supplier_id), CONSTRAINT fk_product_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock_flow ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, change_count INT NOT NULL COMMENT 正数入库负数出库, before_stock INT, after_stock INT, type TINYINT COMMENT 1-采购入库 2-销售出库 3-盘点调整, operator_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_product_time (product_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sale_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, member_phone VARCHAR(20), total_amount DECIMAL(10,2), payment_method TINYINT COMMENT 1-现金 2-微信 3-支付宝, operator_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这条 SQL 里的关键决策有三个。product.barcode设置唯一索引是因为超市扫码枪要求条码必须唯一重复条码会直接导致扫出来的商品不确定stock_flow里同时保存before_stock和after_stock两个快照字段是为了将来对账时直接看流水就能还原当时的库存状态不需要回放全部历史外键fk_product_supplier在这里保留是因为超市管理系统的数据量级几千个商品下外键的维护成本远低于它带来的数据一致性收益。3.2 JDBC 工具类与数据库连接参数JDBC 连接层在这个系统里承担两个职责获取连接和释放资源。用连接池还是用 DriverManager取决于并发量。超市收银场景通常在 3-5 个收银台并发量极低用 DriverManager 完全够用还能少引一个 C3P0 或 DBCP 依赖。但连接参数必须写在配置文件里不能硬编码在 Java 代码中。在src目录下新建db.propertiesjdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.passwordroot123对应的 DBUtil 工具类package com.supermarket.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ResourceBundle; public class DBUtil { private static String driver; private static String url; private static String username; private static String password; static { try { ResourceBundle bundle ResourceBundle.getBundle(db); driver bundle.getString(jdbc.driver); url bundle.getString(jdbc.url); username bundle.getString(jdbc.username); password bundle.getString(jdbc.password); Class.forName(driver); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException e) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) {} } if (conn ! null) { try { conn.close(); } catch (SQLException e) {} } } }注意这里有个细节ResourceBundle.getBundle(db)读取的是db.properties文件而不是db.properties文件名本身。getBundle 参数是资源包的基本名它会在 classpath 下搜索db.properties。Class.forName(driver)这行用的是com.mysql.jdbc.Driver对应 mysql-connector-java 5.1.x如果换了 8.x 驱动类名要改成com.mysql.cj.jdbc.DriverURL 里还要加serverTimezoneAsia/Shanghai否则会报时区异常。3.3 商品入库与库存扣减的事务边界超市系统里最容易被忽视的事务场景是“销售出库”。一次销售行为要更新 sale_order 表、写 stock_flow 流水、还要回改 product.stock这三步要么全成功要么全失败。下面这段代码演示了在 JDBC 原生环境下怎么用手动事务控制保证一致性public boolean createOrder(String orderNo, int productId, int count) { Connection conn null; PreparedStatement psOrder null; PreparedStatement psFlow null; PreparedStatement psStock null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); String sqlOrder INSERT INTO sale_order (order_no, product_id, count) VALUES (?, ?, ?); psOrder conn.prepareStatement(sqlOrder); psOrder.setString(1, orderNo); psOrder.setInt(2, productId); psOrder.setInt(3, count); psOrder.executeUpdate(); String sqlFlow INSERT INTO stock_flow (product_id, change_count, before_stock, after_stock, type) SELECT ?, ?, stock, stock - ?, 2 FROM product WHERE id ?; psFlow conn.prepareStatement(sqlFlow); psFlow.setInt(1, productId); psFlow.setInt(2, -count); psFlow.setInt(3, count); psFlow.setInt(4, productId); psFlow.executeUpdate(); String sqlStock UPDATE product SET stock stock - ? WHERE id ? AND stock ?; psStock conn.prepareStatement(sqlStock); psStock.setInt(1, count); psStock.setInt(2, productId); psStock.setInt(3, count); int rows psStock.executeUpdate(); if (rows 0) { conn.rollback(); return false; } conn.commit(); return true; } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) {} e.printStackTrace(); return false; } finally { DBUtil.close(psOrder, conn); DBUtil.close(psFlow, null); DBUtil.close(psStock, null); } }WHERE id ? AND stock ?这个条件是库存扣减的并发安全防线。在单机收银场景下两个收银员同时操作同一个商品的概率极低但这条语句用数据库的行锁天然挡住了超卖问题。UPDATE返回的影响行数为 0 时说明库存不足直接回滚整个事务不会产生只写了订单没扣库存的脏数据。4. Web 结构分层从 JSP 页面到 Servlet 再到 JavaBean 的请求链路JSP 的 Web 结构有一个必须遵守的铁律JSP 页面里不允许写 Java 业务代码。这个约束不是风格问题而是 JSP 编译成 Servlet 后内嵌的% %脚本会被原样塞进_jspService()方法里业务逻辑一旦写在页面里报错时堆栈信息会直接指向 Tomcat 生成的 class 文件定位成本极高。正确结构是 JSP 只负责展示Servlet 只做参数接收和页面转发JavaBean 处理全部业务逻辑。4.1 MVC 在超市管理系统中的实际映射超市管理系统的代码包结构按职责划分成bean、dao、servlet、util四个包每个包的职责边界要清晰到能直接说出“哪个类不能被谁调用”。bean包放实体类字段与数据库表一一对应dao包放 JDBC 操作只接受参数返回结果集不碰 request 和 responseservlet包接收 HTTP 请求调用 dao 后再把结果放进 request 域或 session 域最后转发或重定向。util 包放 DBUtil、字符串处理等工具类。下面这个商品查询的 Servlet 展示了完整的请求流转路径package com.supermarket.servlet; import java.io.IOException; import java.util.List; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.supermarket.bean.Product; import com.supermarket.dao.ProductDao; WebServlet(/product/list) public class ProductListServlet extends HttpServlet { private ProductDao productDao new ProductDao(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String keyword request.getParameter(keyword); String pageNum request.getParameter(pageNum); int page 1; if (pageNum ! null !.equals(pageNum)) { page Integer.parseInt(pageNum); } ListProduct list productDao.queryByPage(keyword, page, 10); request.setAttribute(productList, list); request.getRequestDispatcher(/product/list.jsp).forward(request, response); } }这段代码里的两个设计决策值得展开。request.setCharacterEncoding(UTF-8)放在 doGet 开头必须做否则从页面传来中文商品名会在 Tomcat 默认的 ISO-8859-1 解码下变成乱码request.getRequestDispatcher().forward()选择转发而不是重定向是因为转发里 request 域中存放的productList还保得住重定向会丢失全部请求属性。查询结果放到 request 域而不是 session 域是为了避免收银员查一次商品就占一份 session 内存多台收银机同时查询时 session 膨胀会非常快。4.2 JavaBean 实体与数据库字段的类型对应JavaBean 里的字段类型要和数据库字段类型严格对应否则ResultSet.getXxx()取值时会报类型转换异常。这张对照表是 JSP 项目里最容易出错的几个对应关系数据库字段类型Java 字段类型说明INTint/Integer使用 Integer 可以区分“未赋值”和“值为 0”DECIMAL(10,2)BigDecimal用 double 会导致金额精度丢失DATETIMEjava.util.Date不要用 java.sql.Date缺少时分秒TINYINTint/boolean用 boolean 取值时需判断是否支持 MySQL 隐式转换VARCHARString无特殊注意事项商品表对应的 JavaBean 写法package com.supermarket.bean; import java.math.BigDecimal; public class Product { private Integer id; private String barcode; private String name; private String spec; private String unit; private BigDecimal purchasePrice; private BigDecimal salePrice; private Integer stock; private Integer supplierId; private Integer warnStock; // 关联字段不属于 product 表但页面展示时需要 private String supplierName; public Integer getId() { return id; } public void setId(Integer id) { this.id id; } // 其余 getter/setter 略 }最后一个supplierName字段是一个隐含的业务需求商品列表页要显示供应商名称但 supplier 表在数据库里和 product 表是外键关系。这个关联字段不需要在 product 表里建立冗余列而是在 DAO 层用 LEFT JOIN 查出来再 set 进去。页面里直接用${p.supplierName}就能取到省去了在 JSP 里嵌套查询供应商表的麻烦。4.3 DAO 层用 PreparedStatement 替代 Statement 的原因商品查询 DAO 里如果用 Statement 拼接 SQL会出现 SQL 注入和拼接引号的双重问题。超市管理系统的搜索框是黑客最容易攻击的入口比如在商品名输入框里输入 OR 11 --如果代码是字符串拼接这条输入就会把整张商品表拉出来。PreparedStatement 的预编译机制从根上解决了这个隐患——参数用占位符?传给 MySQL 服务端数据库会先编译后传值哪怕参数里带特殊字符也只会被当作普通字符串处理。public ListProduct queryByPage(String keyword, int page, int pageSize) { ListProduct list new ArrayList(); String sql SELECT p.*, s.name AS supplier_name FROM product p LEFT JOIN supplier s ON p.supplier_id s.id WHERE p.name LIKE ? LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setInt(2, (page - 1) * pageSize); ps.setInt(3, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Product p new Product(); p.setId(rs.getInt(id)); p.setBarcode(rs.getString(barcode)); p.setName(rs.getString(name)); p.setPurchasePrice(rs.getBigDecimal(purchase_price)); p.setSalePrice(rs.getBigDecimal(sale_price)); p.setStock(rs.getInt(stock)); p.setSupplierName(rs.getString(supplier_name)); list.add(p); } } } catch (SQLException e) { e.printStackTrace(); } return list; }LIMIT ?, ?的占位符写法有两个注意点第一分页参数必须用setInt而不能拼进 SQL 里MySQL 对 LIMIT 的预编译支持是完整的第二页码换算公式(page - 1) * pageSize要在 Java 侧算好不能让数据库去做乘法。还有个小毛病是purchase_price用getBigDecimal读取这是原始值页面想要格式化显示两位小数时可以在 JSP 中用fmt:formatNumber标签处理。5. JSP 页面的三个必踩坑EL 表达式、图书管理系统的分页、离开页面的拦截提示JSP 页面本身的技术含量不高但细节极多。EL 表达式能不能取到值、分页条能不能翻动、离开页面时浏览器弹的提示怎么屏蔽这些问题几乎每个做 JSP 项目的人都会遇到而且网上答案一半是错的。5.1 EL 表达式取值为空时的四层排查商品列表页用c:forEach循环输出商品数据时最常见的结果是页面上什么都显示不出来。这时候不要先怀疑 JSTL 标签的写法而是按下面这个顺序排查第一层看 JSTL 的 jar 包有没有引入。Tomcat 8.5 默认带 JSP 编译器和 EL 解析器但不带 JSTL 实现必须在 WEB-INF/lib 下放 jstl-1.2.jar。第二层确认 JSP 页面头部有没有写% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %少了这行c:forEach会被当成普通文本输出。第三层检查 Servlet 转发前是否真的把数据放进了 request 域——很多人把request.setAttribute写成了session.setAttribute结果刷新页面反而数据在、换页就丢。第四层也是最隐蔽的看 JSP 页面对应的 Servlet 是否走了request.getRequestDispatcher(/product/list.jsp).forward()如果写成了response.sendRedirect()request 域里的数据全部丢失这是重定向和转发的经典区别。提示在 JSP 中调试时可以在页面最前面加一段${requestScope.productList}看看 EL 表达式能不能直接访问到数据。如果能显示内存地址但循环输出为空大概率是c:forEach的 items 属性写错了变量名。5.2 分页条参数传递与 pageNum 的字符串转换分页是 JSP 项目里最容易出逻辑错误的环节。首页、上一页、下一页、末页四个链接的 href 写法要遵循同一个规则统一把当前页码作为参数传给同一个 ServletServlet 内解析后再决定翻到哪一页。比较简单的方法是每页都生成下面五个链接c:if test${pageNum 1} a href${pageContext.request.contextPath}/product/list?pageNum1首页/a a href${pageContext.request.contextPath}/product/list?pageNum${pageNum - 1}上一页/a /c:if span第 ${pageNum} 页 / 共 ${totalPage} 页/span c:if test${pageNum totalPage} a href${pageContext.request.contextPath}/product/list?pageNum${pageNum 1}下一页/a a href${pageContext.request.contextPath}/product/list?pageNum${totalPage}末页/a /c:if这里的一个坑是搜索联动。如果用户输入了关键词“可乐”然后翻到第 3 页链接只带 pageNum 会把 keyword 参数丢掉导致翻页变成全量查询。正确做法是翻页链接必须把当前搜索条件原样带上比如页面里${param.keyword}回填到输入框翻页链接写?pageNum2keyword${param.keyword}同时 Servlet 里要判断 keyword 为空时不能拼LIKE %%否则会全表扫描。5.3 离开页面的浏览器确认提示的实现与屏蔽“屏蔽 jsp 离开页面提示”是网上高频搜索词这里说清楚它的原理。浏览器弹出的离开提示来自beforeunload事件它是一个浏览器原生机制不是 JSP 或 Tomcat 的功能。JSP 项目中如果页面加了下面的代码用户关闭或跳转时就会弹确认框window.onbeforeunload function() { return 确定要离开吗; };但有三个场景下这个提示不该出现表单已提交成功后的跳转、点击“退出登录”按钮、管理员正常翻页。因为这三种行为是有意离开弹窗反而添乱。屏蔽方法不是删除事件函数而是在事件函数里加状态判断let allowLeave false; window.onbeforeunload function(e) { if (allowLeave) return; e.preventDefault(); e.returnValue ; }; // 表单提交成功后放开拦截 function enableLeave() { allowLeave true; window.location.href ${pageContext.request.contextPath}/product/list; }e.preventDefault()和e.returnValue 是在 Chrome 和 Firefox 下触发展示提示的标准写法。特别提醒不要尝试用return null或return undefined来屏蔽浏览器规范已经调整这两个返回值仍会触发提示唯一可靠的屏蔽方式就是设置一个布尔标志位在明确该离开的时候打开。6. 用 Filter 统一处理字符编码、登录校验与 JSP 页面访问控制JSP 项目做到后面会发现一个重复劳动每个 Servlet 里都要写request.setCharacterEncoding(UTF-8)每个非登录页面都要检查 session 里有没有用户信息。Filter 就是专门消除这种重复的机制——它能在请求进入 Servlet 之前和之后插入统一的处理逻辑这也是 JSP Web 结构中最能体现“架构感”的一层。实现一个字符编码和登录校验的双重 Filterpackage com.supermarket.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; WebFilter(/*) public class EncodingLoginFilter implements Filter { private static final String[] EXCLUDE_PATHS {/login.jsp, /user/login}; public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); String path request.getRequestURI().substring(request.getContextPath().length()); boolean needLogin true; for (String exclude : EXCLUDE_PATHS) { if (path.startsWith(exclude)) { needLogin false; break; } } if (needLogin) { HttpSession session request.getSession(); Object user session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } } chain.doFilter(req, resp); } }这个 Filter 暴露了几个比其他写法更健壮的地方。WebFilter(/*)拦截所有请求其中静态资源也会被拦但登录校验只对动态路径生效这是判断了path.startsWith之后才放行的response.sendRedirect到登录页用的是重定向因为此时 request 域里的数据不重要用户被跳走后重新走一次完整的 GET 请求流程安全边界清晰。过滤器的执行顺序要留意如果有多个 Filter执行顺序取决于 web.xml 中filter-mapping的声明顺序注解方式下顺序不可控所以多 Filter 场景建议还是用 web.xml 而不是注解。关于 JSP 页面的访问控制还有一个细节是常见的WEB-INF目录下的 JSP 页面是无法通过 URL 直接访问的因为 Tomcat 默认对WEB-INF目录做了访问限制。如果不想为每个 JSP 页面都加 Filter 白名单可以把所有页面放到WEB-INF/pages/下然后封装一个 BaseServlet所有请求必须走 Servlet 转发才能进入页面这样就天然堵死了未登录用户绕过登录页直接访问 JSP 的漏洞。代价是不能直接用 URL 访问页面需要每次通过request.getRequestDispatcher(/WEB-INF/pages/product/list.jsp)转发到页面路径写起来稍微啰嗦一些但安全性是值得的。7. 部署到 Tomcat 后 JSP 编译 class 的定位与常见故障验证JSP 项目写完不是结束部署到 Tomcat 后才是真正的验收阶段。这个阶段最需要知道的是一个问题JSP 文件经过 Tomcat 编译生成的 class 文件到底存到了哪里。搞清楚这个问题几乎所有 JSP 相关的报错都能自己定位。Tomcat 对 JSP 的编译产物默认存放在WORK目录。以 Tomcat 8.5 为例路径是Tomcat安装目录/work/Catalina/localhost/项目名/org/apache/jsp/。JSP 文件product/list.jsp会被编译成product/list_jsp.java和product/list_jsp.class两个文件存放于此。里面的list_jsp.java是完整的 Servlet 源码当你看到页面上出现异常但 Console 里没有具体报错时直接打开这个.java文件搜索_jspService方法能精确定位到哪一行 JSP 代码导致编译失败。尤其是 JSP 中% %脚本块里写的 Java 代码和页面里引用的 JavaBean 类不在同一个 package 时编译 error 信息会明确指向生成的 Servlet 文件。部署阶段还有四个高频故障需要掌握判断方法这里给出最直接的验证命令# 查看 Tomcat 是否启动了项目 curl -I http://localhost:8080/supermarket/login.jsp # 查看 MySQL 端口是否在监听 netstat -ano | findstr :3306 # Linux 环境下查看 Tomcat 运行日志中的异常堆栈 tail -f Tomcat安装目录/logs/catalina.out如果curl -I返回 404先检查项目有没有被正确部署到 webapps 下再看访问路径是不是少了项目名如果返回 500立刻去Tomcat安装目录/logs/localhost.2025-XX-XX.log里看当天日期的异常堆栈JSP 编译失败、数据库连接池耗尽、类找不到这三大类问题都会写在这里。netstat查 3306 端口是为了排除 MySQL 没启动或端口被占用的情况如果是 MySQL 服务没起来Tomcat 的报错通常是Cannot create PoolableConnectionFactory这个信息链要快速建立起来。关于 MySQL 连接参数的迁移问题再补一个技巧当超市管理系统从课设环境移到生产服务器时db.properties里的jdbc.url需要把localhost改成服务器 IP同时确认 MySQL 的bind-address配置允许外部访问。如果连接被拒在 MySQL 命令行执行GRANT ALL PRIVILEGES ON supermarket.* TO root% IDENTIFIED BY root123; FLUSH PRIVILEGES;授权即可这是数据库侧最常见的权限拦截点多数连接失败问题都是出在这条授权没有执行。本文还有配套的精品资源点击获取