news 2026/10/24 1:28:04

JSP+Servlet+JavaBean超市进销存系统实战:从环境搭建到库存扣减避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+Servlet+JavaBean超市进销存系统实战:从环境搭建到库存扣减避坑指南

简介:这是一套面向计算机专业学生与Java Web初学者的超市进销存管理系统源码,采用JSP+Servlet+JavaBean经典三层架构,配合MySQL数据库实现商品、库存、进货与销售等核心业务模块,适合作为毕业设计、课程设计或框架入门练手项目。压缩包共134个文件,约6.94MB,包含14个Java源文件与对应class文件、9个JSP页面、16个js与15个css前端资源,以及图片、字体、jar依赖和项目配置文件,结构完整、层次清晰。源码均经本地编译验证,按文档配置环境即可运行,难度适中,内容经助教老师审定,已有68人学习关注。读者可从中获得一套可直接运行的完整项目方案,涵盖控制器、实体类、数据库操作与Excel导出等模块实现,便于理解Servlet请求流转与JavaBean数据封装,也可在此基础上二次开发或作为答辩参考,遇到问题还可私信作者获取解答。

1. 从一份 JSP+Servlet+JavaBean 的超市进销存系统说起:它到底解决什么问题

如果你手头正躺着一个「基于JSP+Servlet+JavaBean的超市进销存管理系统.zip」,大概率是三种情况之一:课程设计要交、毕业设计要改、或者面试前想拿一个能讲清楚 MVC 分层的小项目练手。这个标题里的三个技术名词,其实对应的是 Java Web 最经典的开发范式——JSP 负责视图渲染,Servlet 负责请求分发和业务调度,JavaBean 负责数据封装和业务逻辑承载。三者拼在一起,就是一个不依赖 Spring 全家桶、纯原生 Servlet 容器就能跑起来的进销存系统。

超市进销存的核心诉求并不复杂:商品要能录入和分类,采购要能入库并更新库存,销售要能出库并扣减库存,库存要能预警和盘点,最好再有几个角色(管理员、采购员、收银员)分权操作。听起来像 CRUD 堆叠,但真正落地时,库存并发扣减、事务边界、分页查询、中文编码、Session 管理这些坑一个都不会少。这篇文章不打算给你讲教科书式的 MVC 理论,而是顺着这个项目的典型结构,把「怎么跑起来、怎么改、怎么不翻车」一条线讲透。适合有 Java 基础、学过一点 HTML 和 JDBC、但还没完整做过一个 Web 项目的同学,也适合想拿这个项目当模板去改造成其他管理系统的人。

2. 把项目跑起来之前:环境、数据库与目录结构的三重对齐

2.1 为什么 JDK、Tomcat、MySQL 的版本组合最容易让人翻车

这类原生 JSP+Servlet 项目,最常见的运行环境是 JDK 8 + Tomcat 8.5/9.0 + MySQL 5.7/8.0。别小看这个组合,版本错一位就可能出现ClassNotFoundException或者Unknown initial character set index这类玄学报错。JDK 8 是因为很多老项目的 Servlet API 还是javax.servlet包名,Tomcat 10 之后改成了jakarta.servlet,直接不兼容。MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,而 5.x 是com.mysql.jdbc.Driver,连接 URL 还要加时区和 SSL 参数。

我一般会先确认三件事:java -version输出是不是 1.8;Tomcat 的lib目录下有没有servlet-api.jar;MySQL 能不能用命令行登录。这三步过了,再谈导入项目。很多人一上来就双击startup.bat,结果控制台一闪而过,连错误都看不到,这就是典型的「环境没对齐就开跑」。

提示:Tomcat 启动闪退时,不要双击 startup.bat,用命令行进入 bin 目录执行catalina run,错误日志会直接打印在控制台。

2.2 数据库建表与连接配置:从 SQL 脚本到 db.properties

进销存系统的数据库一般至少包含这几张表:user(用户与角色)、goods(商品信息)、category(商品分类)、purchase(采购入库记录)、sale(销售出库记录)、stock(库存快照或流水)。下面是一段最小可用的建表 SQL,你可以直接拿去在 MySQL 里执行:

-- 商品分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '分类名称' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品表,stock 字段直接存当前库存,简化版不做流水 CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT, price DECIMAL(10,2) DEFAULT 0.00, stock INT DEFAULT 0, warn_stock INT DEFAULT 10 COMMENT '库存预警阈值', FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户表,role 区分管理员和普通操作员 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, role VARCHAR(20) DEFAULT 'staff' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建完表之后,连接配置通常放在src/db.properties里,用Properties类加载。注意 URL 里的useUnicode=true&characterEncoding=utf8不能省,否则中文商品名插进去就是问号。MySQL 8.0 还要加serverTimezone=Asia/Shanghai&useSSL=false。密码如果用了caching_sha2_password插件,老驱动可能连不上,要么升级驱动,要么改回mysql_native_password。

2.3 目录结构怎么摆:WEB-INF 下放什么、不放什么

一个标准的原生 Java Web 项目目录大概长这样:src下按包名分dao、service、servlet、bean、util;WebContent(或webapp)下放jsp、css、js、WEB-INF。关键点是:WEB-INF目录下的 JSP 不能被浏览器直接访问,只能通过 Servlet 转发进去,这恰好是做权限控制的一道天然屏障。web.xml可以放在WEB-INF下做 Servlet 映射,也可以用@WebServlet注解代替,后者更省事但要求 Servlet 3.0+。

我见过有人把db.properties放在WebContent根目录,结果浏览器直接能下载到数据库密码,这是血泪教训。配置文件要么放src下随类路径加载,要么放WEB-INF下用ServletContext.getResourceAsStream读取。目录结构一旦摆错,后面写多少代码都是白搭。

3. JavaBean、DAO 与 Servlet 的分层写法:从商品列表到库存扣减

3.1 JavaBean 到底该不该写业务逻辑

JavaBean 在这个项目里承担两个角色:一是数据载体(POJO),对应数据库表的字段,提供 getter/setter;二是可选的业务封装,比如StockBean里写一个isLowStock()方法判断是否低于预警值。我的建议是:POJO 保持干净,业务逻辑尽量放 Service 层,但像「库存是否预警」这种跟数据强绑定的简单判断,放 Bean 里也无妨,反而让 JSP 页面调用更直观。

一个典型的GoodsBean 长这样:

public class Goods { private int id; private String name; private int categoryId; private double price; private int stock; private int warnStock; // 判断库存是否低于预警线,JSP 里可直接调用 public boolean isLowStock() { return this.stock <= this.warnStock; } // getter/setter 省略 }

注意isLowStock这种is开头的方法,在 JSP 里用 EL 表达式${goods.lowStock}就能直接取到,不需要写完整方法名。这是 JavaBean 命名规范带来的便利,很多人不知道,结果在 JSP 里写${goods.isLowStock()}报错。

3.2 DAO 层用 JDBC 还是连接池:一个务实的选择

原生项目里,DAO 层最常见的是直接写 JDBC,每个方法自己拿连接、关连接。这样做简单,但并发一上来就崩,因为每次DriverManager.getConnection都新建物理连接。稍微靠谱一点的做法是用 Druid 或 C3P0 连接池,但引入连接池又要加 jar 包和配置文件。我的经验是:如果只是课程设计或本地演示,直接 JDBC 够用;如果要放到服务器上给几十个人同时用,必须上连接池。

下面是一个不带连接池的商品查询 DAO 方法,重点看PreparedStatement和try-with-resources的写法:

public List<Goods> findAll() { List<Goods> list = new ArrayList<>(); String sql = "SELECT * FROM goods ORDER BY id DESC"; // try-with-resources 自动关闭连接,避免泄漏 try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Goods g = new Goods(); g.setId(rs.getInt("id")); g.setName(rs.getString("name")); g.setStock(rs.getInt("stock")); g.setWarnStock(rs.getInt("warnStock")); list.add(g); } } catch (SQLException e) { e.printStackTrace(); } return list; }

DBUtil是一个工具类,负责从db.properties读配置并返回连接。参数说明:ORDER BY id DESC让最新商品排前面;rs.getInt("warnStock")里的列名必须和数据库一致,大小写不敏感但拼错就报Column not found。如果查询慢,先看goods表的category_id有没有建索引,再看是不是SELECT *拖了不需要的大字段。

3.3 Servlet 做请求分发:一个库存扣减的完整链路

库存扣减是进销存里最不能出错的地方。典型链路是:收银页面提交商品 ID 和数量 → Servlet 接收参数 → 调用 Service → Service 开启事务 → DAO 先查库存再更新 → 返回结果。这里最大的坑是「先查后改」在并发下会超卖,两个线程同时查到库存 1,都认为够,结果扣成 -1。解决办法是用UPDATE goods SET stock = stock - ? WHERE id = ? AND stock >= ?,把判断和更新合并成一条原子 SQL。

// 销售出库的 Servlet 核心逻辑 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int goodsId = Integer.parseInt(req.getParameter("goodsId")); int qty = Integer.parseInt(req.getParameter("qty")); // 调用 Service,返回受影响行数 int rows = goodsService.reduceStock(goodsId, qty); if (rows > 0) { resp.sendRedirect("saleList.jsp"); } else { req.setAttribute("msg", "库存不足或商品不存在"); req.getRequestDispatcher("sale.jsp").forward(req, resp); } }

Service 层的reduceStock里执行那条原子 UPDATE,返回rows为 0 就说明库存不够。参数说明:qty必须做正整数校验,否则用户传-5就变成加库存了。sendRedirect和forward的区别也要清楚:前者是浏览器二次请求,地址栏变;后者是服务器内部转发,地址栏不变。库存扣减成功后用重定向,可以防止用户刷新页面重复提交。

4. 避坑与排查:JSP+Servlet 项目里最容易翻车的 5 个地方

4.1 中文乱码:现象是页面问号,原因是编码链没统一

现象:商品名在 JSP 页面显示正常,但插入数据库后变成???,或者表单提交后后台收到乱码。原因通常有三处:JSP 页面pageEncoding没设 UTF-8;Servlet 里没调request.setCharacterEncoding("UTF-8");数据库连接 URL 没加characterEncoding=utf8。解决:JSP 头部写<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>,Servlet 的doPost第一行就设编码,数据库 URL 补全参数。Tomcat 8 之后 GET 请求默认 UTF-8,但 POST 必须手动设。

4.2 空指针异常:现象是 500 错误,原因是 Session 过期或参数没接住

现象:访问某个页面直接 500,日志里NullPointerException。原因多半是session.getAttribute("user")返回 null 后直接调方法,或者request.getParameter拿到的值是 null 就parseInt。解决:在 Servlet 里先判空再操作,登录拦截可以用 Filter 统一处理,没登录就重定向到登录页。参数转换用try-catch包住NumberFormatException,给用户一个友好提示而不是堆栈。

4.3 数据库连接泄漏:现象是跑一会儿就卡死,原因是连接没关

现象:系统刚启动正常,操作几十次后越来越慢,最后报Too many connections。原因是在 DAO 里拿了连接但异常路径下没关闭,或者ResultSet没关。解决:一律用 try-with-resources,把Connection、PreparedStatement、ResultSet都放在 try 的括号里。如果用了连接池,还要在finally里close()归还连接,而不是真的关闭物理连接。

4.4 JSP 里写 Java 代码太多:现象是页面难维护,原因是没守住 MVC 边界

现象:JSP 页面里<% %>脚本块一大堆,查数据库、算逻辑、拼 HTML 全混在一起。原因是为了省事,把本该书在 Servlet 或 Bean 里的代码直接写进了 JSP。解决:JSP 只负责展示,数据用request.setAttribute传进来,用 EL 和 JSTL 取值。循环用<c:forEach>,判断用<c:if>。这样改页面不用动 Java 代码,改逻辑不用动页面。

4.5 库存超卖:现象是库存变成负数,原因是并发下先查后改

现象:两个人同时买最后一件商品,两个订单都成功,库存扣成 -1。原因就是前面说的「先查后改」非原子。解决:用UPDATE ... WHERE stock >= ?一条语句完成判断和扣减,或者用SELECT ... FOR UPDATE加行锁。前者更轻量,后者适合扣减前还要做复杂校验的场景。不管哪种,都要在事务里执行,并且检查返回的受影响行数。

5. 进阶技巧:用 Filter 做权限拦截与库存预警的自动提醒

5.1 一个 Filter 搞定登录校验和角色控制

原生 Servlet 项目里,Filter 是最被低估的组件。你不需要在每个 Servlet 里写一遍「判断有没有登录」,只需要一个AuthFilter,拦截所有.jsp和业务路径,检查 Session 里有没有用户对象。更进一步,还能根据role字段控制访问权限,比如采购入库只允许管理员和采购员访问,收银员访问就跳转到无权限页面。

@WebFilter("/*") // 拦截所有请求 public class AuthFilter implements Filter { // 白名单:登录页、登录接口、静态资源不拦截 private static final String[] WHITE_LIST = {"/login.jsp", "/login", "/css/", "/js/"}; public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 白名单直接放行 for (String white : WHITE_LIST) { if (uri.contains(white)) { chain.doFilter(req, resp); return; } } // 检查 Session Object user = request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); } else { chain.doFilter(req, resp); } } }

参数说明:@WebFilter("/*")表示拦截所有请求,白名单里的路径用contains判断,实际项目里可以用startsWith更精确。request.getContextPath()拿到项目根路径,避免硬编码/超市系统这种带中文的路径。注意 Filter 里不要做耗时操作,否则每个请求都变慢。

5.2 库存预警怎么自动提醒:定时任务还是每次查询时判断

库存预警有两种做法:一是每次打开商品列表时,在 Service 里查一遍低于预警线的商品,塞到request里让页面弹提示;二是用Timer或ScheduledExecutorService定时扫描,把预警写进一张消息表。前者简单,适合小系统;后者适合需要主动推送的场景。我一般用前者,因为实现成本低,用户每次进首页就能看到红色预警条。

具体做法是在GoodsService里加一个List<Goods> findLowStock(),SQL 写WHERE stock <= warn_stock。然后在首页的 Servlet 里调用它,request.setAttribute("lowStockList", list),JSP 里用<c:if test="${not empty lowStockList}">显示一个警告框。这个技巧不复杂,但能让你的系统看起来「有脑子」,答辩或演示时是个加分项。

5.3 分页查询的通用写法:别每次手写 LIMIT

商品列表和销售记录一多,必须分页。原生 JDBC 分页就是LIMIT offset, size,但每次手写容易算错 offset。我习惯封装一个PageBean<T>,包含list、totalCount、pageNo、pageSize、totalPage五个字段,DAO 里先查COUNT(*)再查LIMIT。这样 JSP 页面只需要遍历pageBean.list,底部分页条用pageBean.totalPage循环生成页码。

一个容易忽略的细节:LIMIT的 offset 从 0 开始,所以offset = (pageNo - 1) * pageSize。如果用户传的pageNo小于 1 或大于总页数,要在 Service 里纠正,否则查出来是空列表,用户以为数据丢了。分页做得好不好,直接决定这个系统能不能从「演示」变成「能用」。

5.4 我踩过的最大一个坑:别在 JSP 里直接 new DAO

最后说一个我早期犯的错:在 JSP 页面里<% new GoodsDao().findAll() %>直接查数据库。当时觉得方便,后来发现页面加载慢、连接泄漏、逻辑没法复用,改一处要翻遍所有 JSP。后来全部改成 Servlet 调 Service、Service 调 DAO,JSP 只拿现成数据,世界才清净。如果你正在改这个项目,第一件事就是把 JSP 里的 Java 代码清干净,哪怕多写几个 Servlet 也值得。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 2:35:30

01.02.01.Astra系列 环境搭建篇(CentOS9系统下OpenCV安装配置)

本页目录 1、安装配置2、测试api&#xff1a;https://osgeo.cn/opencv-python/ch01-setup/chapter.html 安装配置参考: 01.01.02.DeepSeek&#xff1a;环境搭建篇&#xff08;安装配置 显卡驱动&#xff09; 01.01.01.DeepSeek&#xff1a;环境搭建篇&#xff08;安装配置 开发…

作者头像 李华
网站建设 2026/10/16 10:46:16

Python实战:从零搭建知识库问答seq2seq模型,含注意力与部署优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 4:25:05

智能家居B端交付系统的三大架构瓶颈与轻量化破局方案

摘要&#xff1a;本文基于11年智能家居供应链与全链路交付的一线经验&#xff0c;从系统架构视角拆解B端从业者面临的三个结构性瓶颈——产品利润层被双向挤压、交付服务层容错率为零、售后运维层隐性成本反噬。针对每个瓶颈提出可落地的轻量化方案&#xff1a;产品组合解耦与信…

作者头像 李华
网站建设 2026/10/15 15:39:49

AI原生开发工具链:Superpowers概念解析与四大组件协同实践

1. “Superpowers”不是超能力&#xff0c;是开发者工具链的智能增强层最近在好几个技术社区里&#xff0c;看到越来越多开发者在问“superpowers 怎么装”“cursor superpowers 开启不了”“antigravity 和 superpowers 是什么关系”。这词儿听着像漫威电影里的设定&#xff0…

作者头像 李华