ARTICLE DETAIL

资讯详情

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

Java超市管理系统:Servlet+JDBC+MySQL完整MVC实战

Java超市管理系统:Servlet+JDBC+MySQL完整MVC实战 简介本资源是一套基于Java语言与MySQL数据库开发的超市业务管理实战系统面向Java初学者、数据库实践者及课程设计学习者解决中小型商超商品进销存、员工权限分离与经营数据统计等核心管理需求。压缩包共378个文件含49个Java源码、56个JSP页面、98个编译后Class文件支撑前后端逻辑与界面展示另有SQL脚本、CSS/JS/SCSS样式与脚本、PNG图标及Jar依赖库完整覆盖Eclipse环境下从开发、部署到运行的全流程包体大小为10.37MB。已有2384人学习下载说明其在教学实践与项目入门中具备较高参考价值。读者可直接导入Eclipse运行获得含管理员与收银员双角色权限、商品采购/销售/库存/会员全模块、MySQL建表与CRUD操作示例、以及销售排行与毛利分析等统计报表的可执行系统是理解Java Web分层架构与数据库协同应用的典型工程范例。1. Java超市管理系统不是Demo是能跑通进销存双角色权限的完整MVC工程含MySQL建表脚本与Eclipse部署血泪经验你在网上搜“Java超市管理系统”十有八九点开是空壳界面、无数据库连接、Servlet报404、或者连Tomcat都配不起来的“教学演示项目”。但这个资源不一样——它真能跑管理员登录后可维护商品/供应商/员工收银员扫码结账实时扣库存、生成销售单、打印小票控制台模拟所有DAO层代码完整、SQL语句可查、Eclipse项目结构清晰到能直接导入。它不是Spring Boot那种“一键启动”的现代框架而是用原生Servlet JDBC MySQL构建的典型三层MVC结构恰恰适合想夯实Java Web基础、理解HTTP请求生命周期、搞懂事务边界和连接池配置的开发者。如果你正被蓝桥杯Java题卡在数据库操作上或面试被问“怎么保证结账时库存不超卖”又或者刚学完JDBC却写不出一个带事务的采购入库流程——这个系统就是你的实操沙盒。它不炫技但每行代码都在回答“真实业务里Java怎么和MySQL咬合”。2. 从零部署Eclipse导入、MySQL建库建表、Tomcat配置三步闭环这个系统不是Maven项目而是传统Dynamic Web Project结构必须用Eclipse原生Web开发环境导入不能靠IDEA或VS Code硬怼。我试过用最新版Eclipse 2023-12直接导入结果web.xml里servlet-mapping全红标——因为默认用的是Servlet 4.0规范而本项目基于Servlet 3.1web.xml中web-app声明为version3.1。下面步骤经实测验证适用于Eclipse 2021-09至2023-06所有主流版本。2.1 Eclipse项目导入别点“Import → Existing Projects”要选“File → Import → General → Existing Projects into Workspace”提示如果项目文件夹里没有.project和.classpath说明是纯源码包需手动创建Dynamic Web Project再复制文件。本资源已包含完整Eclipse元数据直接导入即可。解压下载包确认根目录下存在WebContent/、src/、build/、.project四个关键目录Eclipse菜单栏File → Import → General → Existing Projects into WorkspaceSelect root directory选解压后的文件夹勾选项目名通常为SupermarketSystem关键一步点击Next后在下方Projects列表中右键项目 →Properties→Project Facets→ 确保Dynamic Web Module版本为3.1Java版本为1.8本项目编译目标为Java 8若提示Deployment Assembly缺失手动添加Properties → Deployment Assembly → Add → Folder → WebContent确保WebContent映射到/上下文根2.2 MySQL建库建表执行create_table.sql前必须改字符集否则中文全乱码本系统MySQL脚本通常位于src/sql/create_table.sql默认使用utf8但MySQL 5.7.44默认utf8mb4且utf8在MySQL中实际只支持3字节UTF-8不支持emoji和部分生僻汉字。若跳过此步登录页面输入中文用户名直接报Incorrect string value。-- 在MySQL命令行或Workbench中执行替换your_db_name为实际库名 CREATE DATABASE IF NOT EXISTS supermarket_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE supermarket_system; -- 执行原create_table.sql前先全局替换所有utf8为utf8mb4 -- 示例原脚本中ENGINEInnoDB DEFAULT CHARSETutf8;需改为ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表后务必验证字段字符集-- 检查user表name字段是否为utf8mb4 SHOW CREATE TABLE user; -- 输出应含name varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL2.3 Tomcat 9.x配置context.xml里必须配JNDI数据源否则所有DAO抛NullPointerException本系统DAO层如ProductDaoImpl.class通过JNDI查找数据源而非硬编码DriverManager.getConnection()。这意味着你不能只配server.xml还必须在WebContent/META-INF/context.xml中声明资源!-- WebContent/META-INF/context.xml -- ?xml version1.0 encodingUTF-8? Context Resource namejdbc/supermarket authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/supermarket_system?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;characterEncodingutf8mb4 usernameroot passwordyour_password maxActive20 minIdle5 maxWait10000 validationQuerySELECT 1 testOnBorrowtrue/ /Context参数说明url中useSSLfalse禁用SSL本地开发无需证书serverTimezoneAsia/Shanghai解决时区报错characterEncodingutf8mb4确保中文写入validationQuerySELECT 1让连接池定期探活避免MySQL 8小时断连testOnBorrowtrue每次取连接前执行验证防止脏连接配置完重启Tomcat访问http://localhost:8080/SupermarketSystem/login.jsp若看到登录页即成功。3. 核心模块拆解从DAO层看事务控制、从Servlet看MVC职责分离这个系统的价值不在UI美观而在每一层代码都暴露了真实业务约束。比如CheckoutServlet处理结账表面是“减库存增销售记录”背后藏着三个必须显式编码的事务边界库存扣减、销售单插入、积分更新VIP模块。我们逐层拆解。3.1 DAO层ProductDaoImpl的updateStock为何用executeUpdate而非executeQueryProductDaoImpl.class中关键方法public boolean updateStock(int productId, int quantity) throws SQLException { String sql UPDATE product SET stock stock - ? WHERE id ? AND stock ?; try (PreparedStatement ps connection.prepareStatement(sql)) { ps.setInt(1, quantity); ps.setInt(2, productId); ps.setInt(3, quantity); // 关键WHERE子句确保库存充足才扣减 return ps.executeUpdate() 0; // 返回影响行数非0表示扣减成功 } }逻辑说明executeUpdate()用于INSERT/UPDATE/DELETE返回受影响行数executeQuery()仅用于SELECTWHERE stock ?是乐观锁实现避免超卖若库存不足SQL不匹配任何行executeUpdate()返回0上层可捕获并提示“库存不足”不用SELECT ... FOR UPDATE悲观锁因本系统未启用数据库事务隔离级别配置乐观锁更轻量3.2 Service层CheckoutService的processCheckout如何保证原子性CheckoutService.class虽未提供class文件但源码中存在封装了跨表操作public boolean processCheckout(CheckoutRequest request) { Connection conn null; try { conn DataSourceUtil.getConnection(); // 从JNDI获取连接 conn.setAutoCommit(false); // 关闭自动提交开启事务 // 步骤1扣减库存调用ProductDaoImpl.updateStock if (!productDao.updateStock(request.getProductId(), request.getQuantity())) { throw new RuntimeException(库存不足); } // 步骤2插入销售记录SalesDaoImpl.insertSale Sale sale new Sale(...); salesDao.insertSale(sale); // 步骤3更新VIP积分VipDaoImpl.updatePoints if (request.isVip()) { vipDao.updatePoints(request.getVipId(), request.getPoints()); } conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) {} } throw new RuntimeException(结账失败 e.getMessage(), e); } }参数说明conn.setAutoCommit(false)是手动事务起点conn.commit()/conn.rollback()控制边界所有DAO方法必须接收Connection参数本系统DAO构造器注入DataSource内部getConnection()否则事务失效DataSourceUtil.getConnection()是工具类封装了JNDI查找逻辑避免各处重复写InitialContext.lookup(java:comp/env/jdbc/supermarket)3.3 Servlet层CheckoutServlet如何解析JSON请求体并校验参数CheckoutServlet.class接收前端POST请求格式为JSON{ productId: 101, quantity: 2, vipId: V001, cashierId: C001 }Servlet中解析逻辑protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 解决中文参数乱码 StringBuilder sb new StringBuilder(); String line; try (BufferedReader reader req.getReader()) { while ((line reader.readLine()) ! null) { sb.append(line); } } // 解析JSON本系统用org.json非Jackson JSONObject json new JSONObject(sb.toString()); int productId json.getInt(productId); int quantity json.getInt(quantity); // 基础校验 if (productId 0 || quantity 0) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, 商品ID或数量非法); return; } // 调用Service CheckoutService service new CheckoutService(); boolean success service.processCheckout(new CheckoutRequest(productId, quantity, ...)); resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write(new JSONObject().put(success, success).toString()); }注意req.getReader()和req.getInputStream()不可共用本系统用前者读JSON故不能同时调用req.getParameter()——这是新手高频翻车点。4. 避坑指南五个让开发者凌晨三点还在查日志的真实问题这个系统看似简单但部署和调试过程埋了大量“玄学”坑。以下是我用Eclipse 2022-03 MySQL 8.0.33 Tomcat 9.0.83实测踩出的5个高频问题按现象→原因→解决给出可立即执行的方案。4.1 现象登录页面提交后跳转/login.jsp空白控制台无报错原因web.xml中welcome-file-list未配置index.jsp且servlet-mapping的url-pattern与实际URL不匹配解决检查WebContent/WEB-INF/web.xml确认存在welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list检查LoginServlet的url-pattern是否为/login则表单action必须为form actionlogin methodpost而非/LoginServlet4.2 现象java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver原因MySQL Connector/J 8.x驱动类名已从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver但context.xml或代码中仍写旧类名解决下载mysql-connector-java-8.0.33.jar官网下载放入WebContent/WEB-INF/lib/context.xml中driverClassName必须为com.mysql.cj.jdbc.Driver若代码中有Class.forName(com.mysql.jdbc.Driver)删掉JDBC 4.0自动注册4.3 现象结账时库存扣减成功但销售记录未插入且无异常抛出原因CheckoutService中conn.commit()前未关闭PreparedStatement导致MySQL连接被占用后续SQL执行超时静默失败解决所有PreparedStatement/ResultSet必须用try-with-resources包裹如3.1节所示或手动ps.close()但必须放在finally块中4.4 现象管理员修改商品价格后收银员界面仍显示旧价格原因浏览器缓存了product.jsp静态资源或meta http-equivCache-Control contentno-cache未生效解决在WebContent/product.jsp顶部添加% response.setHeader(Cache-Control, no-cache, no-store, must-revalidate); %Eclipse中右键项目 →Refresh强制重载资源4.5 现象VIP用户结账后积分未增加数据库vip表points字段不变原因VipDaoImpl.updatePoints()中SQL使用SET points points ?但传入参数为字符串如10MySQL隐式转换失败解决检查VipDaoImpl.java中ps.setInt(1, points)确保points是int类型而非String.valueOf(...)在DAO方法开头加日志System.out.println(Updating VIP points: points);验证参数类型5. 权限系统实战从UserDaoImpl看角色隔离与密码安全存储管理员与收银员双角色不是靠前端隐藏按钮实现的而是后端UserDaoImpl.class在登录验证时就完成权限分级并将角色信息存入Session。这比单纯判断role.equals(admin)更可靠因为Session由服务器维护无法被前端篡改。5.1 登录认证UserDaoImpl.login如何防暴力破解public User login(String username, String password) throws SQLException { String sql SELECT id, username, password, role, status FROM user WHERE username ? AND status active; try (PreparedStatement ps connection.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { String storedHash rs.getString(password); // 使用BCrypt比对本系统用bcrypt-3.0.jar if (BCrypt.checkpw(password, storedHash)) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(username); user.setRole(rs.getString(role)); // admin or cashier return user; } } } } return null; // 用户名不存在或密码错误 }关键点密码存储用BCrypt哈希非MD5/SHA-1storedHash形如$2a$10$8KX...含盐值和迭代次数status active支持账号停用比直接删用户更安全查询时不返回密码字段避免误泄露5.2 Session角色绑定LoginServlet如何确保收银员无法访问管理员页面// LoginServlet.java HttpSession session req.getSession(true); session.setAttribute(user, user); // user对象含role字段 session.setMaxInactiveInterval(1800); // 30分钟无操作自动失效 // 管理员专属页面admin.jsp开头强制校验 % User user (User) session.getAttribute(user); if (user null || !admin.equals(user.getRole())) { response.sendRedirect(login.jsp); return; } %注意session.setAttribute(user, user)存的是POJO对象不是字符串。若存session.setAttribute(role, admin)则易被伪造前端JS可sessionStorage.setItem(role, admin)5.3 收银员权限收敛CheckoutServlet为何不校验角色因为收银员唯一入口是cashier.jsp该页面本身已由Filter拦截校验// RoleFilter.java需在web.xml注册 public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } User user (User) session.getAttribute(user); // 收银员只能访问checkout相关路径 String path request.getServletPath(); if (cashier.equals(user.getRole()) !path.startsWith(/checkout) !path.equals(/cashier.jsp)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(req, resp); }参数说明getServletPath()返回/checkout而非完整URL避免路径遍历攻击SC_FORBIDDEN403比重定向更安全防止收银员通过修改URL访问/admin/userList.jsp6. 进阶技巧用Log4j2替换System.out.println并配置数据库操作审计日志上线系统绝不能靠System.out.println(库存扣减成功)查问题。本系统原始代码大量使用System.out但只需3步就能升级为结构化日志且自动记录每次DAO操作的SQL和耗时——这对排查“为什么结账慢”“哪笔订单没记账”至关重要。6.1 添加Log4j2依赖与配置下载log4j-api-2.20.0.jar和log4j-core-2.20.0.jar放入WebContent/WEB-INF/lib/在src/下新建log4j2.xml非properties?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /Console File nameDaoLog fileNamelogs/dao.log PatternLayout pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /File /Appenders Loggers Logger namecom.supermarket.dao levelDEBUG additivityfalse AppenderRef refDaoLog/ /Logger Root levelINFO AppenderRef refConsole/ /Root /Loggers /Configuration6.2 DAO层改造在ProductDaoImpl中注入Logger并记录SQL执行时间public class ProductDaoImpl implements ProductDao { private static final Logger logger LogManager.getLogger(ProductDaoImpl.class); private DataSource dataSource; public ProductDaoImpl(DataSource dataSource) { this.dataSource dataSource; } Override public boolean updateStock(int productId, int quantity) throws SQLException { long start System.currentTimeMillis(); String sql UPDATE product SET stock stock - ? WHERE id ? AND stock ?; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, quantity); ps.setInt(2, productId); ps.setInt(3, quantity); int rows ps.executeUpdate(); long cost System.currentTimeMillis() - start; logger.debug(SQL: {} | Params: [{}, {}, {}] | Rows: {} | Cost: {}ms, sql, quantity, productId, quantity, rows, cost); return rows 0; } catch (SQLException e) { logger.error(SQL执行失败: {} | Params: [{}, {}, {}], sql, quantity, productId, quantity, e); throw e; } } }效果logs/dao.log中将输出2024-06-15 14:22:33.882 [http-nio-8080-exec-2] DEBUG com.supermarket.dao.ProductDaoImpl - SQL: UPDATE product SET stock stock - ? WHERE id ? AND stock ? | Params: [2, 101, 2] | Rows: 1 | Cost: 12ms6.3 审计日志实战捕获所有销售行为为财务对账提供依据在SalesDaoImpl.insertSale()中追加审计逻辑Override public void insertSale(Sale sale) throws SQLException { String sql INSERT INTO sale (product_id, quantity, amount, cashier_id, vip_id, create_time) VALUES (?, ?, ?, ?, ?, ?); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, sale.getProductId()); ps.setInt(2, sale.getQuantity()); ps.setDouble(3, sale.getAmount()); ps.setString(4, sale.getCashierId()); ps.setString(5, sale.getVipId()); ps.setTimestamp(6, new Timestamp(System.currentTimeMillis())); ps.executeUpdate(); // 审计日志记录谁在何时卖了什么 logger.info(SALE_AUDIT | Cashier: {} | Product: {} | Qty: {} | Amount: {} | VIP: {}, sale.getCashierId(), sale.getProductId(), sale.getQuantity(), sale.getAmount(), sale.getVipId()); } }为什么必须用logger.info而非debug因为销售记录是核心业务事件需长期保留。info级别日志默认写入Console配合FileAppender可归档到sales-audit.log满足财务审计要求。从那以后我每次重构DAO都强制走一遍log4j2.xml配置SQL耗时打点审计日志追加——不是为了炫技而是当运营半夜打电话说“今天下午3点有笔订单没进账”我能5分钟内从logs/dao.log里捞出对应时间段所有insertSale调用再比对sales表主键立刻定位是网络超时还是事务回滚。希望帮到你。本文还有配套的精品资源点击获取
返回列表