JavaWeb商城源码拆解:分层架构、购物车与订单事务实战

发布时间:2026/10/28 20:52:17
JavaWeb商城源码拆解:分层架构、购物车与订单事务实战

简介:这份资源是面向JavaWeb初学者与课程实践者的网上商城系统实例,以华为商城为业务原型,帮助读者理解从用户注册登录、商品浏览到购物车与订单处理的完整电商流程,适合作为毕业设计、课程作业或框架入门后的综合练手项目。压缩包共2个文件,包含1个rar源码包与1个sql数据库脚本,前者提供Java类、JSP页面、web.xml配置及依赖jar等Web应用结构,后者用于快速初始化商品、用户与订单等数据表,整体约58.24MB。目前已有962人学习下载,具备一定的参考基础。项目将Servlet请求处理、JSP动态页面生成与MySQL数据管理串联起来,读者可通过解压运行与导入脚本,直观体会分层结构、数据库交互和页面渲染的协作方式,并借助JSTL标签库理解页面逻辑的简化写法,从而积累可复用的调试与改造经验。

1. 从一份 JavaWeb 商城压缩包说起:它能帮你补齐哪些工程能力

打开这份javaweb华为商城.zip,第一眼看到的不是某个炫技框架,而是一套典型的 JavaWeb 分层商城工程:商品浏览、分类展示、购物车、订单、后台管理这些电商基础链路都在里面。它适合两类人:一类是刚学完 Servlet、JSP、JDBC,想找一个能跑起来的完整项目练手的新手;另一类是想回头看看「不用 Spring Boot 全家桶,纯手写分层」到底怎么组织代码的熟手。电商项目最容易被写成流水账,但真正拆开看,商品表怎么设计、购物车放 Session 还是放数据库、订单状态怎么流转,这些才是能带走的东西。下面按「先看清结构、再跑起来、最后动手改」的顺序,把这份源码拆开讲。

2. 工程结构与技术选型:一个请求在分层里怎么走

2.1 目录骨架与各层职责

拿到压缩包先别急着导 IDE,先在文件管理器里把目录过一遍。这类 JavaWeb 商城通常是标准 Maven Web 工程,骨架大致如下(不同版本细节会有出入,以实际解压为准):

javaweb-huawei-mall/ ├── pom.xml # Maven 依赖与打包配置 ├── src/main/java/ │ ├── controller/ # Servlet 层,接收请求、调 Service、转发 JSP │ ├── service/ # 业务层接口 │ │ └── impl/ # 业务实现,事务边界一般在这层 │ ├── dao/ # 数据访问层接口 │ │ └── impl/ # JDBC 实现,拼 SQL、映射 ResultSet │ ├── entity/ # 实体类,与数据库表一一对应 │ └── util/ # 数据库连接、分页、字符串处理等工具 ├── src/main/resources/ │ └── db.properties # 数据库连接参数 └── src/main/webapp/ ├── WEB-INF/ │ ├── web.xml # Servlet 映射、过滤器、监听器配置 │ └── views/ # JSP 页面,放在 WEB-INF 下防止直接访问 ├── css/ js/ images/ # 静态资源 └── index.jsp # 首页入口

这个结构的关键在于「请求只走一条线」:浏览器打到某个 Servlet,Servlet 只做参数校验和结果转发,业务判断交给 Service,SQL 全部收在 DAO。新手最容易犯的错是把 SQL 写进 Servlet,或者把业务判断塞进 JSP,项目一旦变大就彻底失控。看这份源码时,建议顺着一个具体功能(比如「加入购物车」)从 Controller 一路读到 DAO,把调用链在纸上画一遍,比通读所有类有效得多。

2.2 技术栈与依赖清单

从标题和工程形态判断,这套项目用的是经典 JavaWeb 组合,没有引入 Spring 体系。常见依赖如下表,具体版本以pom.xml为准:

技术作用在项目中的位置
Servlet / JSP请求处理与页面渲染controller 层、webapp 下 JSP
JDBC数据库访问dao/impl 下各实现类
MySQL数据存储商品、用户、订单、购物车表
JSTL / ELJSP 页面循环与取值JSP 中的<c:forEach>、${}
Filter / Listener编码过滤、登录拦截、连接池初始化web.xml 或注解配置
Maven依赖管理与 war 打包pom.xml

选这套组合而不是 Spring Boot,好处是每一层都「看得见」:没有自动配置帮你藏东西,连接怎么拿、事务怎么开、页面怎么转发,全在代码里。对想搞懂 Web 请求生命周期的开发者来说,这种「裸奔」反而是优势。代价是样板代码多,改一个字段要动实体、DAO、Service、Servlet、JSP 五处,这也是后面二次开发要重点优化的地方。

2.3 一个请求的完整链路

以「商品详情页」为例,链路是这样的:浏览器请求/product/detail?id=12,ProductServlet的doGet拿到 id,调ProductService.getById(12),Service 调ProductDao.findById(12),DAO 从连接池取连接、执行select * from product where id = ?、把ResultSet映射成Product实体返回,Servlet 把实体塞进request.setAttribute("product", p),再forward到WEB-INF/views/product/detail.jsp,JSP 用 EL 表达式渲染。整条链路里,Servlet 不碰 SQL,DAO 不碰HttpServletRequest,职责边界清晰。

提示:读源码时优先看web.xml里的<url-pattern>,它决定了每个 Servlet 负责哪段 URL,是理解路由的最快入口。

3. 本地跑通:数据库、依赖、容器三步走

3.1 环境准备与数据库导入

跑这类项目,翻车点八成在环境和数据库上。建议按下面顺序来,别跳步:

  1. 装 JDK 8(这类老项目对高版本 JDK 兼容性一般,先用 8 稳),装 Maven 3.6+,装 MySQL 5.7 或 8.0。
  2. 解压源码,用 IDEA 以 Maven 项目方式打开,等依赖下载完。
  3. 在 MySQL 里建库,比如create database mall default charset utf8mb4;,再导入项目里的.sql脚本(一般在src/main/resources或根目录的sql/下)。
  4. 改db.properties里的连接参数,和本地一致。
# src/main/resources/db.properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的本地密码

这里几个参数值得说清楚:characterEncoding=utf8防止中文商品名乱码;serverTimezone在 MySQL 8 下不加会报时区错误,这是血泪经验;驱动类名 MySQL 8 用com.mysql.cj.jdbc.Driver,5.x 用com.mysql.jdbc.Driver,写错直接ClassNotFoundException。如果项目用的是连接池(常见是 Druid 或 C3DBCP),参数名会略有不同,按实际配置文件改即可。

3.2 依赖与打包配置

pom.xml里重点看三块:packaging是不是war、Servlet/JSP 依赖的scope是不是provided、MySQL 驱动版本和本地数据库是否匹配。

<packaging>war</packaging> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency>

provided的意思是「容器会提供,不打进 war」,Tomcat 自带 Servlet API,如果打成compile会出现类冲突,启动时报ClassCastException或方法找不到。MySQL 驱动版本要和数据库大版本对齐,8.0 的库配 5.x 驱动,连接阶段就可能握手失败。依赖下载慢的话,配个国内镜像仓库即可,这是常规操作。

3.3 容器部署与首次访问

打包和部署有两条路,选一条走通就行:

# 方式一:Maven 打 war 包,丢进 Tomcat mvn clean package -DskipTests cp target/*.war $TOMCAT_HOME/webapps/mall.war $TOMCAT_HOME/bin/startup.sh # 方式二:IDEA 里配 Tomcat,直接部署 exploded war # Run/Debug Configurations -> Tomcat Server -> Local # Deployment 里加 Artifact,Application context 设为 /mall

启动后访问http://localhost:8080/mall/,能看到首页商品列表就算通了。如果 404,先看 war 包名和 context path 是否一致;如果 500,看 Tomcat 日志里第一个Caused by,通常是数据库连不上或 SQL 脚本没导全。后台管理入口一般在/admin/login之类路径,账号密码看 SQL 脚本里的用户表初始数据。

注意:JSP 页面放在WEB-INF/views下是故意的,浏览器无法直接访问,只能通过 Servlet forward 进入,这是这类项目的安全习惯,别为了调试把它挪到外面。

4. 核心模块拆解:商品、购物车、订单三段代码

4.1 商品列表与分页查询

商品列表是商城的第一屏,也是分页逻辑最集中的地方。DAO 层常见写法是「一次查总数、一次查当前页」:

// ProductDaoImpl.java public List<Product> findByPage(int pageNo, int pageSize, Integer categoryId) { List<Product> list = new ArrayList<>(); StringBuilder sql = new StringBuilder( "select id, name, price, stock, image, category_id from product where 1=1 "); if (categoryId != null) { sql.append("and category_id = ? "); // 分类筛选,可选条件 } sql.append("order by id desc limit ?, ?"); // 分页:偏移量 + 条数 try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { int idx = 1; if (categoryId != null) { ps.setInt(idx++, categoryId); } ps.setInt(idx++, (pageNo - 1) * pageSize); // 偏移量从 0 开始 ps.setInt(idx, pageSize); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); // 行映射抽成独立方法 } } } catch (SQLException e) { throw new RuntimeException("查询商品分页失败", e); } return list; }

这段代码有三个点值得抄:一是用StringBuilder拼可选条件,避免写多条 SQL;二是limit ?, ?用占位符而不是字符串拼接,防注入;三是try-with-resources保证连接和语句一定关闭。pageNo从 1 开始,偏移量算成(pageNo - 1) * pageSize,这个减一忘了就是经典 off-by-one。总数查询单独一个countByCondition方法,Service 层把总数和当前页数据一起封装成分页对象返回给 Servlet。

4.2 购物车:Session 还是数据库

购物车是这类项目里最能看出设计取舍的模块。常见做法是未登录时放Session,登录后合并进数据库表。Session 版实现简单:

// CartServlet.java 片段 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int productId = Integer.parseInt(req.getParameter("productId")); int num = Integer.parseInt(req.getParameter("num")); HttpSession session = req.getSession(); Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new LinkedHashMap<>(); // 保持加入顺序 session.setAttribute("cart", cart); } CartItem item = cart.get(productId); if (item == null) { Product p = productService.getById(productId); cart.put(productId, new CartItem(p, num)); } else { item.setNum(item.getNum() + num); // 已存在则累加数量 } resp.sendRedirect(req.getContextPath() + "/cart/list"); }

LinkedHashMap保证购物车条目按加入顺序展示,用HashMap顺序会乱,用户会觉得「东西自己跳了」。数量累加前要校验库存,否则能加到 999 件。登录后合并的逻辑一般在登录成功的 Servlet 里做:遍历 Session 购物车,逐条insert or update到cart表,然后清空 Session。这里有个坑——合并时如果数据库里已有同一商品,要做数量相加而不是覆盖,否则用户登录前加的东西会「消失」。

4.3 下单流程与事务边界

下单是唯一必须保证原子性的地方:扣库存、写订单主表、写订单明细,三步要么全成要么全败。Service 层用 JDBC 手动控制事务:

// OrderServiceImpl.java public boolean createOrder(int userId, Map<Integer, CartItem> cart) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 for (CartItem item : cart.values()) { int affected = productDao.reduceStock(conn, item.getProductId(), item.getNum()); if (affected == 0) { // 库存不足,回滚 conn.rollback(); return false; } } int orderId = orderDao.insertOrder(conn, userId, cart); // 主表 orderDao.insertItems(conn, orderId, cart); // 明细 conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new RuntimeException("下单失败", e); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } } }

关键在两点:所有 DAO 方法都接收同一个Connection参数,保证在同一个事务里;扣库存用update product set stock = stock - ? where id = ? and stock >= ?,靠where条件做乐观校验,返回影响行数为 0 就说明库存不够。这种写法比「先查再改」安全,并发下不会超卖。事务提交后记得恢复autoCommit再关连接,否则连接池回收后会带着错误状态给下一个请求。

5. 避坑与排查:跑不起来时先看这几处

5.1 中文乱码:现象、原因、解决

现象:商品名、分类名在页面上显示成问号或方块。原因通常是三处编码不一致——数据库表字符集、JDBC URL 的characterEncoding、请求/响应的setCharacterEncoding。解决:建库时用utf8mb4,URL 带上characterEncoding=utf8,并在过滤器里统一request.setCharacterEncoding("UTF-8")和response.setContentType("text/html;charset=UTF-8")。POST 乱码靠过滤器,GET 乱码在 Tomcat 8 以后默认 UTF-8,老版本要在server.xml的 Connector 上加URIEncoding="UTF-8"。

5.2 数据库连接失败:现象、原因、解决

现象:启动后访问任意页面报 500,日志里是Communications link failure或Access denied。原因无非三种:MySQL 没启动、账号密码错、驱动版本与数据库不匹配。解决:先用命令行mysql -uroot -p确认能连上,再核对db.properties的库名和端口,最后看pom.xml里驱动版本。MySQL 8 还要注意useSSL=false和时区参数,缺了会连不上或报时区异常。

5.3 静态资源 404:现象、原因、解决

现象:页面能打开但样式全丢,F12 里 css/js 全是 404。原因多半是 JSP 里引用路径写成了绝对路径/css/style.css,而项目部署在/mall这个 context 下。解决:统一用${pageContext.request.contextPath}/css/style.css,或者在 JSP 顶部用<c:set var="ctx" value="${pageContext.request.contextPath}"/>然后${ctx}/css/...。这类问题在改 context path 后必现,属于部署环境一变就翻车的典型。

5.4 登录拦截失效:现象、原因、解决

现象:没登录也能直接访问订单页或后台页。原因通常是拦截逻辑写在每个 Servlet 里,漏了一两个;或者 Filter 的url-pattern配得太窄。解决:用一个LoginFilter拦截/*,在doFilter里判断session.getAttribute("user")是否为空,为空就重定向到登录页,同时放行登录、注册、静态资源等白名单路径。白名单用startsWith判断,别用equals,否则带参数的 URL 会漏过去。

5.5 订单重复提交:现象、原因、解决

现象:用户手快点了两次「提交订单」,生成两笔订单、扣两次库存。原因是没有防重机制。解决:常见做法是下单页生成一个一次性 token 放 Session,提交时校验并立即删除;或者下单成功后redirect到订单详情页而不是forward,避免刷新重复提交。前者更严谨,后者成本低,按项目体量选。

6. 二次开发:从能跑到能改的几个落点

把项目跑起来只是开始,真正有价值的是知道往哪改。第一个落点是把 JDBC 收口:现在每个 DAO 实现里都在手写连接获取和关闭,可以抽一个BaseDao模板类,把getConnection、close、mapRow的公共部分收进去,子类只写 SQL 和映射,改一处全局生效。第二个落点是引入轻量 ORM 或至少用连接池:Druid 或 HikariCP 替换手写连接管理,配置几行就能拿到监控和连接复用,比每次DriverManager.getConnection稳得多。

第三个落点是给 Service 层加接口与实现分离的规范:现在如果 Service 直接是类,测试和替换都麻烦,抽成interface + impl后,后续想换实现(比如加缓存)不用动 Servlet。第四个落点是分页对象统一:商品、订单、用户列表各写一套分页参数很常见,抽一个PageResult<T>泛型类,字段就list、total、pageNo、pageSize,所有列表页复用,前端 JSP 也能用同一段分页标签。

改完怎么验证?我的习惯是每改一层就跑一遍「商品列表 → 详情 → 加购 → 下单 → 后台查订单」这条主链路,再补一个并发下单的小脚本(比如用ab或简单多线程请求同一商品),看库存会不会扣成负数。下面这个片段可以用来快速验证扣库存的原子性:

// 并发验证:10 个线程同时下单同一商品,库存初始为 5 ExecutorService pool = Executors.newFixedThreadPool(10); CountDownLatch latch = new CountDownLatch(10); AtomicInteger success = new AtomicInteger(); for (int i = 0; i < 10; i++) { pool.submit(() -> { try { if (orderService.createOrder(userId, cartWithOneItem)) { success.incrementAndGet(); // 成功下单数不应超过库存 } } finally { latch.countDown(); } }); } latch.await(); System.out.println("成功下单数:" + success.get()); // 期望 <= 5

如果成功数大于库存,说明扣库存的where stock >= ?条件被绕过了,或者事务没生效,回去查Connection是不是每个 DAO 各拿各的。这种验证花不了十分钟,但能挡住上线后最难看的那种 bug。

最后说个教训:我早期改这类老项目,总想一口气把分层重构完再跑,结果改到一半连首页都打不开,回滚都找不到干净版本。从那以后我每次动结构前都先打一个能跑的 tag,改一层验证一层,主链路走通再往下走。这份商城源码的价值不在于它多先进,而在于它把 Web 开发的每一层都摊开给你看,改它的过程就是补工程直觉的过程。希望帮到你。

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