简介基于JSPServletJavaBean的超市进销存管理系统源码压缩包面向需要完成毕业设计或课程设计的计算机相关专业学生以超市进货、销售、库存和供应商信息管理为主线融合JSP、Servlet、MySQL等常用技术难度适中适合用来理解传统JavaWeb分层开发流程。压缩包共134个文件体积6.94MB包含.java源文件、.class编译文件、.jsp页面、.css/.js样式脚本、.jar依赖库以及jpg/png/gif等界面素材和XML等配置文件既有可直接运行的完整工程也便于对照源码学习前后端交互与数据库操作。目前已有68人学习/浏览作为课程设计或备查资料具备一定参考价值。项目内含商品、用户、管理员等实体类、控制器、数据库操作类和导入导出工具类结构清晰下载后按配套文档配置MySQL与Tomcat环境即可运行便于快速跑通流程并在此基础上扩展功能。从控制器、实体类到数据操作与工具类整体分层思路较清楚有利于二次开发和答辩讲解。1. JSPServletJavaBean的老技术栈做个超市进销存还要不要选它拿到这份“基于JSPServletJavaBean的超市进销存管理系统.zip”你大概率是正在做课程设计或者毕业设计的计算机专业学生也可能是想快速搭一套内部管理系统的小团队。这个压缩包对应的是一套很典型的Java Web工程商品档案、进货单、销售单、库存预警、用户登录和权限拦截所有功能都靠JSP、Servlet、JavaBean这三件套手写出来不依赖Spring这类重型框架。很多人第一反应是“这技术太老了还有必要碰吗”但恰恰是这套老技术栈能让你把HTTP请求怎么进Tomcat、Session怎么保持登录态、JDBC怎么连数据库这些底层原理一次看清。后面学Spring MVC时你会发现很多设计都是在给这套手写流程做封装所以先把这个项目跑通、读透账一点都不亏。2. 搭建超市进销存的工程骨架三层分包、核心表与JDBC连接封装2.1 从MVC看JSP、Servlet、JavaBean各自分到的活“JSPServletJavaBean”不是三个组件随便堆在一起而是MVC模式在Java Web里的经典落地。JSP承担View负责把数据渲染成页面Servlet承担Controller接收请求、调用业务逻辑、决定跳转JavaBean承担Model封装商品、订单这类业务数据同时配合DAO类完成数据库读写。很多人初次写这个项目会犯一个通病在JSP里直接写JDBC代码页面里塞满% %脚本段一个月后再看自己都看不懂。正确的做法是让JSP页面只出现HTML和JSTL标签Java代码全部下沉到Servlet和JavaBean里。这个项目的典型分包结构是包名职责entity实体类对应数据库表的字段例如Goods、Supplier、Purchasedao数据访问层用JDBC操作数据库例如GoodsDAO、PurchaseDAOservlet控制层接收请求、调用DAO、转发或重定向utilJDBC连接工具、字符串工具类filter登录拦截、编码处理过滤器JavaBean在JSP时代还有一个专门的用法通过jsp:useBean标签在页面里创建和访问对象。但实际做进销存系统时我更推荐把它当成普通POJO使用配合Servlet和DAO完成数据流转这样结构更清晰也不容易踩作用域坑。JSP里要展示商品列表Servlet从DAO拿到ListGoods塞进request域后转发给JSPJSP只用${goods.goodsName}就能输出数据。2.2 建表设计商品、进货、销售、库存四类数据怎么落库超市进销存的核心数据可以拆成四张主表和两张明细表。商品表goods用来存商品档案进货表purchase和销售表sale分别记进货单和销售单业务上“一张单对应多个商品”所以还需要purchase_item和sale_item两张明细表。库存不需要单独建表它直接作为goods表里的stock字段存在通过进货和销售时更新商品记录来维护。建库建表时有一个很关键的细节数据库字符集一定要用utf8mb4否则后面存商品备注里的中文表情符号或者部分生僻字会直接乱码或者报“Incorrect string value”。建库语句我一般这样写CREATE DATABASE shop_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, goods_code VARCHAR(32) NOT NULL, goods_name VARCHAR(64) NOT NULL, spec VARCHAR(32) DEFAULT , price DECIMAL(10,2) NOT NULL, purchase_price DECIMAL(10,2) DEFAULT 0.00, stock INT DEFAULT 0, stock_low INT DEFAULT 5, category_id INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这张商品表里stock_low是库存预警阈值当stock stock_low时列表页就要给这一行标红提示补货。金额字段用DECIMAL(10,2)而不是double是因为浮点数在计算金额时会有精度丢失0.1加0.2不等于0.3这种问题在后面对账时会非常麻烦。购买记录表里还要记录单价和金额因为商品价格可能变动进货单要保留当时的价格快照。商品编码建议用带业务含义的编号例如前两位是分类后四位是流水号这样在门店盘点时扫码找货更方便。2.3 把JDBC连接封装进DBUtil参数写在一个文件里整个系统所有DAO都要拿数据库连接如果每个类都重复写一遍DriverManager.getConnection后期换数据库或者改密码就得满工程替换。我一般会抽一个DBUtil类把URL、用户名、密码集中放在静态常量里再用静态代码块加载驱动类。package cn.shop.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/shop_db ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } }这段代码里几个参数值得说明一下。characterEncodingutf8保证从数据库读出来的中文不乱码serverTimezoneAsia/Shanghai是MySQL 8.x的必填参数不设会报时区错误allowPublicKeyRetrievaltrue是MySQL 8.x首次连接时用缓存SHA-256密码认证需要的开关useSSLfalse则避免一堆SSL警告日志。如果你用的是MySQL 5.x驱动类名要换回com.mysql.jdbc.DriverURL里时间戳参数也可以去掉。注意看我把Class.forName放在静态代码块里而不是每次连接时都加载一次驱动。因为驱动类只需要注册一回重复加载虽然不报错但确实属于多余开销。这个DBUtil类要放到util包里其他DAO通过DBUtil.getConnection()拿连接用完在finally块里关闭或者直接用Java 7的try-with-resources语法自动关闭。2.4 web.xml和页面编码跑起来之前先定好的三件事工程要能跑web.xml里有三个配置不能省欢迎页、字符编码过滤器、Servlet映射。如果你用的是Servlet 3.0以上的TomcatServlet可以用WebServlet注解免去web.xml里的映射配置但欢迎页和过滤器仍然建议写在web.xml里。欢迎页直接指向登录页避免访问根路径时Tomcat弹一个目录列表出来。welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list filter filter-nameencodingFilter/filter-name filter-classcn.shop.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping这个编码过滤器会在所有请求进入Servlet之前把request.setCharacterEncoding(UTF-8)执行掉解决POST请求的中文参数乱码。GET请求的中文参数在Tomcat 8.5以上默认就是UTF-8解码不用额外处理如果是老Tomcat还需要在server.xml的Connector上配置URIEncodingUTF-8。JSP文件头也要统一加上pageEncodingUTF-8否则文件里的中文注释和写死的提示文案在浏览器里会显示成乱码。这三件事在项目一开始就定好后面调试的时候能少掉一半的“怪问题”。我们经常说的“同一套代码换台电脑就乱码”八成就是这三个地方的编码不一致导致的。3. 商品管理模块完整链路DAO、Servlet、JSP三步走并带上分页3.1 实体类和商品DAO增删改查的每一步都不能省校验商品管理是进销存系统里最基础也最能体现功底的模块。先从实体类开始Goods类的字段要和goods表一一对应这里我用BigDecimal存价格避免浮点误差。package cn.shop.entity; import java.math.BigDecimal; public class Goods { private Integer id; private String goodsCode; private String goodsName; private String spec; private BigDecimal price; private BigDecimal purchasePrice; private Integer stock; private Integer stockLow; private Integer categoryId; private String createTime; // 每个字段的getter/setter省略 }实体类里的字段命名和数据库列名保持一致后面用ResultSet取值时可以直接按列名get。stockLow这个字段在列表页做预警高亮时要频繁读取不要偷懒不写getter。接着写GoodsDAO这里主推用PreparedStatement而不是Statement一方面防止SQL注入一方面参数占位符让SQL语句可读性更好。以搜索加分页这个最常用的查询为例public ListGoods search(String keyword, int pageNo, int pageSize) { ListGoods list new ArrayList(); String sql SELECT * FROM goods WHERE goods_name LIKE ? OR goods_code LIKE ? LIMIT ?,?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); ps.setInt(3, (pageNo - 1) * pageSize); ps.setInt(4, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Goods g new Goods(); g.setId(rs.getInt(id)); g.setGoodsCode(rs.getString(goods_code)); g.setGoodsName(rs.getString(goods_name)); g.setStock(rs.getInt(stock)); g.setStockLow(rs.getInt(stock_low)); list.add(g); } } } catch (SQLException e) { throw new RuntimeException(查询商品列表失败, e); } return list; }这段代码用了try-with-resources语法Connection、PreparedStatement、ResultSet都会在方法结束时自动关闭不用再写冗长的finally块。LIMIT的偏移量是(pageNo - 1) * pageSize这地方新手经常算错页码从1开始第一页要查的是第0条到第pageSize条所以偏移量是0而不是pageSize。LIKE匹配时要自己拼%符号注意这里拼接的是字符串不是用户输入安全上没问题。实际开发中查询条件往往还要加上category_id、库存区间用StringBuilder动态拼SQL和参数列表就好。新增和更新商品时除了INSERT和UPDATE的SQL不同逻辑上要注意两点一是价格、库存这类数字字段要做非空校验页面传来的空字符串直接Integer.parseInt会炸二是更新操作最好先按id查一遍确认商品存在再执行否则影响行数为0时容易误判成更新失败。3.2 商品ServletdoGet查数据doPost写数据顺序别颠倒Servlet是MVC里的控制层它的职责是解析请求参数、调用DAO、决定页面跳转。很多初学者把增删改查全都塞进doPost里这是不对的。我一般这样分工doGet处理列表查询、进入编辑页、删除确认跳转doPost处理表单提交的新增和更新。用WebServlet(/goods)注解映射省去web.xml里的servlet配置。WebServlet(/goods) public class GoodsServlet extends HttpServlet { private GoodsDAO goodsDAO new GoodsDAO(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (edit.equals(action)) { int id Integer.parseInt(req.getParameter(id)); req.setAttribute(goods, goodsDAO.findById(id)); req.getRequestDispatcher(/WEB-INF/pages/goods_edit.jsp).forward(req, resp); return; } // 默认走列表查询 int pageNo req.getParameter(pageNo) null ? 1 : Integer.parseInt(req.getParameter(pageNo)); int pageSize 10; String keyword req.getParameter(keyword); if (keyword null) { keyword ; } PageGoods page goodsDAO.findPage(keyword, pageNo, pageSize); req.setAttribute(page, page); req.getRequestDispatcher(/WEB-INF/pages/goods_list.jsp).forward(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String action req.getParameter(action); if (save.equals(action)) { // 读取表单字段组装Goods对象 // 新增或更新入库 } else if (delete.equals(action)) { // 按id删除 } resp.sendRedirect(req.getContextPath() /goods); } }这里有一个非常重要的设计查询操作用forward转发写操作结束后用sendRedirect重定向。原因是POST提交后如果直接转发到列表页用户按F5刷新浏览器会再次提交表单造成商品重复插入重定向会先让浏览器重新GET一次/goods刷新时只刷新列表不会重放表单。这个模式叫PRG理解之后以后用Spring MVC时你会经常见到它。另外注意转发路径前有/WEB-INF/pages/前缀这是把JSP页面放在WEB-INF目录下保护起来用户无法直接通过URL访问页面只能通过Servlet跳转进入。登录模块做好之后这个设计还能防止未登录用户直接打开页面源码。3.3 用JSTL渲染列表页分页和搜索条件一起保持住JSP页面里不建议写大段Java脚本段显示商品列表用JSTL的c:forEach标签就够% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table thead tr th商品编码/thth商品名称/thth库存/th th预警线/thth售价/thth操作/th /tr /thead tbody c:forEach items${page.list} varg tr td${g.goodsCode}/td td${g.goodsName}/td td${g.stock}/td td${g.stockLow}/td td${g.price}/td td a href${pageContext.request.contextPath}/goods?actioneditid${g.id}编辑/a a href${pageContext.request.contextPath}/goods?actiondeleteid${g.id} onclickreturn confirm(确认删除该商品吗)删除/a /td /tr /c:forEach /tbody /table页面里所有路径都用${pageContext.request.contextPath}拼出来这样项目部署到Tomcat的任意名字的webapp下都不怕路径错误。删除操作我在演示代码里用了GET链接但在真实系统里删除是修改数据应该改成POST表单提交否则链接被搜索引擎爬虫或者预加载插件碰到就会误删商品。列表页下方的分页条用c:forEach循环输出页码点击时将当前页码作为pageNo参数传给/goods。这里有一个隐蔽的坑搜索时输入了“可乐”点击第2页时如果分页链接只传了pageNokeyword就丢了第二页又会回到全量列表。所以分页链接必须把当前的keyword一起带上c:forEach begin1 end${page.totalPage} vari a href${pageContext.request.contextPath}/goods?pageNo${i}keyword${param.keyword}${i}/a /c:forEach表格操作列如果只是三个普通文本链接会显得简陋常见做法是引入Element UI图标库之类的前端资源来美化按钮。在JSP里用这类图标和普通网页没什么区别真正要小心的是路径问题静态资源要放在webapp/statics目录下引用时同样用${pageContext.request.contextPath}/statics/css/index.css拼接否则刷新页面图标就消失。图标字体在JSP里的坑多半是相对路径失效把icon的class名挂在i标签上注意font-face的url路径要用绝对路径。3.4 商品列表的库存预警高亮写DAO别写页面超市进销存里库存预警是个高频需求。实现方式有两种一种是在商品列表页遍历时判断stock stockLow然后给表格行加一个CSS class另一种是查询时就直接查出来。我更推荐在DAO里把这条判断留给页面但不在JSP里写Java判断而是引入一个status字段。public PageGoods findPage(String keyword, int pageNo, int pageSize) { // 常规分页查询代码 // SQL里加一列 // CASE WHEN stock stock_low THEN 1 ELSE 0 END AS low_stock_flag }JSP里用c:if判断这个flag为1时给tr加classdangerCSS里定义红色背景半透明色即可。这样写的好处是以后要改成按店铺维度预警、批量采购提醒只需要改SQL和一个JSP条件不用动页面结构。商品管理模块整个链路跑通之后你应该已经体会到“Servlet收参数、DAO管SQL、JSP管展示”三层协作的节奏了。这套节奏是后面所有业务模块的模板。4. 进货、销售与库存联动用事务和条件更新保住库存数据4.1 进货单提交主表、明细表、库存三个写操作必须绑在一起超市进销存的业务核心在于进货和销售都会改变库存而这两类操作都不只是“改一个数字”那么简单。一次进货需要同时做三件事在进货主表purchase里插入一条单据记录、在进货明细表purchase_item里插入本次进货的商品明细、把每件商品的可卖库存累加回goods表。这三件事要么全部成功要么全部失败不能用代码里if判断代替必须用数据库事务。Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 插入进货主表拿到自增主键 String insertPurchase INSERT INTO purchase(order_no, supplier_id, total_amount, operator, create_time) VALUES (?,?,?,?,NOW()); PreparedStatement ps conn.prepareStatement(insertPurchase, Statement.RETURN_GENERATED_KEYS); ps.setString(1, orderNo); ps.setInt(2, supplierId); ps.setBigDecimal(3, totalAmount); ps.setString(4, operator); ps.executeUpdate(); ResultSet rs ps.getGeneratedKeys(); rs.next(); long purchaseId rs.getLong(1); // 2. 逐条插入明细表并更新库存 for (PurchaseItem item : items) { PreparedStatement psItem conn.prepareStatement( INSERT INTO purchase_item(purchase_id, goods_id, quantity, price, amount) VALUES (?,?,?,?,?)); psItem.setLong(1, purchaseId); psItem.setInt(2, item.getGoodsId()); psItem.setInt(3, item.getQuantity()); psItem.setBigDecimal(4, item.getPrice()); psItem.setBigDecimal(5, item.getAmount()); psItem.executeUpdate(); PreparedStatement psStock conn.prepareStatement( UPDATE goods SET stock stock ? WHERE id ?); psStock.setInt(1, item.getQuantity()); psStock.setInt(2, item.getGoodsId()); psStock.executeUpdate(); } conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw new RuntimeException(进货单保存失败, e); } finally { if (conn ! null) { conn.close(); } }这段代码里的关键点是setAutoCommit(false)和commit()/rollback()配套使用。默认情况下JDBC每次executeUpdate都会自动提交一旦明细插入到一半数据库挂了主表里有单、库存没加上账目就对不上了。手动开启事务后三个写操作共用一个Connection任何一步抛异常都回滚到初始状态。Statement.RETURN_GENERATED_KEYS配合getGeneratedKeys()拿到数据库自动生成的主键这一步在自增主键的MySQL里是唯一可靠的方式不要想着自己生成一个purchase_id传进去并发一高就可能撞键。有同行会问这事务不就是把代码包一层try-catch吗实际坑在细节里conn在事务开启后如果发生异常没有rollback就关闭连接MySQL会按配置自动回滚或提交这种不可控的状态最危险。所以catch块里必须显式rollback()finally里再close。4.2 销售出库防超卖先查询再判断不如一条条件更新销售模块是反向操作库存扣减逻辑和进货正好相反。新手写扣库存时通常会这样做先SELECT stock FROM goods WHERE id?Java代码里判断stock quantity再UPDATE goods SET stockstock-? WHERE id?。这在小门店单机部署时没问题但只要被两台收银机同时访问就会出现超卖两个请求都读到库存还有10都判断可以卖都去扣减最后库存变成负数。这就是并发下的竞态条件。正确的办法是把判断和扣减合并成一条带条件的UPDATE让数据库来保证原子性String sql UPDATE goods SET stock stock - ? WHERE id ? AND stock ?; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, quantity); ps.setInt(2, goodsId); ps.setInt(3, quantity); int rows ps.executeUpdate(); if (rows 0) { // 说明库存不足抛异常回滚整笔销售单 throw new SQLException(商品ID goodsId 库存不足); }WHERE stock ?这个条件确保了扣减操作只在库存足够时执行数据库的行锁会串行化并发请求后到的请求发现库存已经被扣光影响行数为0于是走回滚。这比Java代码里if (stock quantity)的判断严谨得多已经是乐观锁思想的雏形。销售模块同样要开事务插入销售主表、插入销售明细、扣减库存三件事和进货模块的代码结构对称。门店场景里还经常要做“退换货”那就是把扣减改成增加同时反向记一条退货流水这个封装成reverseSale()方法即可。4.3 进货与销售共用一张库存账数据准确性靠对账进销存系统跑到月底老板最关心的问题一定是“账上的库存和实际货架上的数对得上吗”。商品库存的准确性本质上取决于进货和销售两条业务线是否都严格走了“单据明细库存变动”三步。最常出问题的位置是“直接改库存”新手图省事在商品编辑页加了一个库存字段经理手一抖把库存改了没有留下任何业务单据月底对账时就找不出这笔差异来源。所以做这个项目时我强烈建议商品编辑页里的“库存”字段只展示不可编辑库存变动只能通过进货单和销售单触发。进货单里的order_no要设计成带规则的单号进货用PO加日期再加流水号销售用SO加日期加流水号这样从数据库里按单号排序就能看出一天的业务量打印小票时也不用额外拼接。明细表里保存price和amount快照是为了将来商品涨价后老订单的金领和库存仍然能按当时的进价来核对毛利而不是随商品档案的价格漂移。4.4 给业务代码留三个检查口数量、金额、日期进货销售这两个模块的代码跑通后我建议在提交路径上留三个检查口。第一数量字段必须大于0前端表单做了校验后台Servlet里也要再校验一次因为总有人能绕过前端第二金额要和单价乘以数量核对防止前端把总金额传过来后台直接采信第三单据日期不要取客户端时间要用数据库的NOW()否则收银机系统时间不准会导致日结报表错位。这三个检查口看起来琐碎但恰恰是这类系统的血泪经验所在。我在课程设计答辩时老师最喜欢问的也是“你如何保证数量和金额的合法性”把这三条讲清楚加分效果很明显。5. 常见问题排查端口占用、ClassNotFound、乱码与登录失效的5个现场5. 常见问题排查500、404、ClassNotFound、乱码与登录失效的5个现场5.1 404与路径问题上下文路径忘了带现象项目启动成功后浏览器访问http://localhost:8080/goods报404Tomcat控制台没有任何报错。原因有两个高频点一是Servlet映射路径写成了/goods但项目部署名不是根路径实际必须访问/shop/goods二是JSP里链接写成了href/goods少了${pageContext.request.contextPath}前缀浏览器把请求发到了Tomcat根路径下。解决方法是全局搜索所有href和src属性统一替换成c:url或者${pageContext.request.contextPath}拼接。另外/WEB-INF/pages/下的JSP不能直接通过URL访问这是保护机制不是404配置错误。5.2 中文乱码页面、请求、数据库三层要同时对齐现象商品名称在列表页显示成“???”或者表单提交后数据库里存的是乱码更隐蔽的是同一套代码别人电脑上正常、自己电脑上乱码。原因在于JSP页面编码、Servlet请求编码、数据库字符集三个环节只要有一个不一致中文就会在某一步被错误解码乱掉。解决办法按顺序检查JSP文件头的pageEncodingUTF-8、Web.xml里的EncodingFilter、JDBC URL里的characterEncodingutf8、数据库创建时的utf8mb4。Tomcat 8.5以上版本GET请求默认UTF-8如果是老版本还要在server.xml的Connector上补URIEncodingUTF-8。中文乱码解决起来不难难在要养成“从页面到数据库一条链路排查”的习惯不要只改一处就以为能好。5.3 ClassNotFoundException驱动类明明导入了还是找不到现象Tomcat启动后访问第一个查询商品的请求控制台报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因通常是MySQL驱动jar包没有发布到WEB-INF/lib目录下。Eclipse或IntelliJ IDEA里有时候你在Build Path中添加了jar但它不会自动复制到部署目录里。另外注意驱动类名MySQL Connector/J 8.x的驱动类改成com.mysql.cj.jdbc.Driver了5.x是com.mysql.jdbc.Driver类名写错也会报ClassNotFound。解决方法是把mysql-connector-java的jar手动放到WEB-INF/lib下然后右键项目选择刷新并重启Tomcat确保部署目录里真的存在这个文件。5.4 8080端口被占用上次没关干净还是别的服务在跑现象启动Tomcat时报Port 8080 required by Tomcat v9.0 Server at localhost is already in use有时候还伴随SEVERE: Failed to initialize end point。原因多半是上一个Tomcat实例没有完全关闭或者电脑上装了其他占8080端口的服务。解决方法是终端执行netstat -ano | findstr :8080找到监听8080的PID再用taskkill /PID 进程号 /F结束掉。如果8080被一些系统服务占用且不能杀就修改conf/server.xml里的HTTP端口注意8005和8009两个端口也要一起改成不冲突的值改一个端口仍然可能因为其他两个端口被占用而启动失败。5.5 登录后跳回首页又变成未登录状态现象输入用户名密码登录成功后跳转到主页面但主页面的头部不显示用户信息访问其他受保护页面又被弹回登录页。解决办法分三步排查先看在LoginServlet里有没有执行request.getSession().setAttribute(currentUser, user)再看Filter有没有把登录请求本身拦截掉导致Session刚建立就被重定向最后检查浏览器F12里的CookieJSessionId有没有正常种下Cookie的Path是否是/项目名。一个很隐蔽的坑是用localhost登录和用127.0.0.1登录浏览器认为是两个站点Cookie不互通这边登录了那边还是未登录排查时统一访问地址再试。另外浏览器禁用Cookie时Session完全失效前端登录页要提示用户开启。注意Filter拦截路径判断不要用uri.startsWith(/login)要加上contextPath再判断否则项目部署名下的/shop/login.jsp和/shop/loginxxx都会被意外放行或拦截。5.6 改了JSP页面不生效看起来像是没保存其实是Tomcat缓存现象改了JSP文件里的文字刷新页面还是旧内容重启Tomcat之后才正常。这种现象在开发期被我们戏称为“玄学”。原因在于Tomcat会把JSP编译成class缓存到work/Catalina目录下IDE增量发布有时没有触发重新编译。解决分两步第一IDE里执行Project Clean强制重新编译第二手动删除Tomcat安装目录下work/Catalina里对应的项目缓存文件夹然后重启。之后养成习惯改JSP后看一眼Tomcat控制台有没有重新编译的日志输出没有的话就手动清理。排查这类问题我通常建议按“浏览器F12看Network → 看HTTP状态码 → 看Tomcat catalina日志 → 再定位到DAO的SQL”这个顺序走。前端能看到404、500就直接定位到路径和Servlet前端正常但数据不对才需要去翻SQL和数据库不要一开始就怀疑代码逻辑那样只会越查越乱。6. 验证与后续演进手工SQL验库存再决定要不要上框架整个系统跑通后最重要的不是急着加功能而是先验证数据链路是否正确。我最常用的验证方式是写一条对账SQL把商品表里的库存和进货明细、销售明细的合计差做比对。SELECT g.goods_name, g.stock AS current_stock, (IFNULL(SUM(pi.quantity), 0) - IFNULL(SUM(si.quantity), 0)) AS flow_stock FROM goods g LEFT JOIN purchase_item pi ON pi.goods_id g.id LEFT JOIN sale_item si ON si.goods_id g.id GROUP BY g.id, g.goods_name, g.stock HAVING current_stock ! flow_stock;这条SQL的核心逻辑是库存的账面值必须等于“累计进货量减去累计销售量”。如果查询结果为空说明事务和库存联动做得是对的如果不为空就把不一致的商品挑出来按时间倒序翻进货单和销售单找到漏了哪一笔。这个验证技巧在你接手别人写的进销存项目时特别有用不用读全部代码先跑对账SQL就知道系统干净不干净。验证通过之后再谈改造。如果这份JSPServletJavaBean的代码是你的课程设计答辩时老师问“为什么不直接用Spring Boot”你可以从这张表里找答案JSP项目里每一次请求路径、每一次SQL调用、每一个Session操作都是显式的你能完整说清楚Java Web运行时的请求生命周期框架最大的价值是把重复劳动封装起来不是替代你理解原理。如果你打算把它演进成可上线的系统我一般建议分三步第一步把JDBC手写DAO换成Spring JDBC或MyBatis让SQL和Java代码解耦第二步引入Maven管理依赖把mysql-connector-java、jstl这些jar全部从lib目录里解放出来第三步把JSP的公共头尾抽出来配合EL表达式和JSTL标签做模板复用。这三步做完项目结构就会向主流的SSM靠拢之后再迁移Spring Boot就很顺滑。我做这套系统的习惯是每完成一个模块先打开数据库手工跑一遍SQL验证数据再继续下一块。宁可功能少做一个也不要让库存和销售对不上账希望帮到你。本文还有配套的精品资源点击获取