十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于JSP+Servlet+JavaBean的超市进销存系统源码拆解与实战

基于JSP+Servlet+JavaBean的超市进销存系统源码拆解与实战 简介一套完整的超市进销存系统源码采用 JavaWeb 经典三层架构 JSPServletJavaBean 开发面向高校计算机、数学、电子信息等专业学生适用于课程设计、期末大作业或毕业设计参考也适合想深入理解 JavaWeb 分层开发的初学者钻研。源码压缩包共 134 个文件约 6.94MB包括 14 个 Java 逻辑类、9 个 JSP 页面、16 个 JS 交互脚本、15 个 CSS 样式表、多张页面图片以及 6 个依赖 JAR 包java 目录负责业务与数据库操作jsp 与前端脚本构成展示层各类资源分工明确前后端组织完整。已有 114 人学习浏览说明该题目在同类课程项目中具有较高参考需求。解压后即可直接部署运行登录鉴权、商品类别管理、进货入库与销售出库、库存台账等业务模块均有可运行代码有助于对照学习 JavaBean 数据封装、Servlet 控制转发与数据库交互的实际写法也能在此基础上改造扩展成适合自己的课题功能。1. 超市进销存源码里最值得拆解的一段 JSPServletJavaBean拿到这套基于 JSPServletJavaBean 的超市进销存管理系统源码我没有急着把它当成可交付的软件而是先当作一个 Java Web 学习范本拆开看。它覆盖了货品出入库登记、库存查询、分类管理与登录权限这些最常见的进销存业务场景数据层由 DBOper 管 JDBC控制器交给 AdminController、HomeControllerJavaBean 承担商品、用户、分类、进出单据的实体映射ExcelUtil 负责盘点数据导入导出。对课程设计、期末大作业或毕设参考来说这类工程最大的优点是分层清楚、能运行、能改动。源码包根目录出现 AdminController.class、GoodsInfo.class 这类编译产物说明这是旧式 Eclipse Web 工程或 Tomcat 部署目录直接打包的完整资源。下面按数据层、控制器、视图层、Excel 再到部署排错把这套老牌 JSP 项目的可复用点都说一下。2. 从 JavaBean 到 DBOper进销存数据层设计2.1 GoodsInfo、UserInfo 等 JavaBean 对应哪几张表理解这个项目首先要明白实体类不是随便取的。资源里的 AdminInfo、UserInfo、GoodsInfo、CategoryInfo、ImportInfo、ExportInfo 正好对应五类核心业务数据管理员、操作员、商品、分类、进货单、出货单。实际建表时字段名一般用下划线风格JavaBean 里写成驼峰靠手工映射这套源码没有引入 ORM所以这个映射关系要自己看熟。JavaBean业务表核心字段参考GoodsInfogoods_infogoods_id, goods_name, category_id, purchase_price, sale_price, stockUserInfouser_infouser_id, user_name, password, role, create_timeAdminInfoadmin_infoadmin_id, admin_name, password, levelCategoryInfocategory_infocategory_id, category_nameImportInfoimport_infoimport_id, goods_id, quantity, in_price, import_timeExportInfoexport_infoexport_id, goods_id, quantity, out_price, export_time更推荐的写法是让每个 JavaBean 实现 Serializable因为用户对象和商品集合可能要放进 session 或作为请求属性传递。比如 GoodsInfo 的骨架import java.io.Serializable; public class GoodsInfo implements Serializable { private int goodsId; private String goodsName; private int categoryId; private double purchasePrice; private double salePrice; private int stock; public GoodsInfo() { } // 建议用 Integer、Double 包装类型替代基本类型便于判断“未赋值”与“为 0” public int getGoodsId() { return goodsId; } public void setGoodsId(int goodsId) { this.goodsId goodsId; } }参数说明goodsId 自增不参与新增 SQLpurchasePrice 和 salePrice 在 JDBC 里用 setBigDecimal 更精确如果实体里用 double导出 Excel 时容易出现 0.10.2 这种精度问题。这个实体类同时也是 JSP 页面里 EL 表达式读取数据的依据getter 方法名必须与页面里goodsName这样的属性严格对应。2.2 DBOper 把 JDBC 连接、查询、update 收敛到一处在没有 MyBatis 或 JPA 的课程设计里最常见的情况是 JDBC 代码散落在每个 Servlet 中改一个数据库密码要翻很多文件。源码里的 DBOper 类就是用来解决这个问题的。它至少提供三样东西获取连接、执行查询、执行更新。我一般会按下面的结构去做兼容重构public class DBOper { private static final String URL jdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingUTF-8; private static final String USER root; private static final String PASSWORD 123456; public Connection getConnection() throws Exception { Class.forName(com.mysql.jdbc.Driver); return DriverManager.getConnection(URL, USER, PASSWORD); } public ResultSet query(String sql, Object... params) throws Exception { Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } return ps.executeQuery(); } }参数说明URL 里的characterEncodingUTF-8保证中文商品名不乱码query 方法里用了可变参数Object... params调用方写dbo.query(select * from goods_info where category_id?, 3)即可。注意直接把 ResultSet 返回给调用方会把连接生命周期也带出去代码写不好容易漏关闭所以更稳的做法是在 DBOper 里再提供ListGoodsInfo queryGoods(String sql, Object... params)这样的泛型方法数据取完立刻关闭 Statement 和 Connection避免连接数被打满。如果能接受引入第三方库用 Apache Commons DBUtils 的 QueryRunner 也可以它内部已经处理了 ResultSet 到 JavaBean 的映射这套源码的 DBOper 完全可以替换成那套写法而 Servlet 调用层不用大改。2.3 进货和库存更新必须用事务超市进销存最关键的操作不是查库存而是改库存。录入一张进货单时通常要做两件事往 import_info 插入一条进货记录同时把 goods_info 里对应商品的数量增加。两步只要失败一步库存数据就是错的。常见做法是让 DBOper 提供带事务的方法把 Connection 作为参数传进去public void acceptGoods(int goodsId, int quantity, double inPrice) throws Exception { Connection conn null; try { conn dbo.getConnection(); conn.setAutoCommit(false); dbo.update(conn, insert into import_info(goods_id, quantity, in_price, import_time) values(?,?,?,now()), goodsId, quantity, inPrice); dbo.update(conn, update goods_info set stock stock ? where goods_id ?, quantity, goodsId); conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }逻辑说明先关闭自动提交两个更新都成功后 commit任何一个失败就 rollback。参数说明第一个 update 的 inPrice 对应进货单价第二个 update 的 quantity 是本次进货数量stock stock ?不是覆盖而是累加这样如果之前库里已有 10 件这次进 5 件结果就是 15 件。注意这种写法要求数据库表使用 InnoDB 引擎MyISAM 不支持事务回滚不会生效。调试时如果发现一单数据只写了一半先检查建表语句里有没有ENGINEInnoDB。3. Servlet 控制器AdminController 与 HomeController 的请求分发机制3.1 action 参数让一个 Servlet 联动多个页面这套源码里负责业务入口的是 AdminController 和 HomeController它们属于最经典的 HttpServlet 实现。很多老式 JSP 课程设计会为每个功能写一个 Servlet结果 web.xml 里全是 servlet-mapping维护成本很高。更常见的做法是让一个模块的 Servlet 接收 action 参数再分发到不同方法WebServlet(/admin) public class AdminController extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (addGoods.equals(action)) { addGoods(req, resp); } else if (updateStock.equals(action)) { updateStock(req, resp); } else if (deleteCategory.equals(action)) { deleteCategory(req, resp); } else { listGoods(req, resp); } } Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (list.equals(action)) { listGoods(req, resp); } else { req.getRequestDispatcher(/admin/index.jsp).forward(req, resp); } } }逻辑说明doPost 用来处理会改变数据的写操作比如新增商品、修改库存、删除分类doGet 专门处理查询和页面跳转。把 action 作为分发依据后URL 可以写成/admin?actionaddGoods在 JSP 表单里提交到同一个 Servlet只靠隐藏字段区分功能。参数说明action 的值最好是能和 Java 方法名对应的驼峰字符串不要用 1、2 这种数字编号否则后期加需求时看不懂这个数字是干什么的。如果工程不是注解方式需要在 web.xml 里给 AdminController 加servlet和servlet-mappingurl-pattern 写/admin效果完全一样。源代码包里的 AdminController.class 很可能就是带注解的版本导入环境下优先看注解不需要改 web.xml。3.2 转发、重定向和会话配合做一个登录链路登录是进销存系统的第一道门槛。这里要分清 request 和 session 的边界。用户输完账号密码Servlet 先查 user_info 表查到了就把登录用户放进 session然后重定向到主页查不到就把错误信息放进 request转发回登录页。代码如下UserInfo user dbo.findUser(userName, password); if (user ! null) { req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /home?actionindex); } else { req.setAttribute(loginError, 账号或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); }逻辑说明sendRedirect 会让浏览器重新发起一次请求地址栏会变化能避免刷新页面时重复提交登录表单forward 是服务器内部跳转地址栏不变适合带错误提示回显。如果登录成功却用 forward 直接跳到主页用户按 F5 刷新可能把登录请求再发一次。参数说明req.getContextPath()返回的是部署后的应用根路径假设工程上下文是supermarket实际跳转地址是/supermarket/home?actionindex。注意 session 里存 loginUser 后JSP 页面能用${sessionScope.loginUser.userName}直接显示当前操作员姓名。如果项目里的 JSP 个人信息展示页面取不到登录人多半是过滤器或 Servlet 没有把 loginUser 放进 session而不是页面写错。3.3 用 Filter 统一拦截未登录请求如果每个 Servlet 都判断 session 里有没有 loginUser代码会重复且容易漏。老项目里通常会配一个 LoginFilter 做登录状态校验。下面是过滤器的最简实现WebFilter(/admin/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; Object loginUser req.getSession().getAttribute(loginUser); if (loginUser null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }逻辑说明这个过滤器只拦/admin/*下的后台管理页面登录页和登录 Servlet 不拦。没登录时直接重定向到 login.jsp登录过就继续执行后面的控制器。参数说明WebFilter 的 value 路径以/开头/* 表示通配所有子路径如果你希望 home 模块也受控再加WebFilter({/admin/*, /home/*})。授权粒度更细一点可以在 UserInfo 里加 role 字段过滤器里判断/admin/delete.equals(req.getRequestURI())时要求管理员角色普通操作员只能操作库存和查询。4. 视图层 JSP列表渲染、会话信息与 JSON 输出4.1 用 JSTL 替代散落的 scriptlet老式 JSP 项目最容易被诟病的就是页面上到处是% %Java 代码。源码包里既然有 JSP一定有商品列表、用户列表这类页面。我会建议把它改成 JSTL 写法既能缩代码量也能避免把数据库连接写进页面% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body table tr th商品名/th th售价/th th库存/th /tr c:forEach items${goodsList} varg tr td${g.goodsName}/td td${g.salePrice}/td td${g.stock}/td /tr /c:forEach /table /body /html逻辑说明goodsList 是 Servlet 里req.setAttribute(goodsList, list)放进来的集合JSTL 的 c:forEach 循环它var 定义的 g 就是集合里的 GoodsInfo 对象。参数说明EL 表达式${g.goodsName}实际调用的是 GoodsInfo 的 getGoodsName() 方法所以要保证 JavaBean 的 getter 存在。这套源码如果用 scriptlet页面里很可能会看到ListGoodsInfo list (ListGoodsInfo)request.getAttribute(goodsList)这样的语句它的效果与c:forEach完全一样但可读性差很多。改造时不需要动 Servlet只要把 JSP 里的循环替换掉即可。4.2 登录用户信息展示与会话超时商品管理页面通常需要显示当前操作员是谁并决定哪些按钮能看。这里的要点是不要从 request 里取用户因为用户信息来自 session。改造完个人信息展示区域用下面的方式最直接c:if test${not empty sessionScope.loginUser} 当前操作员${sessionScope.loginUser.userName} /c:if逻辑说明sessionScope 是 JSP 内置的会话作用域loginUser 是登录成功时写进 session 的对象。not empty同时判断对象是否为 null 以及是否为空白字符串避免出现“null”字样。如果用户已经掉线或会话超时这里会什么都不显示配合过滤器可以再把请求转到登录页。页面里的权限判断可以在 UserInfo 里加 role 字段然后用c:if test${sessionScope.loginUser.role admin}控制删除按钮显隐。这样“删除”的动作虽然在 Servlet 层还要二次校验但视图层已经不容易产生误操作。4.3 用 JSON 接口给页面回商品价格进销存的销售页面经常要做“选完商品自动带出单价”的效果传统做法是提交表单刷新页面。更好的方案是让某个 JSP 或 Servlet 输出 JSON 数据前端用 Ajax 直接取。这个输出过程也可以放在 JSP 中% response.setContentType(application/json;charsetUTF-8); ListGoodsInfo list (ListGoodsInfo) request.getAttribute(goodsList); JSONArray arr new JSONArray(); for (GoodsInfo g : list) { arr.put(new JSONObject() .put(goodsId, g.getGoodsId()) .put(goodsName, g.getGoodsName()) .put(salePrice, g.getSalePrice())); } out.print(arr.toString()); %逻辑说明先把 response 的内容类型设为 JSON浏览器收到后不会按 HTML 解析。JSONArray 和 JSONObject 来自 json.jar如果用的是 Jackson 或 Gson语法会略有差异。参数说明arr.put()是往数组里追加元素JSONObject 里的 key 必须和前端 JS 读取的字段名一致比如data[0].goodsName。这里要特别注意编码。只写setContentType(application/json)还是会出现中文乱码必须加;charsetUTF-8并且保证 JSP 页面本身保存为 UTF-8 编码。5. ExcelUtil 读写 Excel导入导出和数据迁移5.1 为什么进销存离不开 Excel很多小型超市的商品资料本身就是一张大 Excel 表包括商品编号、名称、进价、售价、库存。把这套系统上线时最省力的方式不是一条条往页面里录入而是直接读取 Excel 批量导入。反之老板也可能要求每个月导出一份库存表存档或更新给财务。源码里的 ExcelUtil 承担的就是这两个方向把 Excel 行转成 JavaBean把数据库记录写回 Excel。这个工具类不建议自己用 IO 流解析 xlsx因为 xlsx 是 ZIP 压缩格式内部是 XML 结构。常见做法是直接用 Apache POI 的 WorkbookFactory让它根据文件后缀自动判断是 HSSFWorkbookxls还是 XSSFWorkbookxlsx代码只面向 Workbook 接口。5.2 从 Excel 批量导入商品导入商品时要注意几个坑第一首行通常有标题第二录入单元格有可能是空值第三售价和数量单元格在 Excel 中可能是数字、文本或公式。下面的写法可以规避掉大部分问题Workbook workbook WorkbookFactory.create(inputStream); Sheet sheet workbook.getSheetAt(0); DataFormatter formatter new DataFormatter(); for (int i 1; i sheet.getLastRowNum(); i) { Row row sheet.getRow(i); if (row null) { continue; } String name formatter.formatCellValue(row.getCell(0)); String priceStr formatter.formatCellValue(row.getCell(1)); String stockStr formatter.formatCellValue(row.getCell(2)); if (name null || name.trim().isEmpty()) { continue; } double price Double.parseDouble(priceStr); int stock (int) Double.parseDouble(stockStr); goodsDao.insert(name, price, stock); } workbook.close();逻辑说明从第二行开始读跳过列标题DataFormatter 能把各种类型的单元格统一转成字符串比直接调 getNumericCellValue 安全。参数说明sheet.getLastRowNum()返回最后一行的索引下标所以循环条件要写row.getCell(0)、row.getCell(1)、row.getCell(2) 分别对应商品名、单价、库存三列。如果 Excel 里库存列是“120”这样的纯数字但格式是文本formatter.formatCellValue 也能读成字符串再用Double.parseDouble转数值。这里用 double 接收价格是为了兼容“12.50”这种显示形式。如果想保留两位小数写入数据库前用BigDecimal.valueOf(price).setScale(2, RoundingMode.HALF_UP)处理一下。5.3 把库存导出成 Excel 并触发下载导出时Servlet 不仅要生成 Workbook还要通过响应头告诉浏览器这是一个附件而不是 HTML 页面。常见写法如下resp.setContentType(application/vnd.ms-excel;charsetUTF-8); resp.setHeader(Content-Disposition, attachment;filenamestock_ System.currentTimeMillis() .xls); Workbook wb new HSSFWorkbook(); Sheet sheet wb.createSheet(库存); Row head sheet.createRow(0); head.createCell(0).setCellValue(商品名); head.createCell(1).setCellValue(售价); head.createCell(2).setCellValue(库存); int rowIndex 1; for (GoodsInfo g : goodsList) { Row row sheet.createRow(rowIndex); row.createCell(0).setCellValue(g.getGoodsName()); row.createCell(1).setCellValue(g.getSalePrice()); row.createCell(2).setCellValue(g.getStock()); } wb.write(resp.getOutputStream()); wb.close();逻辑说明Content-Disposition 设为 attachment 后浏览器会弹出下载而不是直接打开。filename 后面加上时间戳避免同名文件直接覆盖。参数说明HSSFWorkbook 对应 Excel 97-2003 的 xls 格式最大支持 65536 行超市用足够如果你要处理十几万行的导出就换成 XSSFWorkbook但要引入 poi-ooxml 依赖。实际项目中导出操作建议加一个权限校验只有管理员能导出全量库存普通操作员只能导出自己有权限查看的分类否则容易造成数据外发。6. 从源码包到 Tomcat 可运行class 文件、JSP 编译与排错技巧6.1 导入 IDE 时首选 Web 工程而不是 Java 工程从压缩包解压后先看目录结构。如果里面存在.classpath、.project它是 Eclipse 工程直接 File - Import - Existing Projects into Workspace 导入即可。如果没有这两个文件就不要直接打开源码先在 IDEA 或 Eclipse 里新建 Dynamic Web Project再把 src、WebContent 目录拷进去并把 lib 下依赖加到 Web App Libraries。否则启动后会出现找不到 DBOper 或 ExcelUtil 的问题。导入完成后检查 WAR 部署路径。IDE 里配置 Tomcat 时Application Context 建议设成/supermarket这样浏览器访问地址就是http://localhost:8080/supermarket/login.jsp。如果你的工程里已经有 web.xml宿主路径要以web-app里的 metadata-complete 配置为准。6.2 JSP 编译后的 class 文件存在 Tomcat 哪里JSP 在第一次被访问时会翻译成一个 Java 文件再编译成 Servlet 的 class 文件。很多文章只讲理论这个地方看实际例子最直观。在 Tomcat 7/8/9 中这些临时文件默认放在TOMCAT_HOME/work/Catalina/localhost/${应用名}/org/apache/jsp/比如登录页 login.jsp 会被编译成login_jsp.java和login_jsp.class。如果页面报 500想确认 JSP 里的 Java 片段有没有被正确编译去这个目录下打开对应的_jsp.java文件看。参数说明应用名对应当前 Tomcat 的 webapps 目录下文件夹名如果你在 IDE 里部署可能是/home/.../target/tomcat下的临时目录直接搜索org/apache/jsp就能定位。JSP 修改后Tomcat 开发模式下会自动检测文件改动并重新编译。如果开启了 security manager 或把 work 目录设成了只读就会出现改了 JSP 不生效的情况。这时删除 work 目录下所有内容再重启是最快的清理方式。6.3 用 javap 和 work 目录反推编译产物问题如果源码包里的 .class 文件版本和当前 Tomcat 不兼容典型报错是UnsupportedClassVersionError。你可以用 javap 查看主版本号确认它是不是当前 JDK 编译器能识别的字节码。命令如下javap -classpath WEB-INF/classes -verbose com.shop.AdminController | grep major输出版本号大于当前 JDK 支持范围时不要直接拷 class 文件到服务器而是重新编译源码。此时在 IDEA 里创建基于 Maven 的 Servlet 项目反而是最省事的新建 maven-war-webapp 骨架把 JavaBean、Servlet、JSP 按目录拷进去再补上 servlet-api 和 poi 依赖Tomcat 配置成 war exploded。做完这些后再访问一次 JSP去 work 目录查看对应的_jsp.java有没有重新生成。如果文件时间戳是刚更新的说明页面编译链路正常如果还是旧时间戳就去检查 IDE 的构建输出目录和 Tomcat 部署目录是否指向了同一个 classes 文件夹。本文还有配套的精品资源点击获取
返回列表