JavaWeb购物商城项目实战:从Servlet到MySQL事务与部署 简介这套JavaWeb购物商城项目面向已经系统学习过Servlet、JSP、JDBC与MySQL的开发者是检验JavaWeb综合能力的实战练手项目也可供高校学生完成课程设计或毕业设计时参考。压缩包共包含613个文件整体大小16.85MB除完整Java源码和JSP页面外还提供class编译文件、jar依赖库、SQL数据库初始化脚本、JS与CSS前端资源以及图片素材项目导入开发工具并执行数据库脚本后即可运行。项目采用MVC设计模式与动态代理模式前台完整实现了主页热销商品、商品搜索、商品详情与库存校验、购物车增减和勾选、地址管理、立即购买与订单提交等流程并对重复提交、库存不足和商品下架等异常做了响应处理后台则包含会员管理、商品批量添加/上下架、库存维护与订单发货等模块业务流程闭环完整。目前已有21946人学习下载整体代码结构清晰、目录分层合理读者可通过阅读源码理解主流JavaWeb分层思想并在此基础上扩展自己的功能模块。1. JavaWeb 购物商城项目到底在练什么十个简历里八个挂着商城项目但能把登录、购物车、下单、扣库存这条链路当场讲清楚的不超过两成。一个完整的 JavaWeb 购物商城项目练的不是 CRUD 拼装而是 Servlet 生命周期、Session 状态管理、JDBC 事务边界和 MySQL 表设计同时生效的那几个瞬间。网上下到的“完整源码MySQL 数据库”压缩包很多能不能在本地跑起来、敢不敢在答辩现场演示是两码事。与其按压缩包目录去猜不如按一套能跑通、能答辩、之后能平滑迁到 Spring Boot 的工程思路把数据库建模、核心代码路径、Tomcat 部署和排错逐段拆开。适合正在做课程设计的 Java 学习者也适合带新人的老手对照查漏。2. JavaWeb 购物商城的架构选型Servlet、JSP 与三层边界2.1 JavaWeb 教学案例为什么坚持 Servlet JSP选 Servlet JSP 而不是直接上 Spring Boot是这个项目类型最容易被问倒的第一个点。教学案例里这么写不是因为技术落后而是因为 Spring Boot 把内嵌 Tomcat、DispatcherServlet 初始化、请求参数绑定全部自动化之后新手根本没有机会感知 HTTP 请求进入 Tomcat 之后发生了什么。等到面试官问“Servlet 生命周期有几个阶段”“Filter 和 Interceptor 的区别”“Session 什么时候创建、什么时候失效”只写过 Spring Boot 的人很容易卡壳。这个阶段把 Servlet、Filter、JSP 手写一遍后面再去看 Spring MVC 的 DispatcherServlet 源码会发现很多设计动机其实都是对原生 Servlet 的包装。常见做法是保留三层结构但不过度设计web 层只放 Servlet 做参数接收、类型转换和页面跳转service 层放业务规则和事务边界dao 层只做 JDBC 读写。这个阶段别急着引入 MyBatis 或 Spring否则“源码”就失去了教学意义排查问题的路径也会变长。2.2 三层包结构与 JavaWeb 请求流转路径一个结构干净的完整源码包目录应当一眼能看出分层。以 com.mall 为基准包常见划分如下mall/ ├── src/main/java/com/mall │ ├── entity/ # 与表一一对应的实体类 │ ├── dao/ # 每个实体一个 Dao只写 SQL │ ├── service/ # 组合多个 Dao控制事务边界 │ ├── web/ # 所有 Servlet统一暴露 /mall/* 风格路径 │ ├── filter/ # EncodingFilter、AuthFilter │ └── util/DBUtil.java # 连接管理 ├── src/main/webapp │ ├── jsp/ # 页面统一放 WEB-INF 下禁止直接 URL 访问 │ ├── static/ # css、js、images │ └── WEB-INF/web.xml └── pom.xml请求流转是固定的一条线浏览器请求 /mall/product/listProductServlet 收到后调用 ProductService.list()Service 内部再调 ProductDao.selectList()结果封装成 List 放进 request.setAttribute最后 forward 到 productList.jsp。这条链路里最容易写歪的地方是把 SQL 拼进 Servlet或者把业务判断写进 JSP 的 scriptlet。前者让源码失去分层意义后者让页面难以维护。forward 和 redirect 的选择也值得统一查询结果展示用 forward保持一次请求内的属性传递登录成功、下单成功后必须 redirect防止用户刷新页面重复提交订单。这个细节在答辩时主动讲出来比背十个设计模式都有说服力。2.3 JavaWeb 项目的 Maven 依赖与导包取舍依赖管理上建议直接用 Maven少手动拷 jar。虽然课程设计里常见“手动导入 lib 目录”的姿势但 servlet-api 的作用域、mysql 驱动的版本传递问题Maven 能提前帮你暴露。一个最小可跑的 pom.xml 是这样properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdjstl/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependenciesservlet-api 的 scope 必须写 provided因为 Tomcat 自带 Servlet 实现打进 war 会冲突。mysql 驱动版本和本机 MySQL 主版本对齐8.0.x 驱动能连 5.7 服务端反过来老驱动连 MySQL 8.0 会直接报认证错误。两种依赖管理方式的取舍边界如下| 方式 | 适用场景 | 主要成本 | | 手动拷 jar 到 WEB-INF/lib | 演示机无网、评估环境不给配 Maven | 容易漏 servlet-api 造成与 Tomcat 冲突jar 进 Git 后体积难控制 | | Maven 管理依赖 | 自己开发机、后续要接插件和持续构建 | 首次拉依赖耗时需要配镜像加速 |如果拿到的“完整源码”没有 pom.xml先看 WEB-INF/lib 里有哪些 jar再决定是补齐 lib 继续跑还是建一个 Maven 工程把代码迁进去。只要源码是三层结构迁移成本很低如果代码全是 JSP scriptlet 拼 SQL手工迁一次比在烂结构上改半天更省时间。3. MySQL 数据库设计从商品表到订单状态机3.1 MySQL 五张核心表的字段与约束设计商城数据库最常见的设计是五张表用户表、分类表、商品表、订单表、订单明细表。字段不在多在准下面这张表基本覆盖了大多数完整源码里的标准答案| 表名 | 核心字段 | 设计要点 | | user | id 自增主键、username 唯一、password、nickname、create_time | username 建唯一索引password 存摘要不存明文 | | category | id、name、parent_id、sort | 两级分类用 parent_id 自关联sort 控制展示顺序 | | product | id、category_id、name、subtitle、main_image、price、stock、status | price 用 decimal(10,2)stock 加非负约束 | | orders | id、order_no 唯一、user_id、total_amount、pay_status、create_time | order_no 用 varchar(32) 建唯一索引 | | order_item | id、order_id、product_id、product_name、current_unit_price、quantity、total_price | 冗余商品名和价格快照防止商品改价后历史订单失真 |price 用 decimal(10,2) 而不用 float 或 double是初学者最容易忽略的点。二进制浮点在计算 0.10.2 这类金额时会产生尾差单笔看着没问题订单总额多次累加后误差会流进对账环节。decimal 是定点数按字符串存储累加不丢精度代价是空间占用比 float 大商城场景完全可接受。3.2 订单状态机与列表排序的 SQL 写法orders 表里的 pay_status 是这个项目逻辑最密集的字段。一套够用的状态定义是0 待付款、1 已付款、2 已发货、3 已完成、-1 已取消。状态迁移只能按固定方向走待付款可以取消已付款才能发货已发货才能完成取消只允许发生在待付款状态。在 MySQL 里约束状态迁移常见做法是 TINYINT 加应用层判断而不是数据库触发器。因为下单、支付回调、取消订单分布在多个 Service 方法里触发器会让排错困难。应用层每次更新前先查当前状态不符合迁移方向就抛业务异常日志里能看到完整调用栈。订单列表页查询固定用 ORDER BY create_time DESC配合 create_time 上的索引否则数据量上来之后分页会越来越慢。3.3 初始化 SQLutf8mb4 与索引一次到位拿到源码里的 mall.sql先别急着 source 导入看三件事建库语句有没有指定 DEFAULT CHARACTER SET、表引擎是不是 InnoDB、主键是不是自增。很多号称完整的脚本 utf8 和 utf8mb4 混用导入 MySQL 8.0 之后 emoji 字符直接变问号。一份可以落地的初始化脚本片段CREATE DATABASE mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE mall; CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password VARCHAR(64) NOT NULL COMMENT MD5 摘要不存明文, nickname VARCHAR(32) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;CREATE DATABASE 把 utf8mb4 和排序规则一次定死后面建表不写 CHARSET 也会继承库级默认。utf8mb4 是 utf8 的超集能存四字节字符MySQL 8.0 的默认也是它。password 定成 VARCHAR(64) 是因为 MD5 摘要固定 32 位十六进制将来换 SHA-256 也是 64 位无需改表。uk_username 既是唯一约束也是登录查询的索引登录按 username 精确匹配时直接走索引。商品表要在 category_id 上建普通索引否则按分类浏览会全表扫描。订单列表页不要一次性 JOIN 五张表用单表查询加 Service 层组装SQL 更容易命中索引排查问题也更直观。4. JavaWeb 核心实现登录鉴权、Session 购物车与事务下单4.1 编码 Filter 与登录鉴权的三条规则完整源码里常见两种风格一种把 request.setCharacterEncoding 写在每个 Servlet 第一行另一种靠 web.xml 配置过滤器。前者重复代码多后者在 Servlet 3.0 之后可以直接用注解。用注解注册一个所有请求必经的编码过滤器WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; request.setCharacterEncoding(UTF-8); HttpServletResponse response (HttpServletResponse) resp; response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }WebFilter(/*) 拦截所有路径POST 表单的中文参数在 getParameter 之前被转成 UTF-8。GET 请求的 URL 编码不归这个方法管Tomcat 8 之后 URL 默认 UTF-8本地开发基本不用额外配置。response 统一设置 ContentType避免 JSP 页面忘记写 pageEncoding 导致标题乱码。登录鉴权的标准写法是登录成功后把 user 对象放进 Session再写一个 AuthFilter 拦截 /mall/order/、/mall/cart/等需要登录的路径。判断逻辑就三行session.getAttribute(loginUser) 为 null 就 redirect 到 login.jsp否则放行。注意 Session 里不要存密码字段JSP 页面如果直接用 EL 输出 user.password等于把摘要暴露给任何能打开页面的人。提示登录成功之后先 session.invalidate() 再写入新 Session可以防 Session Fixation 攻击。这个点作为 javaweb 教学案例的加分项面试提到会明显拉高印象分。4.2 Session 购物车的存取与数量边界购物车放 Session 还是放表是这个项目里少有的“没有标准答案”的设计点。纯 Session 实现简单适合未登录用户加购数据库购物车表能跨设备同步但要处理合并逻辑。课程设计级别的建议是 Session 加 Map先把主链路保住public class Cart { private final MapInteger, Integer items new LinkedHashMap(); public void add(Integer productId, Integer quantity) { items.merge(productId, quantity, Integer::sum); } public void update(Integer productId, Integer quantity) { if (quantity null || quantity 1) { items.remove(productId); } else { items.put(productId, quantity); } } public MapInteger, Integer getItems() { return items; } }Map 的 key 是 productIdvalue 是数量。用 LinkedHashMap 而不是 HashMap是为了让购物车页面按加入顺序展示。merge 方法把“已存在则累加、不存在则放入”合并成一行省掉先 containsKey 再 put 的啰嗦写法。update 里小于 1 的数量统一按移除处理这样 JSP 页面的加减按钮只需要传新数量不需要单独的删除标记。注意Session 里的购物车在服务端重启后会丢。下单前要判空并提示用户重新加购这个兜底逻辑虽然丑但能避免空指针在答辩现场翻车。4.3 MySQL UPDATE 条件扣库存与事务回滚下单是鉴权、购物车、商品、订单四块逻辑的交汇点最容易出事故的是库存超卖。两个请求同时读到 stock1各自通过检查然后都执行 stock 减一最终卖出去两件。解决的核心不是加锁而是把扣减条件写进 UPDATE 语句public boolean createOrder(int userId, Cart cart) { String orderSql INSERT INTO orders (order_no, user_id, total_amount, pay_status) VALUES (?,?,?,0); String stockSql UPDATE product SET stock stock - ? WHERE id ? AND stock ?; try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try { long orderId insertOrder(conn, orderSql, userId, cart); for (Map.EntryInteger, Integer e : cart.getItems().entrySet()) { // 先扣库存affected rows 0 说明库存不足抛异常触发回滚 try (PreparedStatement ps conn.prepareStatement(stockSql)) { ps.setInt(1, e.getValue()); ps.setInt(2, e.getKey()); ps.setInt(3, e.getValue()); if (ps.executeUpdate() 0) { throw new RuntimeException(库存不足: e.getKey()); } } insertOrderItem(conn, orderId, e.getKey(), e.getValue()); } conn.commit(); return true; } catch (Exception ex) { conn.rollback(); throw ex; } } catch (Exception e) { LOGGER.error(下单失败, e); return false; } }逻辑要点有三个。第一setAutoCommit(false) 之后所有 SQL 在同一个事务里任何一条失败都能 rollback不会出现“订单建了、库存没扣”的中间状态。第二扣库存用 affected rows 判断而不是先查后改天然避免超卖。第三order_item 里的商品名、单价必须从数据库取不能直接用购物车里的展示价因为多个会话同时操作时展示价可能已经过期。事务边界必须放在 Service 层不能散落在 Dao 层。一个下单方法同时写 orders、order_item 并更新 product.stock三张表必须同一个 Connection、同一个事务。下面这组验证场景是下单逻辑最常见的测试路径| 场景 | 期望结果 | 验证手段 | | 库存 1 件、两个会话同时下单 | 仅一单成功stock 不为负数 | 双开浏览器同时提交或用 JMeter 并发 2 | | 下单中途抛异常 | orders 与 order_item 无残留stock 不变 | 在 insertOrderItem 里人为抛错检查数据回滚 |5. Tomcat 部署与 MySQL 8.0 连接排错5.1 IDEA 运行 JavaWeb 项目的 Tomcat 配置IDEA 版本不同创建入口会变但核心步骤是稳定的一套New Project 选 Java EnterpriseApplication Server 选本机 Tomcat 8.5 或 9.x勾选 Web Application如果是老式 Maven war 工程直接补 webapp 目录再配置 Artifact 也可以。关键在 Run 配置里的两个 Tab。Deployment Tab 必须把 Artifact 加进去浏览器访问路径是 /mall_war_exploded 还是 /mall取决于这里填的 Application context。Server Tab 里把 On frame deactivation 设为 Update classes and resources改 JSP 和静态资源时可以热更新改 Java 代码才需要重启。第一次跑通的标准动作是启动 Tomcat访问 http://localhost:8080/mall/看到首页而不是 404 或 Tomcat 默认页。404 的排查顺序固定先看控制台有没有部署异常再看 Artifact 是否为空最后核对 Application context 和访问 URL 是否一致。课程设计里大部分“启动成功但页面 404”都是第三个原因。5.2 MySQL 8.0 的 JDBC 连接串参数MySQL 8.0 默认认证插件是 caching_sha2_password配 5.x 时代的老驱动会直接报错。驱动务必用 mysql-connector-java 8.0.x连接串把几个必填参数写全// DBUtil.java 中的连接串常量 private static final String URL jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue;useSSLfalse 只建议本地开发生产必须改用证书连接。serverTimezone 不写驱动会在连接时报时区异常Asia/Shanghai 对应东八区。allowPublicKeyRetrievaltrue 配合 caching_sha2_password允许客户端从服务端获取公钥做 RSA 加密传输密码。这几个参数在 MySQL Workbench 里不需要关心但在 JDBC 里一个都不能省。提示本机是 MySQL 5.7 的话驱动继续用 8.0.x 没问题反过来的老驱动连 8.0 才会踩认证坑。这个兼容关系在 mysql 安装教程里很少讲但联调时几乎必遇。5.3 MySQL 连接与部署高频报错对照表把完整源码换一台电脑跑起来报错翻来覆去就是下面几类按优先级查| 报错关键字 | 根因 | 处理方式 | | Access denied for user rootlocalhost | 密码错或 root 只允许 localhost | 用 MySQL Workbench 重设密码 | | Public Key Retrieval is not allowed | 缺 allowPublicKeyRetrieval 参数 | 连接串补该参数 | | The server time zone value Öйú±ê׼ʱ¼ä | serverTimezone 未设置 | 连接串补 serverTimezoneAsia/Shanghai | | ClassNotFoundException: com.mysql.jdbc.Driver | 驱动类名写错或 jar 没进 WEB-INF/lib | 8.x 驱动类名是 com.mysql.cj.jdbc.Driver | | Table mall.user doesnt exist | SQL 脚本没导入或没切库 | 确认 USE mall 后再建表 | | HTTP 404 且显示 Tomcat 默认页 | Artifact 未部署或 context 路径不对 | 检查 Deployment 配置 |中文乱码最容易迷惑人经常页面源码正常、浏览器显示乱码。排查顺序固定数据库连接串 characterEncoding过滤器 setCharacterEncodingJSP pageEncoding最后是浏览器编码。前三处统一为 UTF-8最后一个基本不会出问题。还有一个特定于“完整源码”的坑有外键的 sql 文件导入顺序必须按 user、category、product、orders、order_item否则报外键不存在。没外键的脚本也要看一眼索引很多源码把索引全省了数据量小感觉不出来一旦往 product 表插几万条测试数据分类页的慢查询立刻暴露。6. 让 JavaWeb 商城源码通过复查四个验证与两个改进6.1 答辩前必过的四个边界验证源码跑通只是第一步复查应该是逆向的故意做错操作看系统会不会兜底。快速过四件事。第一重复提交下单页连点两次提交按钮看是否生成两笔相同订单正确做法是前端按钮置灰后端落单前查重复 order_no。第二库存边界把某商品库存改成 1另开两个浏览器同时下单只有一单成功且 stock 不为负。第三越权访问不登录直接访问 /mall/order/list应该被 AuthFilter 拦回登录页。第四登录注入在用户名框输入 or 11登录必须失败而不是进入后台。这四条串起来基本覆盖了面试官最爱追问的边界。6.2 两个低成本高回报的源码改进第一所有 SQL 入口删除字符串拼接统一换 PreparedStatement这条同时解决注入和可读性。第二密码从 MD5 换成加盐哈希用标准库的 PBKDF2 或 BCrypt不要自己发明加密算法。两个改动量都不大但能把“课程设计源码”和“生产习惯”区分开。改完后在 MySQL 里开一下慢查询日志跑一遍购物全流程确认没有任何 SQL 出现在 slow log 里这个项目就可以拿去见人了。本文还有配套的精品资源点击获取