☰
JavaWeb购物商城项目拆解:从Servlet到动态代理事务管理
2026/10/7 4:34:30 网站建设 项目流程

简介:一套基于MVC设计模式与动态代理模式实现的JavaWeb购物商城完整源码项目,包含前端商城与后台管理两大部分,面向刚掌握JavaWeb基础、希望以完整项目串联所学知识的开发者,可有效解决学完不知如何应用的常见问题。压缩包共613个文件,大小16.85MB,主要构成是Java源码、JSP页面、CSS/JS前端样式与脚本、SQL数据库脚本,以及运行所需的JAR依赖和商品图片素材;文件类型丰富、目录结构清晰,便于直接导入开发工具对照学习。目前已有21951人浏览学习,在同类JavaWeb练手项目中具有一定热度,适合用于课程设计或毕业设计参考。项目功能覆盖主页热销展示、商品搜索与详情、商品评价评分、库存校验、立即购买、购物车增减与删除、确认订单、防重复提交、地址选择与新增,以及后台会员管理、商品批量上下架、库存维护和订单发货等完整电商链路;对照源码可以学习MVC分层、动态代理、参数校验和数据库操作的落地写法,是边读边练的优质素材。

1. JavaWeb购物商城:MVC+动态代理,把基础语法变成完整项目

一个JavaWeb购物商城项目,听起来像课程设计标配,但它背后覆盖的Servlet、JSP、JDBC、MVC分层、动态代理这些内容,几乎就是JavaWeb从入门到能干活的全过程。这个项目自带MySQL数据库脚本,前台展示、搜索、购物车、下单、后台发货管理是一条完整业务链路。我拆过不少JavaWeb源码,这份的好处是不依赖SSM框架,只用原生Servlet和JDBC就完成一个真实商城,适合学完JavaWeb基础但不知道怎么组织项目的阶段,也适合拿去改造成课设或简历项目。下面的拆解会让新手能跟着跑起来,也会让熟手直接看到几个容易翻车的位置。

2. 项目结构先看懂:MVC三层、数据库表和动态代理事务

2.1 六张核心表:购物商城的字段怎么设计

这个项目把功能描述集中在业务上,但真正支撑业务的是MySQL里那几张表。按购物商城的常规设计,这套源码至少包含六张核心表:用户表t_user、商品表t_product、地址表t_address、购物车表t_cart、订单表t_order、订单明细表t_orderitem。我在拆项目时习惯先看SQL脚本,因为表的字段设计直接决定代码怎么写。

下表是这套源码里核心表的字段作用(字段名可能因版本有出入,但业务含义一致):

表名核心字段承担的业务
t_useruid、username、password、status登录、会员启用/禁用
t_productpid、pname、price、stock、is_hot、pflag商品展示、搜索、库存校验
t_addressaid、uid、address、phone、receiver确认订单页的地址选择与新增
t_cartcid、uid、pid、count购物车数量维护
t_orderoid、uid、aid、total_price、ordertime、status订单生成与状态流转
t_orderitemitemid、oid、pid、count、subtotal订单明细,保证历史订单金额不被商品改价影响

这里最容易混淆的是status字段的双重含义。在t_user表里status表示账户状态,1为启用、0为禁用;在t_order表里status表示订单状态,常见划分是1未发货、2已发货、3已删除。pflag则是商品上下架标识,1上架、0下架。同一个0和1在不同表里代表完全不同的业务含义,这是读源码时最容易看晕的地方,我见过有人把删除订单的SQL直接写成delete from t_order,其实正规做法是更新status为3做逻辑删除。

表之间的关系也值得理一遍。t_cart通过uid关联t_user、通过pid关联t_product;t_order通过uid关联用户、通过aid关联地址;t_orderitem通过oid关联订单、通过pid关联商品。订单明细表的存在是为了保存下单那一刻的商品快照,价格、数量都冗余在明细里,这样商品改价不会影响历史订单的金额计算。画清楚这几条关系,前台链路在代码里走一遍就不会迷路。

2.2 MVC分层:Servlet、service、dao各自的边界

这个项目的包结构基本是MVC教科书式划分:controller层放Servlet,负责接收请求、解析参数、转发或重定向;service层放业务逻辑,比如计算订单金额、校验库存;dao层放JDBC访问代码,只负责SQL和结果集封装;domain或entity包放JavaBean;utils包放JDBCUtils、代理工厂这类通用工具。

判断一个分层是否合理,我常用的标准是看service层有没有被DAO代码污染。如果service里出现Connection、PreparedStatement,说明事务边界没有收拢。这个项目用了动态代理来处理事务,所以service实现类里基本不会出现连接管理代码,事务统一交给代理层。

2.3 动态代理统一事务:一段代码省掉所有提交回滚

这是摘要里点名的一个技术点,也是这个项目值得看懂的地方。常见做法是service实现类都实现同名接口,Servlet从代理工厂拿到的不是实现类对象,而是实现类的代理对象。代理对象拦截每个业务方法,在方法执行前关掉连接自动提交、在方法正常结束后提交、在方法抛异常时回滚。

public class ServiceProxyFactory { public static Object getService(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> { Connection conn = JDBCUtils.getConnection(); try { conn.setAutoCommit(false); Object result = method.invoke(target, args); conn.commit(); return result; } catch (Exception e) { conn.rollback(); throw new RuntimeException("业务执行失败,事务已回滚", e); } finally { JDBCUtils.close(conn); } } ); } }

这段代码有三个参数要知道。第一个target.getClass().getClassLoader()是类加载器,JDK动态代理要靠它生成代理类字节码;第二个getInterfaces()是接口数组,代理对象只能转换成接口类型使用,所以service实现类必须写接口,否则这一步直接抛ClassCastException;第三个lambda是InvocationHandler的invoke方法,代理对象每次调用接口方法都会先进这个方法。方法体里setAutoCommit(false)是事务生效的前提,如果漏掉这行,MySQL默认每执行一条SQL就自动提交,下单时扣库存和插订单明细就会出现扣了库存但订单没生成的情况。

动态代理解决了"每个业务方法都写一遍开关事务"的重复代码,但要注意它管理的是JDBC事务,不是MySQL的事务隔离级别。这两层是独立的概念:代理层控制提交和回滚,MySQL的隔离级别由数据库配置决定。能在简历项目里把这两个概念分开讲清楚,面试官基本会认定你是真做过,而不是背了几道八股。

3. 前台购物链路拆解:从搜索商品到提交订单的完整流程

3.1 商品列表与搜索:分页SQL和like参数是两处细节

前台入口是这三块:首页热销商品、所有商品展示、商品搜索。它们的SQL本质上是同一个查询的不同条件组合:热销是is_hot=1,商品列表是pflag=1(只显示上架商品),搜索是在pflag=1的基础上加pname like "%关键字%"。我用一段典型的dao层查询来拆这个逻辑:

public List<Product> findProductByPage(int currentPage, int pageSize, String keyword) { String sql = "select * from t_product where pflag = 1 "; List<Object> params = new ArrayList<>(); int offset = (currentPage - 1) * pageSize; if (keyword != null && !"".equals(keyword.trim())) { sql += " and pname like ? limit ?, ?"; params.add("%" + keyword.trim() + "%"); params.add(offset); params.add(pageSize); } else { sql += " limit ?, ?"; params.add(offset); params.add(pageSize); } return queryList(sql, params); }

这里有两个高频翻车点。第一个是limit的起始索引,MySQL的limit第一个参数是偏移量,第二个是条数,offset必须等于(currentPage - 1) * pageSize,写反了会出现第一页数据重复,写漏了第二页会直接报SQL语法错误。第二个是like的参数,不能直接传keyword,要拼成"%关键字%",否则模糊查询退化成精确匹配,搜"手"只能命中一个字,搜不到"手机"。我一般建议点击分页按钮时把当前页和关键字一起回传,不然翻到第二页搜索条件就丢了。

这一步在三层架构里的数据流向是:JSP页面的表单或链接携带keyword参数到ProductServlet,Servlet从request中拿参数、做非空校验,再调service层,最终落到dao层这个方法。结果集返回后servlet把list和pageBean放进request域,转发到product_list.jsp渲染。这里要提醒一件事:转发用request.getRequestDispatcher().forward(),重定向用sendRedirect(),带数据到页面必须用forward,用重定向request里的list会丢。

3.2 购物车数量变更:前端手输与后端校验的配合

摘要描述里专门提到"可增减购买商品数量亦可手动输入,同时验证库存",对应到实现上是购物车页面的加减按钮和input输入框各自触发一个请求,后端在更新数量前先查一次商品表的最新库存。

常见做法是前端把商品id和最新数量一起提交到购物车servlet:

public void updateCount(HttpServletRequest request, HttpServletResponse response) { int cid = Integer.parseInt(request.getParameter("cid")); int pid = Integer.parseInt(request.getParameter("pid")); int count = Integer.parseInt(request.getParameter("count")); Product product = productDao.findById(pid); if (product.getStock() < count) { response.getWriter().write("stock_not_enough"); return; } cartDao.updateCount(cid, count); response.getWriter().write("ok"); }

这段代码的关键在"提交之后再查一次库存",而不是信任购物车页面上显示的数字。原因很简单:用户把页面开着不动,库存可能已经被别人买走;或者用户直接改了前端input的value,绕过加减按钮直接提交一个999,如果后端不查库存,下单阶段就会出问题。

前端部分牵涉到离开购物车后的几个跳转:选择了多个商品后点结算,servlet要接收选中的购物车条目id列表,常见做法是checkbox的name值相同,value是cid,后面用request.getParameterValues("cid")拿到数组,注意这个方法拿不到值时返回null,要先判空再遍历。

3.3 提交订单:防重复提交与库存校验的双重防护

下单是整个项目业务复杂度最高的位置,因为这一串操作里至少有四件要一起完成的事:校验库存、插入订单主表、插入订单明细、扣减商品库存。四件事必须在一个事务里,任何一步失败都要回滚。动态代理在这里的价值就体现出来了:只要订单service方法内部完成这四步,事务边界由代理统一管理,不需要手动加回滚逻辑。

防重复提交是这个功能点里的经典考点。实际场景通常是:用户连点了两次"提交订单"按钮,或者提交后网络慢,用户又刷新了一次。解决方式常用的是session token方案:

String token = (String) request.getSession().getAttribute("orderToken"); String formToken = request.getParameter("orderToken"); if (token == null || !token.equals(formToken)) { request.setAttribute("msg", "订单已提交,请不要重复操作"); request.getRequestDispatcher("/order_result.jsp").forward(request, response); return; } // token校验通过后立即移除,保证一次token只能用一次 request.getSession().removeAttribute("orderToken");

这个方案的约束条件有两个:确认订单页生成token时要同时放进session和表单隐藏域;校验通过后必须立即remove,否则用户刷新后session里还是旧token,第二次提交还能通过。订单提交成功后页面要展示订单号、总金额、收货信息,这些可以放在订单对象里带到结果页。库存不足和商品下架的处理则在订单service里:如果库存不足直接抛出带提示信息的异常,代理捕获后回滚,前台页面用try-catch或全局异常处理把提示显示出来。

要特别说的是,这里校验的不只是库存数字。商品下架(pflag变为0)时也要在订单service里做一次状态检查,否则后台把商品下架了,用户从长期打开的购物车页面还能继续下单。摘要里写的"库存不足或商品下架给予响应"就是这个意思,它是两个独立的校验分支,不能合并成一句SQL。

4. 后台管理模块:会员、商品、订单三个入口的状态设计

4.1 会员管理:启用禁用账户与密码修改的实现方式

后台会员管理的核心操作是启用/禁用账户、修改密码。启用禁用对应t_user表的status字段,1为启用、0为禁用。这个开关生效体现在登录service里:用户输入用户名和密码校验通过后,还要检查status是否为1,为0则提示"账户已被禁用"。

修改密码的常见做法是管理员输入新密码,servlet接收后先做一次MD5加密再更新数据库:

public void resetPassword(HttpServletRequest request, HttpServletResponse response) { int uid = Integer.parseInt(request.getParameter("uid")); String newPwd = request.getParameter("newPwd"); String encrypted = MD5Utils.md5(newPwd); userDao.updatePassword(uid, encrypted); response.sendRedirect("admin/user_list.jsp"); }

这里有个细节要注意:加密只能做一次,不能在多个地方重复加密导致密码变成二次MD5。有些新手会把MD5结果明文存在数据库里,稍好一点的加盐,但这个项目定位是练手级,MD5加密一次是够用的,如果能顺手加个固定盐值就更稳。真正要强调的是一致性——管理员重置密码后,用户用原密码登录会失败,这是预期行为,但如果项目里同时存在注册功能,注册时的密码加密规则必须和这里完全一致,否则注册后永远登不进去。

4.2 商品管理:批量添加、上下架与库存维护的实现思路

商品管理模块常见的功能是商品批量添加、上下架、库存维护。批量添加的典型做法是servlet接收多个商品参数,循环调用dao的insert方法,或用批量SQL一次插入多条。上下架则是对应t_product表的pflag字段,上架置1、下架置0。

库存维护的常见实现是提供一个表单让管理员直接修改库存数量,但更稳妥的做法是记录"增加/减少"操作而不是直接覆盖数值。直接覆盖的风险在于管理员如果不小心清空了输入框再提交,库存会变成0或null。我倾向于把库存调整做成一个增量字段,servlet里用当前库存加上调整值,并校验调整后的结果不能为负数。

商品管理会直接影响前台三个地方:商品下架后列表页不再显示,详情页直接访问要提示商品不存在,购物车里已下架的商品在结算时要提示。这三个位置的联调是测试商品模块是否完整的重要指标,只改了列表页的查询条件、不管购物车和详情页,就会出现"前台列表看不到,但购物车还能下单"的矛盾状态。

4.3 订单管理:发货与删除的状态流转约束

订单管理主要是发货和删除两个操作,对应t_order表的status字段。发货是把status从1改成2,删除是把status改成3做逻辑删除。

public void shipOrder(HttpServletRequest request, HttpServletResponse response) { int oid = Integer.parseInt(request.getParameter("oid")); Order order = orderDao.findById(oid); if (order == null || order.getStatus() != 1) { request.setAttribute("msg", "订单状态已变化,无法发货"); request.getRequestDispatcher("/admin/order_list.jsp").forward(request, response); return; } orderDao.updateStatus(oid, 2); response.sendRedirect("admin/order_list?flag=notShipped"); }

这段代码的约束顺序是有讲究的:先查订单、再判断状态、再更新。如果不查订单直接执行update,容易出现重复发货的记录错乱,也拿不到旧状态去做业务判断。删除订单同理,要先判断status是否等于2(已发货),已发货的订单理论上要先取消发货才能删除,或者直接禁止删除已发货订单,这个约束由业务规则决定。

后台三个管理模块加起来其实是同一件事:围绕status字段做状态机。用户表一个status,商品表一个pflag,订单表一个status,每一处状态变更都要校验前置状态。理解了这个,后台模块的代码读起来会快很多。

5. 避坑指南:Idea运行配置、MySQL连接与乱码排查

运行JavaWeb项目时,最容易让人烦躁的不是业务代码,而是环境配置。下面这几条都是我在用IDEA跑这类项目时真实遇到过的,按"现象→原因→解决"写清楚。

5.1 现象一:Tomcat启动成功但访问404

现象:IDEA里Tomcat显示启动成功,浏览器访问项目路径却404,甚至访问Tomcat首页也404。原因:最常见的是部署描述里Application context配置不对。IDEA的Tomcat配置中Deployment选项卡里Application context填的值,决定你访问项目时要带的路径。默认填/项目名,很多人填成了全限定路径或者写错大小写,就找不到资源。解决:在IDEA的Run Configuration里找到Tomcat Server,切到Deployment页,确认Application context写的是/你的项目名,并且Deployment里已经添加了带exploded的artifact。访问地址就是http://localhost:8080/项目名/首页。如果访问后是Tomcat的默认页面而不是项目页面,说明项目根本没部署成功,回到Deployment选项卡重新点一下加号选择artifact。

5.2 现象二:MySQL 8.x连接报Public Key Retrieval错误

现象:项目启动后第一次访问数据库相关页面,控制台抛Public Key Retrieval is not allowed,页面直接500。原因:MySQL 8.0版本默认使用caching_sha2_password认证插件,客户端连接时需要先获取服务器的公钥,JDBC驱动默认不允许自动获取。解决:JDBC连接URL里加上allowPublicKeyRetrieval=true&useSSL=false,同时确认驱动版本和数据库版本匹配。我一般建议直接用mysql-connector-java 8.0.x,不要用旧版5.x驱动连8.x库,会出现认证插件不支持的错误。另外,如果你的MySQL是解压版通过命令行安装的,先确认服务真的启动了,net start mysql不行就检查data目录是不是初始化过。

5.3 现象三:页面中文全部变成问号

现象:JSP页面显示正常,但从数据库读出来的中文、表单提交的中文全变成问号。原因:乱码涉及三个环节——数据库连接URL缺characterEncoding=utf8、JSP页面没设置pageEncoding、Tomcat接收POST请求时没有统一编码过滤。三个环节任何一个漏掉,中文就会在链路上某一段变成问号。解决:在JDBC的URL里追加characterEncoding=utf8;JSP头部设置<%@ page contentType="text/html;charset=UTF-8" %>;在web.xml里配置一个CharacterEncodingFilter,强制把request和response都设置成UTF-8。配置完这三处后重启Tomcat,如果还是乱码,检查MySQL数据库和表的字符集,命令行执行show variables like 'character%',确认不是数据库侧用了latin1。

5.4 现象四:动态代理报ClassCastException

现象:调用ServiceFactory.getService()后,把返回值强转成实现类,运行时报ClassCastException。原因:JDK动态代理生成的代理类只实现了传入的接口,没有继承原始实现类。把代理对象强转成实现类类型当然报错,只能转成接口类型。解决:所有使用代理对象的地方,声明类型都用接口。比如UserService userService = (UserService) ServiceProxyFactory.getService(new UserServiceImpl());,绝不能写成UserServiceImpl userService = ...。这是一个很隐蔽的坑,因为编译期不报错,运行到首次调用才会炸,排查时先看强转的类型是不是接口。

5.5 现象五:下单成功却查不到订单记录

现象:页面提示下单成功,跳转到了成功页,但数据库里t_order表没有新记录,或者有订单但t_product的库存没扣。原因:事务没有真正开启,或者Connection被多个线程共用。常见于没有走动态代理、直接new了service实现类调用方法,导致dao层的每条SQL独立执行,insert成功但后续扣库存失败时没有回滚。解决:确认Servlet里获取的是代理对象而不是原始对象。这是见真章的地方——这个项目强调动态代理,就是为了让下单这种多步操作在一个事务里完成。反复测试的方法很简单:在下单service里故意抛一个异常,如果库存和订单都能查回原值,说明事务生效;如果出现脏数据,说明事务没包住。

6. 上线前的验证:用五个备查项确认购物商城真的跑通了

项目能启动不等于功能是对的。我会用五个备查项把这套JavaWeb购物商城过一遍,每个都是黑匣子级别的高频业务路径,跑通就能放心去演示或写进简历。

第一个备查项是前台链路:登录账号,首页看到热销商品,搜索"手机"能出结果,进详情页,加购两件,购物车改成三件,结算,新增一个地址,提交订单,在"我的订单"里看到这条订单。中间任何一个环节断掉,优先查对应servlet的转发路径和JSP里表单的action地址。

第二个备查项是库存边界:把商品库存故意改成1,购物车添加2件,提交订单时应该被拦截并提示库存不足;再把库存改成1,连续对同一商品下两单,第二单应该失败或提示库存不足。这两个场景验证的就是第3.3节说的双防护逻辑。

第三个备查项是防重复提交:在确认订单页面连续快速点击两次提交按钮,第二次应该被session token拦截。这个测试我在演示前一定会做,因为现场网络一旦慢一拍,评委或同事大概率会习惯性多点一次。

第四个备查项是后台状态变化对前台的影响:后台把商品下架,前台的列表页、详情页、购物车结算三个位置都应该有响应。后台给订单发货后,用户端订单状态应该从"未发货"变"已发货"。这两组联动验证的是状态字段在前后台的传递是否完整。

第五个备查项是数据一致性:下单后打开MySQL命令行,执行select * from t_order where oid=订单号,再执行select * from t_orderitem where oid=订单号,确认主表和明细都在;再查一次t_product的库存和页面显示的剩余库存一致。mysql数据库常用命令里select和update是最高频的两条,排查数据对不对基本就靠它们。

从那以后,我每次接手这类JavaWeb源码,都会先在数据库里做一次"下单前库存—下单后库存—订单明细"的三连查,确认事务是闭环的再继续改代码。这套验证习惯帮我挡掉了不少演示现场的尴尬,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询