JSP超市管理系统实战:环境搭建、库表设计与项目部署
2026/9/14 3:43:25 网站建设 项目流程

简介: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 --basedir=D:\mysql-5.7.44-winx64 --datadir=D:\mysql-5.7.44-winx64\data mysqld --install SuperMarketDB --basedir=D:\mysql-5.7.44-winx64 --datadir=D:\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 'root'@'localhost' 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-timeout>30</session-timeout> </session-config>

session-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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE supplier ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, contact_person VARCHAR(20), phone VARCHAR(20), address VARCHAR(200) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这条 SQL 里的关键决策有三个。product.barcode设置唯一索引,是因为超市扫码枪要求条码必须唯一,重复条码会直接导致扫出来的商品不确定;stock_flow里同时保存before_stockafter_stock两个快照字段,是为了将来对账时直接看流水就能还原当时的库存状态,不需要回放全部历史;外键fk_product_supplier在这里保留,是因为超市管理系统的数据量级(几千个商品)下,外键的维护成本远低于它带来的数据一致性收益。

3.2 JDBC 工具类与数据库连接参数

JDBC 连接层在这个系统里承担两个职责:获取连接和释放资源。用连接池还是用 DriverManager,取决于并发量。超市收银场景通常在 3-5 个收银台,并发量极低,用 DriverManager 完全够用,还能少引一个 C3P0 或 DBCP 依赖。但连接参数必须写在配置文件里,不能硬编码在 Java 代码中。

src目录下新建db.properties

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=root123

对应的 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.propertiesClass.forName(driver)这行用的是com.mysql.jdbc.Driver,对应 mysql-connector-java 5.1.x;如果换了 8.x 驱动,类名要改成com.mysql.cj.jdbc.Driver,URL 里还要加serverTimezone=Asia/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 在超市管理系统中的实际映射

超市管理系统的代码包结构按职责划分成beandaoservletutil四个包,每个包的职责边界要清晰到能直接说出“哪个类不能被谁调用”。bean包放实体类,字段与数据库表一一对应;dao包放 JDBC 操作,只接受参数返回结果集,不碰 request 和 response;servlet包接收 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); } List<Product> 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 1=1 --,如果代码是字符串拼接,这条输入就会把整张商品表拉出来。PreparedStatement 的预编译机制从根上解决了这个隐患——参数用占位符?传给 MySQL 服务端,数据库会先编译后传值,哪怕参数里带特殊字符也只会被当作普通字符串处理。

public List<Product> queryByPage(String keyword, int page, int pageSize) { List<Product> 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_pricegetBigDecimal读取,这是原始值,页面想要格式化显示两位小数时,可以在 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 prefix="c" uri="http://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 写法要遵循同一个规则:统一把当前页码作为参数传给同一个 Servlet,Servlet 内解析后再决定翻到哪一页。比较简单的方法是每页都生成下面五个链接:

<c:if test="${pageNum > 1}"> <a href="${pageContext.request.contextPath}/product/list?pageNum=1">首页</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}回填到输入框,翻页链接写?pageNum=2&keyword=${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 nullreturn undefined来屏蔽,浏览器规范已经调整,这两个返回值仍会触发提示;唯一可靠的屏蔽方式就是设置一个布尔标志位,在明确该离开的时候打开。

6. 用 Filter 统一处理字符编码、登录校验与 JSP 页面访问控制

JSP 项目做到后面会发现一个重复劳动:每个 Servlet 里都要写request.setCharacterEncoding("UTF-8"),每个非登录页面都要检查 session 里有没有用户信息。Filter 就是专门消除这种重复的机制——它能在请求进入 Servlet 之前和之后插入统一的处理逻辑,这也是 JSP Web 结构中最能体现“架构感”的一层。

实现一个字符编码和登录校验的双重 Filter:

package 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.javaproduct/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;授权即可,这是数据库侧最常见的权限拦截点,多数连接失败问题都是出在这条授权没有执行。

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

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

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

立即咨询