恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
JSP+Servlet+JavaBean超市进销存系统实战:从环境搭建到库存扣减避坑指南
首页
资讯中心
/
JSP+Servlet+JavaBean超市进销存系统实战:从环境搭建到库存扣减避坑指南
JSP+Servlet+JavaBean超市进销存系统实战:从环境搭建到库存扣减避坑指南
发布时间:2026/10/9 1:32:54
简介这是一套面向计算机专业学生与Java Web初学者的超市进销存管理系统源码采用JSPServletJavaBean经典三层架构配合MySQL数据库实现商品、库存、进货与销售等核心业务模块适合作为毕业设计、课程设计或框架入门练手项目。压缩包共134个文件约6.94MB包含14个Java源文件与对应class文件、9个JSP页面、16个js与15个css前端资源以及图片、字体、jar依赖和项目配置文件结构完整、层次清晰。源码均经本地编译验证按文档配置环境即可运行难度适中内容经助教老师审定已有68人学习关注。读者可从中获得一套可直接运行的完整项目方案涵盖控制器、实体类、数据库操作与Excel导出等模块实现便于理解Servlet请求流转与JavaBean数据封装也可在此基础上二次开发或作为答辩参考遇到问题还可私信作者获取解答。1. 从一份 JSPServletJavaBean 的超市进销存系统说起它到底解决什么问题如果你手头正躺着一个「基于JSPServletJavaBean的超市进销存管理系统.zip」大概率是三种情况之一课程设计要交、毕业设计要改、或者面试前想拿一个能讲清楚 MVC 分层的小项目练手。这个标题里的三个技术名词其实对应的是 Java Web 最经典的开发范式——JSP 负责视图渲染Servlet 负责请求分发和业务调度JavaBean 负责数据封装和业务逻辑承载。三者拼在一起就是一个不依赖 Spring 全家桶、纯原生 Servlet 容器就能跑起来的进销存系统。超市进销存的核心诉求并不复杂商品要能录入和分类采购要能入库并更新库存销售要能出库并扣减库存库存要能预警和盘点最好再有几个角色管理员、采购员、收银员分权操作。听起来像 CRUD 堆叠但真正落地时库存并发扣减、事务边界、分页查询、中文编码、Session 管理这些坑一个都不会少。这篇文章不打算给你讲教科书式的 MVC 理论而是顺着这个项目的典型结构把「怎么跑起来、怎么改、怎么不翻车」一条线讲透。适合有 Java 基础、学过一点 HTML 和 JDBC、但还没完整做过一个 Web 项目的同学也适合想拿这个项目当模板去改造成其他管理系统的人。2. 把项目跑起来之前环境、数据库与目录结构的三重对齐2.1 为什么 JDK、Tomcat、MySQL 的版本组合最容易让人翻车这类原生 JSPServlet 项目最常见的运行环境是 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.8Tomcat 的lib目录下有没有servlet-api.jarMySQL 能不能用命令行登录。这三步过了再谈导入项目。很多人一上来就双击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 分类名称 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品表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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建完表之后连接配置通常放在src/db.properties里用Properties类加载。注意 URL 里的useUnicodetruecharacterEncodingutf8不能省否则中文商品名插进去就是问号。MySQL 8.0 还要加serverTimezoneAsia/ShanghaiuseSSLfalse。密码如果用了caching_sha2_password插件老驱动可能连不上要么升级驱动要么改回mysql_native_password。2.3 目录结构怎么摆WEB-INF 下放什么、不放什么一个标准的原生 Java Web 项目目录大概长这样src下按包名分dao、service、servlet、bean、utilWebContent或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 ListGoods findAll() { ListGoods 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. 避坑与排查JSPServlet 项目里最容易翻车的 5 个地方4.1 中文乱码现象是页面问号原因是编码链没统一现象商品名在 JSP 页面显示正常但插入数据库后变成???或者表单提交后后台收到乱码。原因通常有三处JSP 页面pageEncoding没设 UTF-8Servlet 里没调request.setCharacterEncoding(UTF-8)数据库连接 URL 没加characterEncodingutf8。解决JSP 头部写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-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里加一个ListGoods 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。我习惯封装一个PageBeanT包含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 调 DAOJSP 只拿现成数据世界才清净。如果你正在改这个项目第一件事就是把 JSP 里的 Java 代码清干净哪怕多写几个 Servlet 也值得。希望帮到你。本文还有配套的精品资源点击获取