1. 先搞清楚这个项目到底解决什么实际问题
超市管理系统是Java Web开发中最经典的实战项目之一,但很多人拿到源码后最大的困惑是:这到底是个演示Demo还是能真正跑起来的完整系统?从实际经验看,这个项目的核心价值在于让你完整走一遍企业级Web应用开发的全流程——从数据库设计、分层架构、业务逻辑实现到最终部署上线。
它解决的最实际问题有三个:
- 给Java学习者一个可落地的综合练习:很多教程只讲零散知识点,这个项目把Servlet/JSP、JDBC、MySQL、MVC模式串成完整链路
- 为毕业设计提供可直接复用的基础框架:数据库表设计、登录权限、CRUD操作都是现成的,你只需要根据具体需求调整业务逻辑
- 理解前后端交互的真实场景:虽然标题提到"前后端分离",但传统JSP+Servlet项目更能让你看清数据从浏览器到数据库的完整流转路径
我一般建议先不看具体代码,而是把项目跑起来,体验完整功能流程。能跑通再研究代码结构,比直接读源码效率高得多。
2. 环境准备阶段最容易踩的坑
这个项目最关键的其实不是代码多复杂,而是环境配置。我遇到过太多"源码没问题但就是跑不起来"的情况,基本都是环境问题。
2.1 版本匹配是首要问题
先确认你的环境版本组合,不要随意混用新老版本:
| 组件 | 推荐版本 | 替代方案 | 必须避开的版本 |
|---|---|---|---|
| JDK | 1.8 | JDK 11 | JDK 17+(兼容性问题多) |
| Tomcat | 8.5.x | Tomcat 9.0.x | Tomcat 10.x(Jakarta EE不兼容) |
| MySQL | 5.7 | MySQL 8.0 | MySQL 8.0.26之前的部分版本 |
| 数据库驱动 | mysql-connector-java-8.0.x | 5.1.x | 新版本驱动配老MySQL |
验证环境是否就绪的几个关键命令:
# 检查Java版本 java -version # 检查Tomcat启动(端口号根据实际修改) curl http://localhost:8080 # 检查MySQL连接 mysql -u root -p -e "SELECT VERSION();"2.2 数据库初始化要一步步来
很多人直接导入整个SQL文件,出错后找不到问题根源。我更建议分步执行:
-- 1. 先创建数据库 CREATE DATABASE supermarket_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 逐表创建,避免外键依赖问题 -- 先创建没有外键依赖的表:user, goods_type, supplier -- 再创建有外键的表:goods, sale, sale_item, stock_in -- 3. 最后插入初始数据 INSERT INTO user (username, password, real_name, role) VALUES ('admin', MD5('admin123'), '系统管理员', 0);注意:如果使用MySQL 8.0,确保连接字符串包含
serverTimezone=Asia/Shanghai,否则时区问题会导致时间字段异常。
3. 项目结构解读:重点看这几个目录
拿到源码后不要急着找main方法,Java Web项目的入口在web.xml和Servlet配置。先理清目录结构:
SupermarketMS/ ├── src/ │ ├── com.supermarket.dao/ # 数据访问层 - 直接操作数据库 │ ├── com.supermarket.service/ # 业务逻辑层 - 处理复杂业务规则 │ ├── com.supermarket.servlet/ # 控制层 - 接收HTTP请求 │ ├── com.supermarket.model/ # 实体类 - 与数据库表对应 │ └── com.supermarket.util/ # 工具类 - 数据库连接、加密等 ├── WebContent/ │ ├── WEB-INF/ │ │ ├── lib/ # 第三方JAR包存放位置 │ │ └── web.xml # 项目配置文件 │ ├── css/ # 样式文件 │ ├── js/ # JavaScript文件 │ ├── *.jsp # 前端页面 │ └── login.jsp # 登录页面入口最容易出问题的地方:
WEB-INF/lib下的JAR包版本不匹配web.xml中Servlet配置路径与实际访问路径不一致- JSP页面中的相对路径引用错误
4. 从登录功能理解MVC数据流转
登录流程是理解整个项目架构的最佳切入点,我们跟踪一次完整的请求处理:
4.1 前端表单提交到Servlet
<!-- login.jsp --> <form action="login" method="post"> <input type="text" name="username"> <input type="password" name="password"> <button type="submit">登录</button> </form>这里action="login"对应web.xml中配置的Servlet映射:
<!-- web.xml配置方式 --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.supermarket.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> <!-- 或者使用注解配置(更现代的方式) --> @WebServlet("/login") public class LoginServlet extends HttpServlet { // Servlet代码 }4.2 Servlet如何处理业务逻辑
protected void doPost(HttpServletRequest request, HttpServletResponse response) { // 1. 获取参数 String username = request.getParameter("username"); String password = request.getParameter("password"); // 2. 调用Service层处理业务 UserService userService = new UserService(); User user = userService.login(username, password); // 3. 根据结果跳转页面 if (user != null) { HttpSession session = request.getSession(); session.setAttribute("currentUser", user); // 用户信息存入Session response.sendRedirect("main.jsp"); // 重定向到主页 } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); // 转发回登录页 } }关键理解点:
sendRedirect()是重定向(浏览器地址栏变化),forward()是转发(地址栏不变)- Session用于保持用户登录状态,关闭浏览器后失效
- Request属性用于在转发时传递数据给JSP页面
4.3 Service层的业务处理
public User login(String username, String rawPassword) { // 1. 密码加密 String encryptedPassword = MD5Util.encode(rawPassword); // 2. 调用DAO查询数据库 UserDao userDao = new UserDao(); User user = userDao.findByUsernameAndPassword(username, encryptedPassword); // 3. 可在此添加更多业务逻辑:登录日志、失败次数限制等 if (user != null) { logLoginSuccess(user); // 记录成功日志 } else { logLoginFailure(username); // 记录失败尝试 } return user; }4.4 DAO层的数据库操作
public User findByUsernameAndPassword(String username, String password) { Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); // 获取数据库连接 String sql = "SELECT * FROM user WHERE username = ? AND password = ?"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, username); // 防止SQL注入 pstmt.setString(2, password); rs = pstmt.executeQuery(); if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); // ... 设置其他属性 return user; } } finally { // 释放资源 DBUtil.closeConnection(conn); } return null; }5. 商品管理模块的CRUD实战
商品管理是系统的核心功能,完整展示了增删改查的实现方式。
5.1 查询商品列表的完整链路
前端页面发起请求:
<!-- 页面加载时自动查询 --> <script> window.onload = function() { loadGoodsList(); }; function loadGoodsList() { // 实际项目中常用Ajax,这里简单演示页面跳转 window.location.href = 'goods?action=list'; } </script>Servlet控制器分发:
protected void doGet(HttpServletRequest request, HttpServletResponse response) { String action = request.getParameter("action"); if ("list".equals(action)) { listGoods(request, response); } else if ("add".equals(action)) { // 跳转到添加页面 } else if ("edit".equals(action)) { // 跳转到编辑页面 } else if ("delete".equals(action)) { // 执行删除操作 } } private void listGoods(HttpServletRequest request, HttpServletResponse response) { GoodsService goodsService = new GoodsService(); List<Goods> goodsList = goodsService.getAllGoods(); request.setAttribute("goodsList", goodsList); request.getRequestDispatcher("goods_list.jsp").forward(request, response); }Service层处理复杂查询:
public List<Goods> getAllGoods() { // 这里可以添加缓存逻辑、权限校验等 return goodsDao.findAllWithDetail(); // 查询商品及关联信息 }DAO层复杂SQL查询:
public List<Goods> findAllWithDetail() { String sql = "SELECT g.*, gt.name as type_name, s.name as supplier_name " + "FROM goods g " + "LEFT JOIN goods_type gt ON g.type_id = gt.id " + "LEFT JOIN supplier s ON g.supplier_id = s.id " + "WHERE g.status = 1 " + "ORDER BY g.id DESC"; // 执行查询并映射结果到Goods对象 // 需要处理结果集,设置typeName和supplierName等关联字段 }5.2 添加商品的完整流程
前端表单验证:
<form action="goods?action=save" method="post" onsubmit="return validateForm()"> <input type="text" name="code" placeholder="商品编码" required> <input type="text" name="name" placeholder="商品名称" required> <select name="typeId"> <option value="">选择分类</option> <!-- 从数据库动态加载分类选项 --> </select> <button type="submit">保存</button> </form> <script> function validateForm() { // 前端验证:非空检查、格式验证等 var code = document.querySelector('input[name="code"]').value; if (code.length < 3) { alert('商品编码至少3位'); return false; } return true; } </script>后端数据验证与保存:
private void saveGoods(HttpServletRequest request, HttpServletResponse response) { // 1. 获取参数 String code = request.getParameter("code"); String name = request.getParameter("name"); String typeId = request.getParameter("typeId"); // 2. 数据验证 if (StringUtils.isEmpty(code) || StringUtils.isEmpty(name)) { request.setAttribute("errorMsg", "商品编码和名称不能为空"); request.getRequestDispatcher("goods_form.jsp").forward(request, response); return; } // 3. 封装对象 Goods goods = new Goods(); goods.setCode(code); goods.setName(name); goods.setTypeId(Integer.parseInt(typeId)); // ... 设置其他字段 // 4. 调用Service保存 GoodsService service = new GoodsService(); boolean success = service.saveGoods(goods); // 5. 结果处理 if (success) { response.sendRedirect("goods?action=list"); } else { request.setAttribute("errorMsg", "保存失败,商品编码可能重复"); request.getRequestDispatcher("goods_form.jsp").forward(request, response); } }6. 销售模块的事务处理实战
销售收银是系统中最需要事务保证的功能模块,涉及多表操作和数据一致性。
6.1 购物车Session管理
// 添加到购物车 public void addToCart(HttpServletRequest request, Goods goods, int quantity) { HttpSession session = request.getSession(); List<CartItem> cart = (List<CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<>(); session.setAttribute("cart", cart); } // 检查是否已存在相同商品 boolean exists = false; for (CartItem item : cart) { if (item.getGoodsId().equals(goods.getId())) { item.setQuantity(item.getQuantity() + quantity); exists = true; break; } } if (!exists) { CartItem newItem = new CartItem(goods.getId(), goods.getName(), goods.getSalePrice(), quantity); cart.add(newItem); } }6.2 销售事务的完整实现
public boolean submitSale(Sale sale, List<SaleItem> items) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 SaleDao saleDao = new SaleDao(conn); SaleItemDao itemDao = new SaleItemDao(conn); GoodsDao goodsDao = new GoodsDao(conn); // 1. 插入销售主记录 int saleId = saleDao.insert(sale); // 2. 插入销售明细并扣减库存 for (SaleItem item : items) { item.setSaleId(saleId); itemDao.insert(item); // 扣减库存,需要检查库存是否充足 if (!goodsDao.reduceStock(item.getGoodsId(), item.getQuantity())) { throw new SQLException("商品库存不足: " + item.getGoodsId()); } } conn.commit(); // 提交事务 return true; } catch (SQLException e) { // 事务回滚 if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { DBUtil.closeConnection(conn); } }6.3 库存扣减的线程安全考虑
public boolean reduceStock(Integer goodsId, Integer quantity) { String sql = "UPDATE goods SET stock = stock - ? WHERE id = ? AND stock >= ?"; // 使用原子操作,避免并发问题 try (PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setInt(1, quantity); pstmt.setInt(2, goodsId); pstmt.setInt(3, quantity); // 确保库存充足 int affectedRows = pstmt.executeUpdate(); return affectedRows > 0; // 返回是否更新成功 } }7. 部署上线与性能优化建议
7.1 项目打包与部署
传统WAR包部署:
# 在Eclipse中导出WAR文件,然后部署到Tomcat cp SupermarketMS.war $CATALINA_HOME/webapps/ # 启动Tomcat $CATALINA_HOME/bin/startup.sh # 访问应用 http://localhost:8080/SupermarketMS现代部署方式建议:
# Dockerfile示例 FROM tomcat:9-jdk8 COPY SupermarketMS.war /usr/local/tomcat/webapps/ EXPOSE 8080 CMD ["catalina.sh", "run"]7.2 数据库连接池配置
替换简单的DBUtil,使用DBCP或HikariCP:
<!-- context.xml配置 --> <Resource name="jdbc/supermarketDB" type="javax.sql.DataSource" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/supermarket_db" username="root" password="yourpassword" maxTotal="20" maxIdle="10" maxWaitMillis="10000"/>7.3 常见性能优化点
数据库优化:
- 为经常查询的字段建立索引(username, goods_code等)
- 避免SELECT *,只查询需要的字段
- 分页查询大数据集
前端优化:
- 使用Bootstrap等CSS框架减少自定义样式
- JavaScript文件合并压缩
- 图片资源优化
后端优化:
- 频繁访问的数据加入缓存(Redis)
- 静态资源通过Nginx直接服务
- 数据库查询结果集分页
8. 从本项目扩展到实际生产环境
这个超市管理系统作为学习项目很完整,但要用于真实业务还需要考虑:
8.1 安全性增强
// 1. SQL注入防护 - 始终使用PreparedStatement String sql = "SELECT * FROM user WHERE username = ? AND password = ?"; // 2. XSS防护 - 输出转义 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:out value="${userInput}" /> // 3. CSRF防护 - 添加Token验证 <input type="hidden" name="csrfToken" value="${sessionScope.csrfToken}">8.2 日志记录与监控
// 使用Log4j记录操作日志 public class OperationLogger { private static final Logger logger = Logger.getLogger(OperationLogger.class); public static void logOperation(String username, String operation, String detail) { logger.info(String.format("用户[%s]执行[%s]: %s", username, operation, detail)); } }8.3 向Spring Boot演进
当掌握这个传统项目后,可以尝试用Spring Boot重构:
@SpringBootApplication public class SupermarketApplication { public static void main(String[] args) { SpringApplication.run(SupermarketApplication.class, args); } } @RestController @RequestMapping("/api/goods") public class GoodsController { @Autowired private GoodsService goodsService; @GetMapping public List<Goods> listGoods() { return goodsService.getAllGoods(); } }这个超市管理系统项目最大的价值不在于代码本身,而在于它提供了一个完整的Web应用开发范本。建议先确保能稳定运行基础版本,然后根据自己的需求逐步添加新功能,在这个过程中深入理解每个技术环节的工作原理。