ARTICLE DETAIL

资讯详情

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

Java在线购物系统开发实战:从Servlet到数据库事务的完整实现

Java在线购物系统开发实战:从Servlet到数据库事务的完整实现 简介Java Web开发是构建企业级应用的核心技术栈其核心原理基于HTTP协议、请求响应生命周期和MVC设计模式。通过Servlet、JSP和JDBC等技术组合开发者能够实现动态网页交互与数据持久化为构建复杂业务系统提供基础框架。在电商领域这些技术的工程价值体现在高并发处理、数据一致性和用户体验优化等方面。典型的应用场景包括用户会话管理、商品展示与搜索、购物车持久化以及订单处理等关键业务流程。本文以经典的在线购物系统为例深入解析了如何利用Java EE技术栈实现数据库事务管理和库存扣减机制确保在高并发场景下的数据一致性为初学者和进阶开发者提供了一套可落地的实践方案。1. 项目概述一个Java在线购物系统的诞生记最近在整理硬盘翻出来一个压箱底的老项目——“基于Java的在线购物系统”。看着那个熟悉的.7z压缩包里面不仅有完整的源代码还有部署好的系统。这让我想起了当年为了完成毕业设计或者课程大作业没日没夜敲代码的日子。这个项目可以说是无数计算机专业学生踏入Web开发领域的“启蒙老师”之一。它麻雀虽小五脏俱全从前端页面到后端逻辑从数据库设计到业务实现完整地走了一遍电商系统的基本流程。今天我就以这个经典项目为蓝本拆解一下它的设计与实现希望能给正在做类似项目的朋友一些实实在在的参考。无论你是想学习Java Web开发还是需要一个课程设计的思路这篇文章都会带你从零开始理解一个在线购物系统是如何构建起来的。2. 系统整体架构与核心技术选型2.1 为什么选择Java EE技术栈提到Java Web项目尤其是学校里的课程设计Servlet JSP JDBC这套经典组合的出现概率极高。我们这个在线购物系统也不例外。选择这套技术栈背后有非常实际的考量。首先教学与学习的普适性。Servlet和JSP是Java EE Web开发的核心基础几乎所有高校的Java Web课程都会覆盖。使用它们来完成项目能够将课堂理论知识与实践紧密结合巩固对HTTP协议、请求响应生命周期、MVC模式等核心概念的理解。其次环境搭建简单。只需要一个Tomcat服务器和一个MySQL数据库配合Eclipse或IDEA开发环境几分钟就能跑起来避免了在环境配置上消耗过多精力。最后技术成熟稳定。这套技术栈历经多年考验资料丰富社区活跃遇到任何问题几乎都能找到解决方案对于学习者来说非常友好。当然从今天的工程化视角看这套技术略显“复古”但它所蕴含的分层思想和业务逻辑与现代的Spring Boot项目是相通的。理解了这个“原始版本”再学习Spring MVC、MyBatis乃至Spring Cloud你会更有一种“知其所以然”的透彻感。2.2 核心功能模块拆解一个基础的在线购物系统通常包含以下五个核心模块它们共同构成了用户从浏览到下单的完整闭环用户管理模块这是系统的门户。负责用户的注册、登录、个人信息维护如收货地址管理以及权限控制区分普通用户和管理员。这里会涉及密码的加密存储通常使用MD5但现在更推荐BCrypt或SHA-256加盐、Session会话管理等技术点。商品展示模块系统的“橱窗”。包括商品分类浏览、商品列表分页展示、商品详情查看、商品搜索按名称、分类等功能。这里需要考虑前端页面如何优雅地展示大量商品图片和信息以及后端如何高效地进行数据库查询和分页处理。购物车模块交易的核心中转站。用户可以将心仪的商品加入购物车在购物车内调整商品数量、删除商品。购物车数据通常需要持久化即使用户关闭浏览器再次打开购物车内容依然存在。实现方式主要有两种利用Session在服务器端临时存储或者将购物车信息存入数据库关联用户ID。前者实现简单但服务器压力大后者更持久但需要频繁读写数据库。订单处理模块业务流程的终点。用户从购物车结算生成订单。这个模块最为复杂涉及订单的创建生成唯一订单号、订单项商品快照的保存、订单状态的管理如待付款、待发货、已发货、已完成、以及库存的预扣减防止超卖。这里需要特别注意数据库事务的处理确保订单生成、库存扣减等多个操作要么全部成功要么全部回滚。后台管理模块系统的“驾驶舱”。仅供管理员使用功能包括商品信息的上传与维护增删改查、商品分类管理、订单管理查看订单详情、修改发货状态、用户管理等。这个模块通常有独立的登录验证和界面。注意在数据库设计时一定要为每个商品记录设置一个“库存”字段。在用户下单时必须在同一个数据库事务中完成“扣减库存”和“生成订单”的操作这是防止商品超卖的最基本、最重要的防线。许多初学者项目会忽略这一点导致逻辑漏洞。3. 数据库设计与核心表结构解析数据库是系统的基石设计的好坏直接决定了系统的性能、稳定性和扩展性。对于一个在线购物系统以下几张核心表是必不可少的。3.1 用户表 (user_table)这张表存储所有注册用户的信息。CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘用户ID主键’, username varchar(50) NOT NULL UNIQUE COMMENT ‘用户名唯一’, password varchar(255) NOT NULL COMMENT ‘加密后的密码’, email varchar(100) DEFAULT NULL COMMENT ‘邮箱’, phone varchar(20) DEFAULT NULL COMMENT ‘手机号’, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT ‘注册时间’, is_admin tinyint(1) DEFAULT ‘0’ COMMENT ‘是否是管理员0-否1-是’, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT‘用户表’;设计要点password字段长度要足够建议255为未来使用更安全的加密算法如BCrypt留出空间。绝对不要明文存储密码。is_admin字段用于实现简单的权限分离。管理员和普通用户看到的是不同的菜单和功能。username通常设为唯一索引方便快速登录校验。3.2 商品表 (product_table) 与分类表 (category_table)商品和分类通常是多对一的关系一个商品属于一个分类一个分类下有多个商品。CREATE TABLE category ( cid int(11) NOT NULL AUTO_INCREMENT COMMENT ‘分类ID’, cname varchar(100) NOT NULL COMMENT ‘分类名称’, PRIMARY KEY (cid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT‘商品分类表’; CREATE TABLE product ( pid int(11) NOT NULL AUTO_INCREMENT COMMENT ‘商品ID’, pname varchar(200) NOT NULL COMMENT ‘商品名称’, price decimal(10,2) NOT NULL COMMENT ‘商品价格’, stock int(11) NOT NULL DEFAULT ‘0’ COMMENT ‘商品库存’, description text COMMENT ‘商品描述’, image_url varchar(500) DEFAULT NULL COMMENT ‘商品主图路径’, cid int(11) DEFAULT NULL COMMENT ‘所属分类ID外键关联category表’, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT ‘上架时间’, PRIMARY KEY (pid), KEY idx_cid (cid), CONSTRAINT fk_product_category FOREIGN KEY (cid) REFERENCES category (cid) ON DELETE SET NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT‘商品表’;设计要点price字段使用DECIMAL类型精确存储金额避免浮点数精度问题。stock库存是关键字段所有涉及商品售卖的操作都必须关注它。image_url存储的是图片在服务器上的相对路径或静态资源服务器地址而不是将图片以二进制形式存入数据库BLOB后者会严重影响数据库性能。在cid上建立索引(idx_cid)能大幅提升按分类查询商品列表的速度。3.3 购物车表 (cart_table) 与订单表 (order_table)这是业务逻辑最复杂的部分。购物车是用户的临时选择而订单是最终的交易凭证。购物车表记录了用户选了哪些商品选了多少。CREATE TABLE cart ( cart_id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT ‘用户ID’, product_id int(11) NOT NULL COMMENT ‘商品ID’, quantity int(11) NOT NULL DEFAULT ‘1’ COMMENT ‘商品数量’, add_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (cart_id), UNIQUE KEY uk_user_product (user_id,product_id), -- 同一用户的同一商品在购物车中只存一条记录 KEY idx_user (user_id), CONSTRAINT fk_cart_user FOREIGN KEY (user_id) REFERENCES user (id), CONSTRAINT fk_cart_product FOREIGN KEY (product_id) REFERENCES product (pid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT‘购物车表’;设计要点uk_user_product这个唯一联合索引是关键。它保证了同一个用户的购物车里同一件商品只会有一条记录用户再次添加时只需要更新quantity数量字段即可避免了数据冗余和逻辑混乱。订单表分为两张主表记录订单概要从表订单项表记录订单详情。CREATE TABLE order ( oid varchar(32) NOT NULL COMMENT ‘订单号使用时间戳随机数生成唯一’, user_id int(11) NOT NULL COMMENT ‘下单用户ID’, total_amount decimal(10,2) NOT NULL COMMENT ‘订单总金额’, status tinyint(4) NOT NULL DEFAULT ‘1’ COMMENT ‘订单状态1-待付款2-待发货3-已发货4-已完成5-已取消’, address varchar(500) NOT NULL COMMENT ‘收货地址’, receiver varchar(50) NOT NULL COMMENT ‘收货人’, phone varchar(20) NOT NULL COMMENT ‘收货电话’, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT ‘下单时间’, pay_time datetime DEFAULT NULL COMMENT ‘付款时间’, PRIMARY KEY (oid), KEY idx_user (user_id), KEY idx_create_time (create_time), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT‘订单主表’; CREATE TABLE order_item ( item_id int(11) NOT NULL AUTO_INCREMENT, order_id varchar(32) NOT NULL COMMENT ‘所属订单号’, product_id int(11) NOT NULL COMMENT ‘商品ID’, product_name varchar(200) NOT NULL COMMENT ‘商品名称快照’, product_price decimal(10,2) NOT NULL COMMENT ‘商品单价快照’, quantity int(11) NOT NULL COMMENT ‘购买数量’, total_price decimal(10,2) NOT NULL COMMENT ‘商品小计’, PRIMARY KEY (item_id), KEY idx_order (order_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES order (oid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT‘订单项表’;设计要点订单号oid不使用自增ID而使用自定义的唯一字符串如UUID或时间戳随机数更安全也便于在业务中传递。数据快照order_item表中存储了product_name和product_price这是商品信息的快照。即使后台后来修改了商品名称或价格已产生的订单记录也不会改变保证了交易记录的不可篡改性。状态枚举status字段使用数字表示状态在代码中定义常量枚举逻辑清晰。索引优化在user_id和create_time上建立索引能极大提升“我的订单”列表查询的效率。4. 核心功能实现与代码要点4.1 用户登录与会话管理用户登录是系统的入口。在Servlet中处理登录请求的doPost方法大致流程如下获取前端传来的用户名和密码。根据用户名查询数据库获取用户信息包括加密后的密码。将用户输入的密码用相同算法加密与数据库存储的密文进行比对。如果匹配成功将用户对象至少包含user_id和username存入HttpSession中。// 伪代码示例 User user userDao.findByUsername(username); if(user ! null encrypt(password).equals(user.getPassword())) { request.getSession().setAttribute(“loginUser”, user); response.sendRedirect(“index.jsp”); // 登录成功跳转首页 } else { request.setAttribute(“msg”, “用户名或密码错误”); request.getRequestDispatcher(“login.jsp”).forward(request, response); // 返回登录页并提示错误 }实操心得在每一个需要登录才能访问的页面或Servlet如购物车页、下单页的开头都要先检查Session中是否存在loginUser对象如果没有则重定向到登录页面。这是实现权限控制最基本的方式。4.2 商品分页查询与展示当商品数量很多时分页是必须的。后端需要接收两个参数pageNum当前页码和pageSize每页条数。// 伪代码示例ProductServlet.java int pageNum Integer.parseInt(request.getParameter(“pageNum”) null ? “1” : request.getParameter(“pageNum”)); int pageSize 10; // 每页显示10条 // 计算起始索引 int start (pageNum - 1) * pageSize; // 查询当前页数据 ListProduct productList productDao.findByPage(start, pageSize, categoryId); // 查询总记录数 int totalCount productDao.getCount(categoryId); // 计算总页数 int totalPage (totalCount pageSize - 1) / pageSize; // 将数据存入request转发给JSP页面 request.setAttribute(“productList”, productList); request.setAttribute(“pageNum”, pageNum); request.setAttribute(“totalPage”, totalPage); request.getRequestDispatcher(“product_list.jsp”).forward(request, response);对应的SQL语句类似-- 查询数据 SELECT * FROM product WHERE cid ? LIMIT ?, ?; -- 查询总数 SELECT COUNT(*) FROM product WHERE cid ?;注意事项LIMIT语句在数据量极大时可能存在性能问题但作为学习项目完全够用。前端JSP页面需要根据pageNum和totalPage来动态生成上一页、下一页、页码数字的链接。4.3 购物车功能的两种实现思路如前所述购物车数据可以存Session也可以存数据库。方案一Session存储适合课程设计优点实现极其简单无需设计数据库表不增加数据库压力。缺点数据在用户会话结束后丢失服务器内存压力大用户换浏览器或清Cookie后购物车清空。实现在Servlet中将购物车对象如一个MapProduct, Integer直接存入Session。方案二数据库存储更贴近实际优点数据持久化用户体验好便于做购物车商品推荐等扩展功能。缺点实现稍复杂需要设计表结构频繁读写数据库。实现就是我们上面设计的cart表。每次添加商品执行INSERT ... ON DUPLICATE KEY UPDATE quantity quantity ?语句。查询购物车时通过user_id联查product表获取完整的商品信息。个人建议对于课程项目我强烈推荐使用数据库存储方案。虽然多写几张表、几个DAO但这能让你更深刻地理解数据持久化和用户状态管理项目的完整度和实用性也更高。面试时聊起来也更有底气。4.4 下单与库存扣减事务是关键这是整个系统最需要严谨对待的环节。下单不是一个简单的INSERT而是一个包含多个步骤的原子操作。// 伪代码示例OrderServlet.java 中的下单方法 public void createOrder(HttpServletRequest request) { Connection conn null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 1. 开启事务 // 2. 验证并获取购物车商品、库存 ListCartItem cartItems cartService.getCartForUser(userId); for(CartItem item : cartItems) { Product product productDao.getByIdForUpdate(item.getProductId(), conn); // 悲观锁锁定商品行 if(product.getStock() item.getQuantity()) { throw new RuntimeException(“商品【” product.getName() “】库存不足”); } // 预扣库存在内存中计算先不更新数据库 product.setStock(product.getStock() - item.getQuantity()); // ... 其他校验 } // 3. 生成订单号 String orderId generateOrderId(); // 4. 插入订单主表 orderDao.insertOrder(order, conn); // 5. 插入订单项表 for(CartItem item : cartItems) { orderItemDao.insertItem(orderItem, conn); } // 6. 批量更新商品库存实际扣减 for(CartItem item : cartItems) { productDao.updateStock(item.getProductId(), -item.getQuantity(), conn); } // 7. 清空当前用户的购物车 cartDao.clearByUserId(userId, conn); conn.commit(); // 8. 提交事务 return orderId; } catch (Exception e) { if(conn ! null) { conn.rollback(); // 9. 发生异常回滚事务 } throw e; } finally { if(conn ! null) { conn.setAutoCommit(true); conn.close(); } } }核心要点解析事务Transaction使用conn.setAutoCommit(false)开启手动事务确保2-7步所有数据库操作要么全部成功commit要么全部失败回滚rollback。这保证了数据的一致性不会出现“扣了库存却没生成订单”的灾难性bug。库存校验与锁定在循环中先查询商品当前库存并判断是否充足。这里使用了SELECT ... FOR UPDATE通过getByIdForUpdate方法体现进行悲观锁在事务提交前锁住这条商品记录防止其他并发请求同时修改库存导致超卖。订单号生成需要生成全局唯一的订单号常用“时间戳精确到毫秒随机数/序列号”的方式。数据快照插入order_item时保存的是商品下单时的名称和价格而非实时查询的结果。5. 项目部署、优化与常见问题排查5.1 从开发环境到生产部署课程项目通常在本机Tomcat运行但了解部署流程有益无害。打包在IDE中将项目导出为WAR包Web Application Archive。部署将WAR包复制到服务器Tomcat的webapps目录下启动Tomcat它会自动解压部署。数据库在服务器上安装MySQL执行项目的SQL脚本创建数据库和表结构并修改项目中的数据库连接配置JDBC URL,Username,Password通常这些配置放在WEB-INF/web.xml或单独的属性文件中。静态资源如果项目中有用户上传的图片需要确保上传路径如/upload在服务器上存在且Tomcat有写入权限或者配置到Tomcat外部。5.2 性能与安全优化建议虽然是一个学习项目但引入一些优化思想能让你的项目脱颖而出。连接池不要在每个Servlet里都创建和关闭数据库连接。使用像DBCP或HikariCP这样的数据库连接池可以极大提升性能。在Servlet的init()方法中初始化连接池在整个应用中共享。密码加密不要再使用MD5。使用BCryptPasswordEncoderSpring Security提供或SHA-256加盐哈希来存储密码安全级别更高。前端输入校验在JSP页面使用JavaScript进行非空、格式等基本校验同时在Servlet后端也必须再次进行严格的校验和过滤防止SQL注入和XSS攻击。所有从request.getParameter()获取的参数在拼接SQL前都要进行转义或使用PreparedStatement。异常处理不要简单地将异常堆栈信息打印给用户。定义统一的错误页面如error.jsp在web.xml中配置error-page给用户友好的提示同时将详细错误记录到日志文件中。5.3 常见问题排查实录在开发过程中你几乎一定会遇到下面这些问题中文乱码现象页面显示问号“??”或插入数据库后变成乱码。排查这是一个“三码合一”问题。解决JSP页面在页面顶部添加% page contentType“text/html;charsetUTF-8” language“java” pageEncoding“UTF-8”%。Servlet请求/响应在doGet/doPost方法最开始设置request.setCharacterEncoding(“UTF-8”)和response.setContentType(“text/html;charsetUTF-8”)。数据库连接在JDBC连接URL后加上参数?useUnicodetruecharacterEncodingUTF-8。数据库本身确保创建数据库和表时指定了DEFAULT CHARSETutf8mb4。404错误找不到页面或Servlet现象浏览器提示404。排查检查URL是否拼写错误。检查web.xml中servlet和servlet-mapping的配置是否正确url-pattern是否匹配。检查Tomcat的webapps目录下你的项目文件夹或WAR包是否存在。查看Tomcat的logs/catalina.out日志文件看是否有部署失败的错误信息。500错误服务器内部错误现象浏览器显示500状态码有时伴有异常信息。排查这是最需要看日志的错误。直接查看Tomcat控制台或logs/localhost.yyyy-MM-dd.log文件。常见原因空指针异常NullPointerException某个对象为null却调用了它的方法。仔细检查从Session或Request中获取的对象是否为null。数据库连接失败检查数据库服务是否启动连接配置用户名、密码、URL是否正确。SQL语法错误将控制台打印的SQL语句复制到数据库客户端执行看是否报错。购物车或Session数据丢失现象登录后过一段时间购物车空了或者需要重新登录。排查Session默认有失效时间如30分钟。检查Tomcat的web.xml或项目web.xml中session-config的session-timeout设置单位是分钟。如果是数据库存储的购物车丢失检查清空购物车的逻辑是否被意外触发。下单时库存超卖现象多人同时抢购同一商品库存减到了负数。排查与解决根本原因是没有处理好高并发下的数据一致性问题。解决方案就是我们上面详细描述的**“事务 悲观锁SELECT ... FOR UPDATE”**。确保在查询库存和扣减库存的整个过程中这条商品记录是被锁定的。这个基于Java的在线购物系统项目虽然用的是比较基础的技术但它涵盖了Web开发中最核心的CRUD操作、会话管理、数据库事务、基础业务逻辑等知识点。把它吃透不仅仅是得到一个能运行的毕业设计更是为自己构建了一个坚实的技术认知框架。当你以后面对更复杂的分布式、微服务系统时你会清楚地知道那些复杂的功能最终都是在解决这些基础问题之上的扩展与优化。动手把它实现一遍遇到问题并解决它这个过程中的收获远比代码本身更有价值。本文还有配套的精品资源点击获取
返回列表