☰
纯JavaWeb商城系统:Servlet+JSP+MySQL全链路实战
2026/10/7 14:35:56 网站建设 项目流程

简介:这是一份面向JavaWeb初学者与课程设计实践者的仿小米在线商城完整项目资源,涵盖JSP、Servlet、MySQL数据库、JDBC连接、CSS/JS前端交互及jQuery动态效果等核心技术点,帮助学习者系统掌握电商类Web应用的前后端开发流程。资源包共2个文件:1个39.94MB的主项目压缩包(含可直接部署的JavaWeb工程结构、页面源码与配置文件),1个SQL脚本文件(用于初始化商城数据库表结构与基础商品数据),类型明确、开箱即用。目前已有28049人学习下载,热度高、实操反馈扎实。读者可获得结构清晰的分层项目代码(含首页展示、商品详情页、购物车增删改查、价格实时计算等核心功能模块)、可运行的MySQL建库建表语句,以及与CSDN配套博文深度联动的开发思路解析,特别适合课程设计、毕业设计参考或JavaWeb综合能力进阶训练。

1. 这不是又一个“Hello World”商城:它用纯 JavaWeb 技术栈跑通了从用户注册到订单生成的全链路闭环,连 MySQL 的事务隔离级别都踩过坑才敢上线

你可能已经看过十多个“JavaWeb 商城 demo”,但多数卡在登录页就断了——前端页面能渲染,后端 Servlet 一调就 404,MySQL 表建好了却连不上,或者加购物车时数据重复插入、支付成功后库存没扣减。这个ShoppingMall不是教学截图拼凑的幻灯片,而是一个真实可运行、可调试、可扩展的最小可行商城系统:它用原生 Servlet + JSP(非 Spring Boot)实现用户中心、商品浏览、购物车、订单提交、后台管理五大模块;MySQL 使用 InnoDB 引擎,关键操作(如下单扣库存)显式开启事务并设置REPEATABLE READ隔离级别;所有 SQL 均经 PreparedStatement 预编译防注入;JSP 页面嵌套合理,无 scriptlet 滥用,CSS/JS 独立外链。它适合两类人:一是刚学完 JDBC 和 Servlet 生命周期、正卡在“理论懂但项目跑不起来”的 Java 初学者;二是需要快速验证 JavaWeb 工程结构、部署路径、数据库事务落地细节的一线开发——比如你要在 Tomcat 9 上部署一个轻量级内部采购系统,它就是最干净的起点。别被“仿小米”名字误导,它没用任何前端框架,但把 HTML 表单验证、AJAX 局部刷新、重定向跳转这些 JavaWeb 核心交互逻辑,全写进了webapp目录下可逐行调试的.jsp和.java文件里。

2. 从零启动:Tomcat 部署、MySQL 初始化与项目结构解剖

2.1 项目目录结构:为什么src下没有main/java?这是 JavaWeb 原生项目的“老派”约定

这个项目采用传统 JavaWeb 目录结构,而非 Maven 标准布局。打开压缩包后,你会看到:

ShoppingMall/ ├── src/ # Java 类文件直接放这里(非 Maven 结构) │ ├── dao/ # 数据访问层:UserDao.java, ProductDao.java... │ ├── entity/ # 实体类:User.java, Product.java, Order.java... │ ├── service/ # 业务逻辑层:UserService.java, OrderService.java... │ ├── servlet/ # 控制层:LoginServlet.java, AddToCartServlet.java... │ └── util/ # 工具类:DBUtil.java(含连接池配置)、StringUtil.java... ├── WebContent/ # 等价于标准 webapp 目录(IDEA 中需手动设为 Web Root) │ ├── css/ │ ├── js/ │ ├── images/ │ ├── WEB-INF/ │ │ ├── web.xml # 关键!Servlet 映射和初始化参数都在这 │ │ └── lib/ # mysql-connector-java-5.1.47.jar 就放这儿 │ ├── index.jsp │ ├── login.jsp │ └── ... └── db/ # SQL 脚本目录 └── shopping_mall.sql # 包含建库、建表、初始数据(含管理员账号)

注意:IDEA 导入时不要选 “Maven project”,而要选 “Import project → Create project from existing sources → Next → 选择 ShoppingMall 根目录 → Next → 选择 “Web Application” 模板 → Finish”。否则src下的 Java 类不会被识别为源码根目录,WEB-INF/web.xml也不会被自动加载。

2.2 MySQL 初始化:执行shopping_mall.sql前必须确认的三件事

db/shopping_mall.sql是一个完整的初始化脚本,但它依赖三个前提条件。漏掉任意一个,后续所有 Servlet 都会报SQLException: No database selected或Table 'xxx' doesn't exist。

  1. 字符集必须为 utf8mb4
    MySQL 5.7+ 默认字符集是utf8mb4,但部分本地安装仍为latin1。执行前先确认:

    SHOW VARIABLES LIKE 'character_set_database'; SHOW VARIABLES LIKE 'collation_database';

    若返回latin1,请在 MySQL 配置文件my.cnf中添加:

    [client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci

    重启 MySQL 后再执行脚本。

  2. 脚本中建库语句不可删
    shopping_mall.sql开头有:

    CREATE DATABASE IF NOT EXISTS shopping_mall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE shopping_mall;

    如果你已手动创建了同名库,但未执行USE shopping_mall;,后续建表会失败。务必保留这两行,不要手动建库后删掉它们。

  3. 初始管理员账号密码明文存储,首次登录后必须修改
    脚本末尾插入了默认管理员:

    INSERT INTO user (username, password, email, role) VALUES ('admin', '123456', 'admin@mall.com', 'admin');

    密码123456是明文(非 BCrypt 加密),仅用于快速启动。登录后台/admin/index.jsp后,立即进入“用户管理”修改密码,否则存在安全风险。

2.3 Tomcat 9 部署:web.xml中的<servlet-mapping>是 404 的唯一开关

所有 Servlet 的 URL 映射都定义在WebContent/WEB-INF/web.xml中。这是 JavaWeb 项目启动的核心契约。例如登录处理:

<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>

这意味着:
✅ 访问http://localhost:8080/ShoppingMall/login会触发LoginServlet的doPost()方法;
❌ 访问http://localhost:8080/ShoppingMall/LoginServlet(带类名)或/login.do(没配映射)会 404;
⚠️ 若你修改了<url-pattern>,必须同步更新所有 JSP 表单的action属性,例如login.jsp中:

<form action="login" method="post"> <!-- 必须与 web.xml 中的 url-pattern 完全一致 -->

3. 核心功能链路:从用户登录到订单生成的四次关键跳转与数据流转

3.1 登录流程:LoginServlet如何用 Session 绑定用户身份并规避明文密码传输

LoginServlet的doPost()方法是整个系统的第一个业务入口。它不做密码加密(因数据库存明文),但通过三层防护保证会话安全:

  1. 表单提交走 POST,避免密码出现在 URL 中
    login.jsp的<form method="post">是基础,但更重要的是LoginServlet中对空值的防御:

    String username = request.getParameter("username").trim(); String password = request.getParameter("password"); if (username == null || username.length() == 0 || password == null) { request.setAttribute("msg", "用户名或密码不能为空"); request.getRequestDispatcher("login.jsp").forward(request, response); return; // 防止后续代码执行 }
  2. 查询结果用User对象封装,而非裸 ResultSet
    UserDao.login(username, password)返回User实例,其中role字段决定跳转路径:

    if (user != null) { HttpSession session = request.getSession(); session.setAttribute("user", user); // 关键:将用户对象存入 Session if ("admin".equals(user.getRole())) { response.sendRedirect("admin/index.jsp"); // 管理员跳后台 } else { response.sendRedirect("index.jsp"); // 普通用户跳首页 } } else { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); }
  3. Session 失效时间在web.xml中统一配置
    项目已在web.xml中声明:

    <session-config> <session-timeout>30</session-timeout> <!-- 单位:分钟 --> </session-config>

    这比在每个 Servlet 中调用session.setMaxInactiveInterval(1800)更可靠,避免遗漏。

3.2 购物车操作:AddToCartServlet的并发安全设计与库存校验逻辑

购物车不是简单往 Session 里塞 List,而是每次请求都查库、校验、更新,确保库存一致性。AddToCartServlet的核心逻辑如下:

// 1. 先查商品当前库存(SELECT ... FOR UPDATE 在事务中) Product product = productDao.findById(productId); if (product.getStock() < quantity) { request.setAttribute("msg", "库存不足,当前剩余:" + product.getStock()); request.getRequestDispatcher("product.jsp?id=" + productId).forward(request, response); return; } // 2. 开启事务,扣减库存并插入购物车记录 Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); try { // 扣库存(UPDATE ... WHERE id=? AND stock>=?) int updated = productDao.updateStock(productId, quantity); if (updated == 0) { // 并发场景下,其他请求已扣完库存 throw new RuntimeException("库存已被抢光"); } // 插入购物车 cartDao.addCartItem(userId, productId, quantity); conn.commit(); } catch (Exception e) { conn.rollback(); request.setAttribute("msg", "添加失败:" + e.getMessage()); }

关键点说明:

  • productDao.updateStock()的 SQL 是UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?,利用 MySQL 的原子性判断,避免先查后更新的竞态;
  • cartDao.addCartItem()插入的是cart_item表,而非把商品对象序列化进 Session —— 这样支持多设备同步购物车;
  • 事务回滚后,request.setAttribute()保证错误信息透传到 JSP,用户看到“库存已被抢光”而非 500 错误页。

3.3 订单生成:OrderServlet的事务边界与幂等性控制

下单是最易出错的环节。OrderServlet用单一事务包裹“扣库存 + 创建订单 + 创建订单项”三步,并加入幂等校验:

String orderNo = "ORD" + System.currentTimeMillis(); // 简单订单号生成 HttpSession session = request.getSession(); User user = (User) session.getAttribute("user"); // 1. 幂等校验:检查该用户是否已有未支付订单(防止重复提交) if (orderDao.hasUnpaidOrder(user.getId())) { request.setAttribute("msg", "您有未支付订单,请先完成支付"); request.getRequestDispatcher("cart.jsp").forward(request, response); return; } // 2. 开启事务 Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); try { // 扣库存(同购物车逻辑) for (CartItem item : cartItems) { productDao.updateStock(item.getProductId(), item.getQuantity()); } // 创建订单主表 Order order = new Order(orderNo, user.getId(), totalAmount, "unpaid"); orderDao.createOrder(order); // 创建订单项 for (CartItem item : cartItems) { OrderItem oi = new OrderItem(order.getId(), item.getProductId(), item.getQuantity(), item.getPrice()); orderItemDao.createOrderItem(oi); } conn.commit(); // 清空购物车 cartDao.clearCart(user.getId()); request.setAttribute("orderNo", orderNo); request.getRequestDispatcher("order_success.jsp").forward(request, response); } catch (Exception e) { conn.rollback(); request.setAttribute("msg", "下单失败:" + e.getMessage()); request.getRequestDispatcher("cart.jsp").forward(request, response); }

参数说明:

  • orderNo用时间戳前缀保证全局唯一,避免 UUID 生成开销;
  • hasUnpaidOrder()查询SELECT COUNT(*) FROM orders WHERE user_id = ? AND status = 'unpaid',是幂等性的第一道闸;
  • clearCart()在事务提交后执行,确保只有成功下单才清空购物车。

4. 避坑指南:五个让新手卡三天的真实问题与血泪解决方案

4.1 现象:启动 Tomcat 后访问http://localhost:8080/ShoppingMall显示 404,但index.jsp确实存在

原因:IDEA 未正确识别WebContent为 Web Root,导致静态资源无法映射。web.xml中的<welcome-file-list>也未生效。
解决:右键项目 →Open Module Settings→Project Settings→Modules→ 选中项目 →Sources标签页 → 点击WebContent文件夹 → 点击上方Mark as: Web图标(小地球)。然后在Paths标签页确认Web resource directory指向WebContent。最后重启 Tomcat。

4.2 现象:登录时提示java.lang.ClassNotFoundException: com.mysql.jdbc.Driver

原因:MySQL 8.0+ 驱动类名已从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver,而项目DBUtil.java中仍用旧类名。
解决:打开src/util/DBUtil.java,找到Class.forName("com.mysql.jdbc.Driver");,改为Class.forName("com.mysql.cj.jdbc.Driver");。同时确认WEB-INF/lib/下的 jar 包是mysql-connector-java-8.0.28.jar(非 5.x 版本),并在jdbc.url中添加时区参数:jdbc:mysql://localhost:3306/shopping_mall?serverTimezone=Asia/Shanghai&useSSL=false。

4.3 现象:添加商品到购物车后,页面跳转到cart.jsp但购物车为空

原因:AddToCartServlet中response.sendRedirect("cart.jsp")是重定向,导致 Session 中的cartItems未传递。而cart.jsp依赖request.getAttribute("cartItems"),但重定向后 request 已失效。
解决:将sendRedirect改为RequestDispatcher.forward,并在跳转前将数据存入 request:

// 替换原来的 response.sendRedirect("cart.jsp"); request.setAttribute("cartItems", cartItems); // 先存数据 request.getRequestDispatcher("cart.jsp").forward(request, response); // 再跳转

4.4 现象:后台管理页/admin/index.jsp打开空白,查看浏览器控制台报Uncaught ReferenceError: $ is not defined

原因:admin/index.jsp引用了 jQuery,但WebContent/js/jquery.min.js文件缺失或路径错误。项目压缩包中该文件实际位于WebContent/admin/js/jquery.min.js。
解决:打开WebContent/admin/index.jsp,找到<script src="js/jquery.min.js"></script>,改为<script src="admin/js/jquery.min.js"></script>。同理检查所有 admin 页面的 JS/CSS 路径。

4.5 现象:下单成功后,数据库product表库存未减少,但orders表已新增记录

原因:OrderServlet中productDao.updateStock()的 SQL 语句未启用事务,或DBUtil.getConnection()返回的 Connection 未设置autoCommit=false。
解决:检查src/dao/ProductDao.java中updateStock()方法,确认其使用的是DBUtil.getConnection()获取的连接,且该连接在OrderServlet中已调用conn.setAutoCommit(false)。切勿在 DAO 方法内单独获取 Connection,必须由 Service/Servlet 统一管理事务边界。

5. 进阶验证:用三组 curl 命令验证核心接口的健壮性与边界行为

5.1 验证登录接口的抗压能力:模拟 10 次并发登录请求

不要只靠浏览器点点点,用curl直接打接口,暴露隐藏问题。在终端执行:

# 启动 10 个并发请求,测试登录接口 for i in {1..10}; do curl -X POST "http://localhost:8080/ShoppingMall/login" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin" \ -d "password=123456" \ -w "\nHTTP Status: %{http_code}\n" \ -o /dev/null -s & done; wait

✅预期结果:10 次请求全部返回HTTP Status: 302(重定向),无 500 或 404;
❌翻车信号:出现HTTP Status: 500,说明LoginServlet未处理好并发 Session 创建;出现HTTP Status: 404,说明web.xml中 Servlet 映射路径有误。

5.2 验证购物车库存校验:用 curl 模拟超量添加,观察数据库状态

直接绕过前端,在命令行构造恶意请求:

# 尝试添加 999 件商品(远超库存) curl -X POST "http://localhost:8080/ShoppingMall/addToCart" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "productId=1" \ -d "quantity=999" \ -b "JSESSIONID=YOUR_SESSION_ID" \ -w "\nResponse Body: %{redirect_url}\n" \ -o /dev/null -s

✅预期结果:响应头中Location字段应为product.jsp?id=1,且返回页面显示“库存不足”;
❌黑匣子风险:若返回Location: cart.jsp且数据库product.stock变为负数,说明updateStock()的WHERE stock >= ?条件未生效,需检查 SQL 是否被意外注释。

5.3 验证订单幂等性:连续发送两次相同下单请求

获取一次下单的表单数据(用浏览器开发者工具抓包),然后用 curl 重放:

# 第一次下单(正常) curl -X POST "http://localhost:8080/ShoppingMall/order" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "address=北京市朝阳区" \ -d "phone=13800138000" \ -b "JSESSIONID=YOUR_SESSION_ID" \ -w "\nFirst Order Status: %{http_code}\n" \ -o /dev/null -s # 立即第二次重放(应被拦截) curl -X POST "http://localhost:8080/ShoppingMall/order" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "address=北京市朝阳区" \ -d "phone=13800138000" \ -b "JSESSIONID=YOUR_SESSION_ID" \ -w "\nSecond Order Status: %{http_code}\n" \ -o /dev/null -s

✅预期结果:第一次返回200,第二次返回302且重定向到cart.jsp,数据库orders表仅新增 1 条记录;
⚠️后悔药时刻:若两次都成功,立刻检查OrderServlet中hasUnpaidOrder()方法的 SQL 是否漏写了AND status = 'unpaid'条件。

6. 生产就绪技巧:把ShoppingMall改造成可维护的工程模板的三个硬核习惯

6.1 用log4j.properties替代System.out.println:让每一笔订单都有迹可循

项目当前所有日志都用System.out.println("订单创建成功"),这在开发阶段够用,但上线后无法分级、无法输出到文件、无法追踪用户 ID。我一般会在src/下新建log4j.properties:

log4j.rootLogger=INFO, stdout, file log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c{1} - %m%n log4j.appender.file=org.apache.log4j.RollingFileAppender log4j.appender.file.File=logs/mall.log log4j.appender.file.MaxFileSize=10MB log4j.appender.file.MaxBackupIndex=5 log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c{1} - %m%n

然后在OrderServlet中:

import org.apache.log4j.Logger; private static final Logger logger = Logger.getLogger(OrderServlet.class); // 下单成功后 logger.info("用户[" + user.getId() + "] 创建订单[" + orderNo + "],金额[" + totalAmount + "]");

为什么必须做:当运营反馈“某用户说订单没到账”,你不用翻 1000 行控制台日志,直接grep "用户[12345]" logs/mall.log就能定位全链路行为。从那以后我每次接手 JavaWeb 项目,第一件事就是删掉所有System.out,补上 log4j。

6.2 把DBUtil升级为 C3P0 连接池:告别java.sql.SQLException: Too many connections

原生DriverManager.getConnection()每次都新建物理连接,10 个并发请求就可能耗尽 MySQL 默认 100 连接数。替换步骤:

  1. 下载c3p0-0.9.5.5.jar放入WEB-INF/lib/;
  2. 删除src/util/DBUtil.java,新建src/util/C3P0Util.java:
import com.mchange.v2.c3p0.ComboPooledDataSource; public class C3P0Util { private static ComboPooledDataSource dataSource = new ComboPooledDataSource(); static { try { dataSource.setDriverClass("com.mysql.cj.jdbc.Driver"); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/shopping_mall?serverTimezone=Asia/Shanghai"); dataSource.setUser("root"); dataSource.setPassword("123456"); dataSource.setInitialPoolSize(5); dataSource.setMaxPoolSize(20); } catch (Exception e) { throw new RuntimeException(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }
  1. 所有 DAO 类中DBUtil.getConnection()替换为C3P0Util.getConnection()。

参数说明:initialPoolSize=5保证启动时就有 5 个空闲连接;maxPoolSize=20防止突发流量打爆数据库;连接池自动回收空闲连接,无需手动close()。

6.3 为web.xml添加 Filter 实现统一编码与 XSS 过滤

JSP 表单提交中文乱码、用户输入<script>alert(1)</script>直接入库,都是 JavaWeb 新手的高频翻车点。在web.xml中追加:

<filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>util.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>XSSFilter</filter-name> <filter-class>util.XSSFilter</filter-class> </filter> <filter-mapping> <filter-name>XSSFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

对应src/util/CharacterEncodingFilter.java:

public class CharacterEncodingFilter implements Filter { private String encoding; public void init(FilterConfig config) throws ServletException { this.encoding = config.getInitParameter("encoding"); } public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; request.setCharacterEncoding(encoding); response.setCharacterEncoding(encoding); chain.doFilter(request, response); } }

以及src/util/XSSFilter.java(简化版,过滤 request 参数中的 script 标签):

public class XSSFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; XSSRequestWrapper wrapper = new XSSRequestWrapper(request); chain.doFilter(wrapper, resp); } } // XSSRequestWrapper 类需重写 getParameter() 等方法,用 String.replace("<script", "") 等清洗

为什么值得花这 2 小时:统一编码解决 90% 的中文乱码;XSS 过滤让request.getParameter("content")返回的永远是安全字符串,而不是一段可执行的 JS。从那以后我每次新建 JavaWeb 项目,都强制走一遍 Filter 配置,哪怕当时觉得“就几个页面,没必要”。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询