ARTICLE DETAIL

资讯详情

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

纯JavaWeb商城系统:Servlet+JSP+MySQL全链路实战

纯JavaWeb商城系统:Servlet+JSP+MySQL全链路实战 简介这是一份面向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 一调就 404MySQL 表建好了却连不上或者加购物车时数据重复插入、支付成功后库存没扣减。这个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 doesnt exist。字符集必须为 utf8mb4MySQL 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 后再执行脚本。脚本中建库语句不可删shopping_mall.sql开头有CREATE DATABASE IF NOT EXISTS shopping_mall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE shopping_mall;如果你已手动创建了同名库但未执行USE shopping_mall;后续建表会失败。务必保留这两行不要手动建库后删掉它们。初始管理员账号密码明文存储首次登录后必须修改脚本末尾插入了默认管理员INSERT INTO user (username, password, email, role) VALUES (admin, 123456, adminmall.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-nameLoginServlet/servlet-name servlet-classservlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/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 actionlogin methodpost !-- 必须与 web.xml 中的 url-pattern 完全一致 --3. 核心功能链路从用户登录到订单生成的四次关键跳转与数据流转3.1 登录流程LoginServlet如何用 Session 绑定用户身份并规避明文密码传输LoginServlet的doPost()方法是整个系统的第一个业务入口。它不做密码加密因数据库存明文但通过三层防护保证会话安全表单提交走 POST避免密码出现在 URL 中login.jsp的form methodpost是基础但更重要的是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; // 防止后续代码执行 }查询结果用User对象封装而非裸 ResultSetUserDao.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); }Session 失效时间在web.xml中统一配置项目已在web.xml中声明session-config session-timeout30/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?serverTimezoneAsia/ShanghaiuseSSLfalse。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 srcjs/jquery.min.js/script改为script srcadmin/js/jquery.min.js/script。同理检查所有 admin 页面的 JS/CSS 路径。4.5 现象下单成功后数据库product表库存未减少但orders表已新增记录原因OrderServlet中productDao.updateStock()的 SQL 语句未启用事务或DBUtil.getConnection()返回的 Connection 未设置autoCommitfalse。解决检查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 usernameadmin \ -d password123456 \ -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 productId1 \ -d quantity999 \ -b JSESSIONIDYOUR_SESSION_ID \ -w \nResponse Body: %{redirect_url}\n \ -o /dev/null -s✅预期结果响应头中Location字段应为product.jsp?id1且返回页面显示“库存不足”❌黑匣子风险若返回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 phone13800138000 \ -b JSESSIONIDYOUR_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 phone13800138000 \ -b JSESSIONIDYOUR_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.propertieslog4j.rootLoggerINFO, stdout, file log4j.appender.stdoutorg.apache.log4j.ConsoleAppender log4j.appender.stdout.layoutorg.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern%d{yyyy-MM-dd HH:mm:ss} [%p] %c{1} - %m%n log4j.appender.fileorg.apache.log4j.RollingFileAppender log4j.appender.file.Filelogs/mall.log log4j.appender.file.MaxFileSize10MB log4j.appender.file.MaxBackupIndex5 log4j.appender.file.layoutorg.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 连接数。替换步骤下载c3p0-0.9.5.5.jar放入WEB-INF/lib/删除src/util/DBUtil.java新建src/util/C3P0Util.javaimport 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?serverTimezoneAsia/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(); } }所有 DAO 类中DBUtil.getConnection()替换为C3P0Util.getConnection()。参数说明initialPoolSize5保证启动时就有 5 个空闲连接maxPoolSize20防止突发流量打爆数据库连接池自动回收空闲连接无需手动close()。6.3 为web.xml添加 Filter 实现统一编码与 XSS 过滤JSP 表单提交中文乱码、用户输入scriptalert(1)/script直接入库都是 JavaWeb 新手的高频翻车点。在web.xml中追加filter filter-nameCharacterEncodingFilter/filter-name filter-classutil.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping filter filter-nameXSSFilter/filter-name filter-classutil.XSSFilter/filter-class /filter filter-mapping filter-nameXSSFilter/filter-name url-pattern/*/url-pattern /filter-mapping对应src/util/CharacterEncodingFilter.javapublic 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 配置哪怕当时觉得“就几个页面没必要”。希望帮到你。本文还有配套的精品资源点击获取
返回列表