资讯动态

JavaWeb购物商城实战:Servlet+JSP+MySQL完整电商闭环与二次开发

发布时间:2026/10/17 6:57:52 来源:尧图企业网站定制
简介购物商城是典型的JavaWeb实践项目基于JSPServletMySQL实现面向正在学习Java后端开发的学生以及需要快速搭建电商功能模块的开发者。资源包含270个文件核心源码涵盖Commodity、OrderForm、User等实体类与对应的Servlet控制器并配有JSP页面、XML配置文件、CSS样式与JS脚本以及SQL数据文件整包7.08MB便于下载与部署。项目中主要模块包括用户注册登录、商品浏览、购物车管理、订单提交与跟踪以及管理员后台的商品和订单维护较完整地展示了MVC思维下JSP与Servlet的分工协作。对准备期末项目或毕业设计的人群有直接参考价值从环境搭建到数据库设计都有迹可循至今已有1287人浏览学习。1. javaweb购物商城到底能干什么一个练手就能跑完的完整电商闭环A同学上周把这份项目发我说老师给的作业要求是“做一个能买的网站”他翻遍模板不是乱码就是缺表。我帮他跑了一遍这套javaweb购物商城确实是少见的“前后台齐全、能下单、能发货”的完整闭环前台有注册、登录、商品分类、购物车、结算下单后台有商品管理、订单处理和用户管理。技术栈老老实实落在 Servlet JSP MySQL Tomcat 上没有任何花哨框架包着出了问题直接看得到是哪一行在干活。适合三类人准备交 Web 课程作业的学生、刚学完 JavaWeb 想找一个完整项目练手的新手、以及需要一套可以二次开发的基础电商源码的开发者。2. 技术选型与工程结构Servlet JSP MySQL 的分层组合为什么还能打2.1 为什么不用 Spring Boot反而选经典 JavaWeb 三件套现在很多新项目一上来就是 Spring Boot MyBatis-Plus Redis这套组合确实开发效率高但对基础阶段的开发者来说有个致命问题框架把请求处理、事务控制、参数绑定全封装好了你根本看不到 HTTP 请求是怎么被处理的。而这份商城用的 Servlet JSP JDBC MySQL恰好是理解 Web 应用运行原理的最佳教材。Servlet 负责接收 HTTP 请求和响应JSP 负责渲染页面JDBC 负责访问数据库三层之间通过 Service 和 DAO 解耦。它的运行链路非常直白浏览器发请求 → Tomcat 找到对应的 Servlet → Servlet 调 Service → Service 调 DAO → DAO 执行 SQL → 结果反向传回 JSP 渲染。每一步都能在源码里找到对应的类查问题基本靠肉眼就能定位。Maven 管理依赖JDK 1.8Tomcat 8.5MySQL 5.7。前端的 JSP 页面里嵌了 Bootstrap 和 jQuery不需要 node 环境不依赖任何前端构建工具。这个组合对新手最大的好处是你不需要先学会 Spring 全家桶才能开始跑项目。2.2 目录结构逐层拆解每个包是干什么的拿到源码后先别急着跑花十分钟把目录结构过一遍后面改代码会顺很多。标准的 Maven Web 工程结构长这样shop/ ├── pom.xml ├── src/main/ │ ├── java/com/shop/ │ │ ├── dao/ # 数据库访问层每个表对应一个 DAO 接口和实现 │ │ ├── model/ # 实体类与数据库字段一一对应 │ │ ├── service/ # 业务逻辑层处理订单、购物车等核心逻辑 │ │ ├── controller/ # 前台 Servlet处理商城页面的请求 │ │ ├── admin/ # 后台管理 Servlet处理商品和订单管理 │ │ ├── filter/ # 过滤器处理编码、登录校验 │ │ ├── util/ # 工具类如 MD5 加密、数据库连接管理 │ │ └── exception/ # 自定义异常统一业务错误提示 │ ├── resources/ │ │ └── db.properties # 数据库连接配置 │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 注册和过滤器配置 │ │ └── jsp/ # 所有 JSP 页面按前台后台分子目录 │ ├── static/ # CSS、JS、图片资源 │ └── index.jsp # 首页入口controller 包和 admin 包是并列的前台商城和后台管理分别使用独立的 Servlet业务逻辑统一放到 service 层。这样划分的好处是你在改后台商品管理的时候完全不用碰前台的用户逻辑。web.xml 里配置了全局过滤器所有请求先进过滤器再做分发。2.3 数据库设计五张核心表怎么关联数据库是这个项目的根基初始化脚本 shop.sql 在源码根目录下。整个商城围绕五张表展开它们之间的关联关系决定了业务逻辑怎么写。表名核心字段职责userid, username, password, role, phone, address用户注册信息和角色标识role 区分管理员和普通用户categoryid, name, description商品分类首页导航栏的数据来源productid, category_id, name, price, stock, main_image, status商品主表status 字段控制上架下架cart_itemid, user_id, product_id, quantity, checked购物车明细每个商品一条记录ordersid, order_no, user_id, payment, status, create_time订单主表保存总金额和支付状态order_itemid, order_id, product_id, product_name, product_image, current_unit_price, quantity订单明细下单时快照商品信息和价格这里要特别说下 order_item 表。下单的时候不是直接引用 product 表的 id 就结束了而是把商品名称、图片、单价全部复制一份到 order_item 里。这样设计的原因是商品价格可能会调整、商品可能会下架但历史订单里的成交价不能变。如果你在二次开发时发现订单金额和商品现价对不上多半就是这个快照机制在起作用。用户的角色字段 role 需要解释一下。默认注册的用户都是 0普通用户管理员账号是手动在数据库里改 role 为 1 创建的。项目里内置了一个初始化管理员账号登录后台后可以看到订单处理和商品管理菜单。前端页面会根据 session 中的用户角色决定是否显示后台入口这个判断逻辑写在了 index.jsp 的头部。3. 核心模块拆解一个请求从进入数据库到渲染回页面的完整链路3.1 用户登录Session 管理和表单安全处理登录模块是整个项目的入口也是理解这套代码风格最好的起点。用户提交表单后请求会先经过 EncodingFilter 设置字符编码再进入 LoginServlet 的 doPost 方法。看一下核心代码WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); if (username null || username.trim().isEmpty() || password null || password.trim().isEmpty()) { req.setAttribute(errorMsg, 用户名和密码不能为空); req.getRequestDispatcher(/WEB-INF/jsp/login.jsp).forward(req, resp); return; } String encrypted MD5Util.md5(password); User user userService.login(username, encrypted); if (user null) { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/WEB-INF/jsp/login.jsp).forward(req, resp); return; } req.getSession().setAttribute(currentUser, user); resp.sendRedirect(req.getContextPath() /index); } }这段代码的逻辑很清晰第一层校验参数是否为空第二层将明文密码做 MD5 加密后与数据库比对第三层判断用户是否存在。注意 userService.login 返回的是一个 User 对象而不是 true/false这样登录成功后可以直接把完整用户信息塞进 Session后续页面展示昵称头像就不用再查一次库。MD5 加密这一步是很关键的。数据库里存的不是明文密码就算数据库泄露别人拿到的也是一串哈希值。当然 MD5 本身不算安全的加密算法现在更推荐加盐或使用 bcrypt但在这个项目的技术选型下用 MD5 至少做到了“不存明文”这一层。我把登录成功后为什么用 sendRedirect 而不是 forward 讲一下。如果使用 forward浏览器地址栏会停留在 /login用户刷新一下页面就会重新提交一次表单可能产生重复登录或重复下单。重定向会让浏览器重新发送一次 GET 请求到首页彻底避开表单重复提交的问题这是 JavaWeb 开发里非常经典的一个字段。3.2 商品列表分页JDBC 查询的性能细节首页的商品列表不能把数据库里所有商品一次性查出来商品多了页面就会越滚越长而且数据库压力也大。这里用的是传统的 LIMIT 物理分页ProductDao 中对应的方法长这样public ListProduct queryPage(int pageNum, int pageSize) throws SQLException { int offset (pageNum - 1) * pageSize; String sql SELECT id, name, subtitle, main_image, price, stock FROM product WHERE status 1 LIMIT ?, ?; ListProduct list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Product p new Product(); p.setId(rs.getInt(id)); p.setName(rs.getString(name)); p.setSubtitle(rs.getString(subtitle)); p.setMainImage(rs.getString(main_image)); p.setPrice(rs.getBigDecimal(price)); p.setStock(rs.getInt(stock)); list.add(p); } } } return list; }第 2 行的 offset 计算是分页的核心第 1 页的 offset 是 0第 2 页是 pageSize第 3 页是 2 倍 pageSize以此类推。第 3 行 SQL 中 status 1 是只查上架商品下架商品直接过滤掉。第 11 行用了 try-with-resources 语法Connection、PreparedStatement、ResultSet 三个资源用完自动关闭避免连接泄漏。页面底部分页导航条的页码是通过查询总记录数除以 pageSize 计算出来的。总记录数的 SQL 很简单就是SELECT COUNT(*) FROM product WHERE status 1。你也可以把它合并到一个查询里做但代码结构上拆开更清晰分页参数的计算在 IndexServlet 中进行最后以 request attribute 形式传给 index.jsp 渲染。前后端分页逻辑分离的好处是你想换成 PageHelper 或者直接做前端分页只需要改 Servlet 那一层DAO 不用动。3.3 购物车Session 缓存与数据库持久化的取舍购物车模块有一个值得细说的设计决策购物车数据存的是 Session 还是数据库。好多初学项目偷懒把购物车放 Session 里一换浏览器或者清一下缓存购物车就没了这是用户投诉最多的问题。这套商城用的是数据库存储方案也就是登录用户购物车持久化到 cart_item 表。看下加购 Servlet 的处理WebServlet(/cart/add) public class CartAddServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(); User user (User) session.getAttribute(currentUser); if (user null) { resp.sendRedirect(req.getContextPath() /login?returnUrl/cart/add); return; } int productId Integer.parseInt(req.getParameter(productId)); int quantity Integer.parseInt(req.getParameter(quantity)); CartService cartService new CartService(); cartService.add(productId, user.getId(), quantity); resp.sendRedirect(req.getContextPath() /cart); } }CartService.add 内部的逻辑分两种情况如果 cart_item 里已经存在这个用户和这个商品就执行UPDATE cart_item SET quantity quantity ? WHERE user_id ? AND product_id ?如果不存在这条记录才执行 INSERT。这个判断的 SQL 写在 CartDao 里先查一遍再决定插入还是更新。数据库方案也有个缺点每次操作购物车都要访问一次数据库频繁的读写会带来额外压力。项目里做了一些补偿——在展示购物车页面时按 user_id 一次性查出所有购物车项再在 Java 内存中关联商品详情而不是一条购物车记录查一次商品表。这种逐条 CRUD 的写法在数据量小的时候完全够用不建议在这套基础代码上过度优化。如果你真的要改造成 Session 购物车注意登录之后要把 Session 里的临时数据合并到数据库否则用户登录前后购物车不一致这个坑我已经踩过了。4. 交易链路背后的关键实现事务、库存与订单状态流转4.1 订单创建的完整过程一手动事务控制保证数据一致性购物车结算到订单生成是整个项目中最容易出现数据不一致的环节。想象一下用户点击提交订单系统要做四件事——生成订单主记录、生成订单明细、扣减商品库存、清空购物车。如果这四步没有在同一个事务里执行扣库存失败了但订单已经生成用户付款后发现根本发不了货这会直接引发财务纠纷。我在订单创建核心代码里加上了手动事务控制public boolean createOrder(int userId) throws BusinessException { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 查询购物车中勾选的商品 ListCartItem items cartDao.findCheckedByUserId(conn, userId); if (items.isEmpty()) { throw new BusinessException(没有选中任何商品); } // 2. 生成订单主记录并计算总金额 String orderNo ORD System.currentTimeMillis(); BigDecimal totalPrice BigDecimal.ZERO; for (CartItem item : items) { Product product productDao.findById(conn, item.getProductId()); if (product.getStock() item.getQuantity()) { throw new BusinessException(商品库存不足: product.getName()); } // 3. 扣减库存 productDao.decreaseStock(conn, product.getId(), item.getQuantity()); // 4. 写入订单明细保存商品快照 orderDao.insertOrderItem(conn, orderNo, product, item); BigDecimal itemTotal product.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity())); totalPrice totalPrice.add(itemTotal); } // 5. 生成订单主记录 orderDao.insertOrder(conn, orderNo, userId, totalPrice); // 6. 清空该用户的购物车 cartDao.clearByUserId(conn, userId); conn.commit(); return true; } catch (Exception e) { conn.rollback(); throw new RuntimeException(e); } finally { DBUtil.close(conn); } }第 4 行的setAutoCommit(false)是 JDBC 事务的基石。MySQL 的默认隔离级别下如果不开事务每条 SQL 执行完立即提交一旦某条语句失败前面的操作就落库了。这里把整个下单流程包在一个 Connection 里第 28 行 commit 之前任何一步抛异常第 31 行 rollback 会把所有已执行的 SQL 全部撤销。事务边界值得留意不是所有数据库操作都要加事务只有涉及到“多表写入且必须保持一致性”的场景才需要。单纯查购物车列表就不需要事务。如果你后续改造这个模块比如加入优惠券功能记住把“计算优惠金额”的步驟也包进同一个事务而且要在扣库存之前计算完成否则订单金额和优惠券核销不在一个事务里对账就对不上。关于订单号的生成项目用了时间戳拼接方式并发高时可能撞单二次开发可以改成 UUID 或雪花算法。4.2 后台商品和订单管理Admin Servlet 的分发设计后台管理不单独建一套框架而是用 AdminProductServlet 和 AdminOrderServlet 两个核心 Servlet 承接所有后台请求。每个 Servlet 通过 action 参数分发不同的方法路由规则是这样设计的WebServlet(/admin/product) public class AdminProductServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (list.equals(action)) { listProducts(req, resp); } else if (edit.equals(action)) { showEditPage(req, resp); } else if (delete.equals(action)) { deleteProduct(req, resp); } else { resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } }商品的新增和编辑用的都是 doPost因为这个操作会修改数据。这里的 delete 做的是逻辑删除即把 product.status 改成 0而不是真正从数据库里 DELETE 掉。这样保留商品记录的好处是历史订单的明细不会因为商品删除而关联不上缺点是被删商品仍然占着数据库空间数据量大了需要定期归档。后台的订单管理逻辑也简单管理员登录后台后看到所有的订单列表可以对订单执行“发货”操作将 status 从 1已支付改为 2已发货。后台和前台的权限控制用同一个 AuthFilter 做拦截。web.xml 里为 /admin 路径单独配置了权限校验过滤器Session 中的用户 role 不为 1 直接重定向到登录页。这里给二次开发提一个建议把 role 的判断抽到一个公共工具方法里因为你会发现前台的“我的订单”和后台的“订单管理”都会判断用户角色抽成isAdmin(User user)可以少写很多重复代码。4.3 两个核心过滤器编码处理和登录拦截web.xml 里配置了项目中最重要的两个过滤器。第一个是编码过滤器filter filter-nameencodingFilter/filter-name filter-classcom.shop.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingforceEncoding 会同时作用于 request 和 response这样不论是表单提交的 POST 参数还是 JSP 输出的中文字符都能统一为 UTF-8。很多新手遇到页面显示乱码第一反应是 HTML 加 meta 标签实际上服务器响应的编码才是根因。第二个是登录拦截过滤器它对三种资源做了白名单放行登录和注册请求、静态资源文件、首页和商品详情页。其余请求必须校验 Session 中是否有 currentUser没有就重写 URL 拼上 returnUrl登录成功后跳回原页面。returnUrl 这个参数很容易被忽视但它对用户体验的提升非常明显——用户未登录时点击了“购物车”登录完成后应该回到购物车而不是回到首页这个小功能在二次开发时要特别注意拼接的 URL 是完整路径还是相对路径用 req.getRequestURI() 拿到的只是路径名要拼上 contextPath 才是完整可跳转的地址。5. 运行避坑与排查从导入源码到看到首页的完整跑通指南5.1 环境准备与项目导入步骤这份项目对运行环境有硬性要求。JDK 必须是 1.8 或 11Tomcat 建议用 8.5 或 9.0MySQL 用 5.7 或 8.0。特别要注意不要用 Tomcat 10 及以上版本后面我会讲为什么。开发工具用 IDEA 或 Eclipse 都行项目是以 Maven 工程组织的导入时选 Maven 类型不要选普通 Java 项目否则依赖不会自动下载。导入后第一步不是急着写代码而是等 Maven 把 pom.xml 里的依赖下载完看到项目里 External Libraries 出现 servlet-api、jstl、mysql-connector-java 这些依赖后再开始配置 Tomcat。配置的时候记得在本机先装一个 Tomcat然后在 IDEA 里配置 Tomcat ServerDeployment 中把 shop 这个 artifact 部署上去。5.2 数据库初始化和连接配置MySQL 里建一个名为 shop 的数据库字符集选 utf8mb4然后执行源码根目录下的 shop.sql。文件里包含了建表和默认数据部分分类和商品数据内置在里面有 6 个分类和几十条商品记录跑起来首页不会空。数据库连接配置在 resources 目录下的 db.propertiesjdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456这里要注意 jdbc.url 里那几个参数不要删——characterEncoding 设置了客户端编码为 UTF-8避免存入数据库乱码serverTimezone 指定了时区MySQL 8.0 如果没加这个参数会直接报 CST 时区异常useSSLfalse 是关闭 SSL 握手本地开发提速。如果你的 MySQL 密码不是 123456改这里即可。如果数据库是 8.0 以上版本需要把驱动换成com.mysql.cj.jdbc.Driver否则会报 ClassNotFoundException。5.3 常见问题3 个运行阶段的踩坑记录问题一项目启动时报 NoClassDefFoundError: javax/servlet/HttpServlet现象Tomcat 启动时一片红控制台提示找不到 javax.servlet 相关类。原因pom.xml 中 servlet-api 依赖没有声明 scope 为 provided导致它被一起打包进了 lib 目录和 Tomcat 自带的 servlet-api 冲突。解决检查 pom.xml把 servlet-api 和 jsp-api 的 scope 设为 providedMaven 打包时会避开这个依赖冲突。问题二首页商品图片全部裂开显示小图标现象JSP 页面布局正常但商品图片全部加载不出来。原因项目里的商品图片使用相对路径upload/xxx.jpg而 HTML 页面中的相对路径是相对于当前的 URL 路径解析的如果你的站点不是部署在根路径下图片就会找错位置。解决改造 image 的 src 使用${pageContext.request.contextPath}/upload/xxx.jpg这种方式拼绝对路径或者在 web.xml 里配置一个虚拟目录映射到本机图片存储目录。这个项目默认使用了前者。问题三填写注册信息后一直 500查日志发现表不存在现象点击注册按钮报 500 错误MySQL 控制台显示Table shop.user doesnt exist。原因数据库里建的库名和表名与实际执行的 SQL 不一致或者是大小写敏感导致表名匹配不上。Linux 环境下 MySQL 表名是大小写敏感的而 Windows 下不敏感你在 Windows 上建了个User表后面导入 Linux 就容易出问题。解决统一用小写表名并在建库时设置lower_case_table_names1让 MySQL 不区分大小写重新执行一遍 shop.sql。6. 二次开发实战给商城加上关键词搜索的完整改造过程6.1 需求拆解搜索功能要动哪几层给你的商城加一个搜索框导航栏和商品列表页各加一个入口。这个需求听上去不大但要改的东西贯穿全链路JSP 页面加表单、新建 SearchServlet 接收参数、ProductDao 加模糊查询方法、index.jsp 上展示搜索结果。整个改造逻辑和现有的商品分页很像完全可以复用 ProductDao.queryPage 那套分页逻辑把 SQL 的 WHERE 条件从只过滤 status 扩展成同时过滤商品名称。6.2 代码改造从 DAO 到 Servlet 到 JSP 的完整改动ProductDao 中新增一个带关键词的分页查询方法public ListProduct searchByKeyword(String keyword, int pageNum, int pageSize) throws SQLException { int offset (pageNum - 1) * pageSize; String sql SELECT id, name, subtitle, main_image, price, stock FROM product WHERE status 1 AND name LIKE ? LIMIT ?, ?; ListProduct list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setInt(2, offset); ps.setInt(3, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Product p new Product(); p.setId(rs.getInt(id)); p.setName(rs.getString(name)); p.setSubtitle(rs.getString(subtitle)); p.setMainImage(rs.getString(main_image)); p.setPrice(rs.getBigDecimal(price)); list.add(p); } } } return list; }第 6 行的LIKE ?使用了参数绑定而不是字符串拼接这是防 SQL 注入的关键。第 7 行把关键词拼接成%keyword%实现模糊匹配。如果你拼的是keyword%只能匹配以关键词开头的商品名。搜索页的 Servlet 拿到 keyword 参数后做一次 trim如果为空就直接重定向到全部商品列表避免用户提交空的搜索词出现全表扫描。前端导航栏的搜索框提交 GET 请求到/search?keywordxxx搜索结果的展示复用商品列表的 JSP 片段只是把标题改成“搜索关键词 xxx 的结果”。需要考虑的边界情况是搜索不到商品时不要显示空页面要给出“没有找到相关商品”的提示和一个返回首页的链接。验证的时候把关键词换成特殊字符script如果页面原样显示而不是弹对话框说明你做对了转义。从那以后我每次给商城加功能都强制走一遍“正常值、边界值、非法值”三种输入的手工冒烟测试顺手把 Tomcat 的 localhost 日志开着出问题先看 catalina.out再翻浏览器控制台。这套项目跑熟之后你再去看 Spring Boot 的电商项目会发现无非是换了个壳思路一模一样希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑