拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SSH框架甜品商城系统解析:从MVC分层到Spring Boot迁移

简介一套基于SSH框架SpringStrutsHibernate开发的甜品商城管理系统面向正在准备毕业设计的计算机专业学生以及需要JavaWeb项目实战练习的开发者。系统完整覆盖商城前台与后台核心业务前台支持用户注册登录、甜品分类浏览、购物车、订单结算、支付宝沙箱支付、会员办理和论坛发帖后台提供订单管理、用户管理、商品与类目管理、会员管理等模块功能完善且界面简洁可直接作为毕设项目运行演示。压缩包共1166个文件总大小约49.57MB囊括Java源码、JSP页面、jar依赖库、JS/CSS前端资源以及数据库脚本.sql和项目配置文件其中图片资源占比较高便于前端界面直接使用项目基于JDK8、Eclipse或IDEA环境开发导入即可调试部署。目前该资源已有368人学习下载适合需要快速获取完整可运行商城系统、参考模块设计与前后端交互逻辑的Java学习者。1. 一个SSH老项目值不值得下先说结论如果你在找毕设或练手项目这套基于SSH框架的甜品商城管理系统是典型的老牌 JavaWeb 课程设计Spring Struts Hibernate MySQL前端用 JSP、jQuery、CSS。它不像 Spring Boot 那套全家桶那么新但正因为旧反而把 MVC 分层、DAO 封装、事务控制这些基本功摊开给你看。项目里还能看到Apriori.class这类关联规则算法类也就是说它不只是增删改查还带了一点推荐逻辑。对想理解 JavaWeb 从请求到数据库全链路的人这个源码能复现的价值比网上那些只贴截图的项目高得多。适合两类人一是要做毕设答辩、需要讲清楚框架原理的在校生二是想快速补一遍 SSH 三种框架协作方式的在职开发。下文按“框架分工 → 本地运行 → 核心模块 → 演进迁移”的顺序拆每个环节都会给出可抄的配置和代码。2. SSH框架如何在甜品商城里各司其职SSH 组合里 Struts2 管请求分发Spring 管对象装配和事务Hibernate 管数据库表映射。三方各做各的事但最终要在struts.xml、applicationContext.xml和一堆.hbm.xml里碰头。理解这三份配置的联动关系比背概念有用得多。2.1 Struts2 的 Action 与请求路由前台所有用户操作都从 URL 打到 Struts2 的 FilterDispatcher再按action配置找到对应的 Action 类。项目里UserOrderAction.class、GoodsAction.class、ForumAction.class都是这一层。一个典型配置如下package namegoods namespace/goods extendsstruts-default action namelist classgoodsAction methodlist result namesuccess/WEB-INF/pages/goodsList.jsp/result /action action namedetail classgoodsAction methoddetail result namesuccess/WEB-INF/pages/goodsDetail.jsp/result result nameerror/WEB-INF/pages/error.jsp/result /action /package这段配置的逻辑是访问contextPath/goods/list时Struts2 从 Spring 容器里取名为goodsAction的 Bean调用它的list()方法方法返回字符串success后跳转到goodsList.jsp。重点是classgoodsAction这个值不是类全名而是 Spring 容器里的 Bean 名称因为 struts2-spring-plugin 会把 Action 实例的创建权交给 Spring。如果配置成classcom.shop.action.GoodsAction则 Action 内的 service 依赖不会被注入运行时大概率报 null 指针。2.2 Spring 的 Bean 管理与事务边界applicationContext.xml里做两件事开启组件扫描和配置事务管理器。项目里的BizService.class就是业务层门面购物车结算、订单提交、会员办理都集中在里面。事务边界一般划在业务方法上推荐用注解声明式事务bean idtransactionManager classorg.springframework.orm.hibernate5.HibernateTransactionManager property namesessionFactory refsessionFactory/ /bean tx:annotation-driven transaction-managertransactionManager/HibernateTransactionManager会把事务绑定到当前线程的 Session 上意思是同一个业务方法内的所有 DAO 操作共享一个数据库连接。订单结算场景里先插入订单主表、再插入订单明细、最后扣减库存这三步必须在一个事务里任何一步抛异常都会整体回滚。注意事务默认只对RuntimeException回滚如果业务代码里 catch 掉异常又返回提示事务就失效了这是很多订单数据对不上的根源。2.3 Hibernate 的 ORM 映射与 BaseHibernateDao 封装BaseHibernateDao.class是 DAO 层的公共基类用泛型封装了增删改查和分页。它直接持有 Hibernate 的SessionFactory通过getCurrentSession()获取会话这样能跟 Spring 事务绑定。public class BaseHibernateDaoT, K extends Serializable implements IBaseDaoT, K { Resource private SessionFactory sessionFactory; protected Session getSession() { return sessionFactory.getCurrentSession(); } public void save(T entity) { getSession().save(entity); } public ListT queryByHql(String hql, MapString, Object params) { Query query getSession().createQuery(hql); for (Map.EntryString, Object entry : params.entrySet()) { query.setParameter(entry.getKey(), entry.getValue()); } return query.list(); } }这段代码里的每个点都有讲究getCurrentSession()只允许在事务内使用所以 DAO 方法依赖外层的Transactional否则会报No Session异常setParameter用命名参数而不是拼字符串是为了防止 SQL 注入同时也能让 Hibernate 走 PreparedStatement 缓存。如果你看到同事把 HQL 用连接变量拼进去那基本就是在裸奔。2.4 前后端数据流与表结构设计要点前台 JSP 提交表单后Struts2 把请求参数自动映射到 Action 的 JavaBean 属性Action 调 BizService 里的方法BizService 通过 BaseHibernateDao 完成持久化。回到页面需要的数据放进 request 或 session 域JSP 用 JSTL 和 EL 表达式读取。这个项目的数据库脚本里有 9 张核心表对应甜品的商品、分类、订单、订单项、用户、地址、论坛帖子、会员、购物车。建表时要注意几个约束否则会在 Hibernate 校验时报错CREATE TABLE dessert_goods ( id INT(11) NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 甜品名称, price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT(11) NOT NULL DEFAULT 0, category_id INT(11) NOT NULL, status TINYINT(4) DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dessert_order_item ( id INT(11) NOT NULL AUTO_INCREMENT, order_id INT(11) NOT NULL, goods_id INT(11) NOT NULL, quantity INT(11) NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT 成交单价, PRIMARY KEY (id), KEY idx_order (order_id), CONSTRAINT fk_order_item_order FOREIGN KEY (order_id) REFERENCES dessert_order (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里DECIMAL(10,2)存储价格避免 Double 浮点误差utf8mb4支持四字节表情甜品店名称里可能带“”这类 emoji用旧的utf8会插入报错。分类和订单之间用逻辑外键即可但订单明细必须物理外键否则会出现孤儿数据。3. 本地跑通项目从源码到浏览器许多人在这一步卡住往往不是代码问题而是 JDK 版本、Tomcat 版本、数据库驱动不匹配。SSH 项目对版本极其敏感下面按我实际调试的顺序走一遍。3.1 环境版本选型与 JDK8 陷阱项目标注运行环境是 JDK8、Eclipse 或 IDEA、Navicat。如果你电脑装了 JDK17直接跑会报UnsupportedClassVersionError因为 Hibernate 3/4 的字节码只在 Java 8 下编译过。建议安装 JDK8并在 IDE 里把编译级别设为 1.8。另一个坑是 Tomcat 版本。Struts2 早期版本在 Tomcat 9 上运行会因 Servlet API 版本兼容问题报ClassNotFoundException: javax.servlet.Filter之类的错误。常见做法是使用 Tomcat 8.5它既能兼容 Servlet 3.1又能跑老项目。版本组合可以参考下表组件推荐版本说明JDK1.8.0_202最后一个免费商用版本Tomcat8.5.x兼容 Struts2 2.3/2.5MySQL5.7避免 MySQL 8 的密码插件问题Hibernate4.x 或 5.2对应 Spring 4.xStruts22.3.32 或 2.5.302.5 需要留意 Struts2 漏洞补丁MySQL 8 的认证插件是caching_sha2_password老版 JDBC 驱动不认识连接会报Unable to load authentication plugin。项目里如果用的是mysql-connector-java 5.1.x建议直接换 5.7 数据库或者在连接串上加allowPublicKeyRetrievaltrueuseSSLfalse。3.2 导入 Eclipse 与数据库初始化拿到源码后不要直接双击项目文件正确步骤是先新建一个 Dynamic Web Project再把源码覆盖进去最后右键项目 → Properties → Targeted Runtimes 勾选 Tomcat 8.5。依赖的 jar 包放在WebContent/WEB-INF/lib下不要用 Maven否则版本冲突会把你逼疯。数据库初始化用 Navicat 新建一个dessert_shop数据库然后执行项目里的dessert.sql脚本mysql -uroot -p123456 dessert.sql执行完成后进入jdbc.properties修改数据库连接参数。注意这个文件里的配置会被 Spring 的占位符读取jdbc.driverClassNamecom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/dessert_shop?useUnicodetruecharacterEncodingutf8 jdbc.usernameroot jdbc.password123456 jdbc.maxActive20 jdbc.initialSize5characterEncodingutf8是必须的少了它中文甜品名会乱码。maxActive20表示连接池最大 20 个连接调试时如果频繁出现连接超时可以临时调大到 50但上线前要改回来。3.3 支付宝沙箱配置项目里的订单结算对接支付宝沙箱这需要你去支付宝开放平台申请沙箱应用拿到 APPID、商户私钥、支付宝公钥三样东西。在项目里搜索AlipayConfig类修改以下参数public static final String APP_ID 2021003122690000; public static final String PRIVATE_KEY MIIEvQIBADANBg...; public static final String ALIPAY_PUBLIC_KEY MIIBIjANBg...; public static final String NOTIFY_URL http://localhost:8080/dessert_shop/order/notify; public static final String RETURN_URL http://localhost:8080/dessert_shop/order/return;NOTIFY_URL是异步通知地址支付宝服务器会用 POST 请求回调它业务上在这个方法里更新订单状态为已支付。沙箱环境下这个地址必须是公网可访问的但调试时常见做法是用内网穿透工具把本机 8080 端口映射出去。如果只想看流程也可以把回调逻辑改成在RETURN_URL的页面里直接标记支付成功但对比完两者差异后你会发现异步通知才是能扛住高并发的方案。3.4 常见启动报错与排查启动 Tomcat 时最容易遇到三类报错。第一类ClassNotFoundException: org.springframework.orm.hibernate5.LocalSessionFactoryBean说明 spring-orm 版本与 Hibernate 版本不匹配检查 jar 包中 Hibernate 是 5 还是 4LocalSessionFactoryBean 也要改成对应的类名。第二类java.lang.NoSuchMethodError: javax.servlet.http.HttpServletResponse.getStatus()这是 Tomcat 版本太旧导致 Struts2 与 Servlet API 冲突换 Tomcat 8.5。第三类DataIntegrityViolationException通常是数据库脚本没执行干净比如外键引用的表不存在把SHOW ENGINE INNODB STATUS里的报错信息和 SQL 脚本逐行核对。下面给一个快速自检脚本能判断项目是否已经正确连接到数据库curl http://localhost:8080/dessert_shop/goods/list如果返回的是 JSP 渲染后的 HTML说明 Struts2 路由通了如果是 500 页面去看catalina.out日志关注以Caused by开头的第一行那才是问题根因。不要被前面一大段Exception堆栈干扰SSH 项目的日志里80% 的异常信息是外层框架包装的。4. 核心业务模块的实现拆解购物车、订单与 Apriori 推荐这个项目的价值不止于 SSH 框架本身它的业务模块设计能直接迁移到电商类毕设里。购物车、订单状态、会员积分通常被学生写成一坨代码但这里用了相对规范的分层。重点看三个地方购物车怎么存、订单事务怎么控制、Apriori 算法怎么弱化。4.1 购物车设计Session 还是数据库项目里购物车使用了 Session 级存储用户在未登录状态下也能往购物车加商品登录后把 Session 中的购物车数据合并到数据库cart表。这种混存方案兼顾了转化率和一致性。核心加购逻辑如下public void addToCart(HttpServletRequest request, Integer goodsId, int quantity) { MapInteger, Integer cart (MapInteger, Integer) request.getSession() .getAttribute(cart); if (cart null) { cart new HashMapInteger, Integer(); request.getSession().setAttribute(cart, cart); } Integer oldQty cart.get(goodsId); cart.put(goodsId, oldQty null ? quantity : oldQty quantity); }这里的MapInteger, Integer键是商品 ID值是数量。优点是读写快、支持游客加购缺点是 Session 过期后数据丢失。所以支付前结算时会把 cart 里的数据遍历写入cart_item表。改造点也很明显如果要支持多端同步就该用 Redis 替换 Session键设计为cart:userId哈希字段为商品 ID值为数量。4.2 订单状态机与事务控制订单模块是整个系统里表关联最复杂的部分也是最容易扣分的答辩点。一张订单主表加一张明细表状态从“待支付”到“已支付”到“已发货”到“已完成”。状态变更集中在UserOrderAction中Transactional(rollbackFor Exception.class) public String submitOrder(HttpServletRequest request, Long userId, Long addrId) { Order order new Order(); order.setUserId(userId); order.setStatus(0); // 0待支付 1已支付 order.setCreateTime(new Date()); BigDecimal total computeTotal(userId); order.setTotalPrice(total); orderDao.save(order); ListCartItem items cartItemDao.findByUserId(userId); for (CartItem item : items) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setGoodsId(item.getGoodsId()); orderItem.setQuantity(item.getQuantity()); orderItem.setPrice(item.getPrice()); orderItemDao.save(orderItem); goodsDao.deduceStock(item.getGoodsId(), item.getQuantity()); } cartItemDao.deleteByUserId(userId); return payment; }这段代码最关键的是deduceStock方法在循环里逐条扣减库存。如果库存不足deduceStock内部会抛出RuntimeException整个事务回滚订单和明细都不会落库。但如果有人在deduceStock里 catch 了异常并 return false事务就不会回滚订单保存了库存却扣少了。正确做法是让 DAO 层抛异常由事务管理器统一处理。另一个细节是计算总价computeTotal应从数据库商品表查价格相乘而不是取前端传过来的价格否则用户可以伪造低价。答辩时提到这一点老师通常会认可你的安全意识。4.3 Apriori 类的推荐逻辑Apriori.class在这个项目里承担简单的关联推荐比如“买了提拉米苏的人还买了芒果班戟”。它使用经典 Apriori 算法挖掘频繁项集再生成关联规则。虽然项目里只用了两轮扫描但骨架是完整的public MapListInteger, Double mineRules(ListListInteger transactions, double minSupport, double minConfidence) { MapInteger, Integer itemCount new HashMapInteger, Integer(); for (ListInteger trans : transactions) { for (Integer item : trans) { Integer count itemCount.get(item); itemCount.put(item, count null ? 1 : count 1); } } ListInteger frequent1 new ArrayListInteger(); for (Map.EntryInteger, Integer e : itemCount.entrySet()) { if (e.getValue() minSupport * transactions.size()) { frequent1.add(e.getKey()); } } // 频繁1项集基础上做自连接生成候选2项集 ListListInteger candidate2 new ArrayListListInteger(); for (int i 0; i frequent1.size(); i) { for (int j i 1; j frequent1.size(); j) { ListInteger pair Arrays.asList(frequent1.get(i), frequent1.get(j)); candidate2.add(pair); } } // 统计候选2项集的支持度达到阈值则保留 MapListInteger, Double rules new HashMapListInteger, Double(); for (ListInteger cand : candidate2) { int count countSupport(transactions, cand); if (count minSupport * transactions.size()) { double confidence (double) count / itemCount.get(cand.get(0)); if (confidence minConfidence) { rules.put(cand, confidence); } } } return rules; }这个实现的问题是只挖到 2 项集对于“三件套套餐”这类场景就挖不出来了。但作为毕设已经够用答辩时你可以说这是为了控制计算时间而做的简化并指出改进方向是用递归生成 k 项集并做剪枝。注意minSupport是相对值比如 0.3 表示至少出现在 30% 的订单中订单量太少时它会失效所以回填推荐数据时要注意造数据量至少几百条订单。4.4 论坛与会员模块的边界论坛模块挂在一个独立的ForumAction下发帖、回帖、查看帖子权限全部只对登录用户开放。这个模块的核心表是帖子表回复表用户每次发帖系统会自动给用户增加积分积分累计到一定数量可以自助办理会员。会员办理本身不涉及支付流程只改用户表的member_type字段。会员模块的边界感做得不错会员打折逻辑在结算时由BizService统一处理而不是散落在 JSP 页面里。前端展示价格调用getMemberPrice()后台计算订单金额也走同一方法避免了“页面显示一个价、结算一个价”的经典 bug。5. 进阶把 SSH 项目改造成 Spring Boot 的增量迁移SSH 项目现在被提问最多的场景不是直接上线而是如何在保留业务逻辑的前提下迁移到 Spring Boot。这个项目结构刚好适合练手Struts2 的 Action 可以平滑替换成 Spring MVC 的 ControllerHibernate 换不换都行。下面给出一个低风险的迁移路径。5.1 保留 Hibernate替换 Struts2 为 Spring MVC先别动 DAO 层把struts.xml里的 action 映射逐一对应成 Controller。原GoodsAction的list()方法迁移后如下Controller RequestMapping(/goods) public class GoodsController { Autowired private BizService bizService; RequestMapping(/list) public String list(Model model, Integer categoryId, Integer pageNum) { PageResultGoods page bizService.queryGoods(categoryId, pageNum, 12); model.addAttribute(page, page); return goodsList; } }迁移时注意三个变化参数不再靠 Struts2 拦截器自动注入而是通过方法入参绑定返回字符串时不再写success而是直接写视图名重定向要用redirect:/goods/list不能返回 JSP 路径。原来在struts.xml里配置的result nameerror变成ExceptionHandler统一处理。5.2 用 Spring Data JPA 重写 DAO如果连 Hibernate 的 XML 映射也想一起换可以把BaseHibernateDao替换成 Spring Data JPA 仓库。例如商品的单表查询可以这样写public interface GoodsRepository extends JpaRepositoryGoods, Integer { Query(select g from Goods g where g.status 1 and g.name like concat(%, ?1, %)) PageGoods searchOnSale(String keyword, Pageable pageable); Modifying Transactional Query(update Goods g set g.stock g.stock - ?2 where g.id ?1 and g.stock ?2) int deduceStock(Integer goodsId, Integer quantity); }这段里deduceStock的where g.stock ?2是关键它在数据库层面避免了超卖。返回值是影响行数如果为 0 说明库存不足业务层通过判断返回值来抛异常事务配合回滚。原来的BaseHibernateDao里queryByHql那套手写 HQL 的方式就不需要了但分页的Pageable和排序逻辑要重新对齐PageResult的分页参数。5.3 迁移后的支付宝沙箱适配迁移到 Spring Boot 后支付宝回调地址变成了PostMapping(/order/notify)并且要用RequestBody或HttpServletRequest接收参数。旧项目的AlipayConfig静态变量可以整理成ConfigurationProperties配置类alipay: app-id: 2021003122690000 private-key: MIIEvQIBADANBg... alipay-public-key: MIIBIjANBg... notify-url: https://yourdomain.com/order/notify return-url: https://yourdomain.com/order/return在 Controller 里通过Value或构造器注入读取这些配置。注意 Spring Boot 内置 Tomcat 默认只允许单次请求体读取如果用getParameter方式获取支付宝回调需要把请求包装类留着否则在验签时拿不到原始参数。5.4 验证迁移结果的回归用例迁移完成后用最笨但也最可靠的方式验证把原项目的数据库脚本原样导入写一组针对核心链路的集成测试。下面是一个最小验证用例覆盖“加购 → 下单 → 扣库存”主链SpringBootTest Transactional class OrderFlowTest { Autowired private GoodsRepository goodsRepository; Autowired private OrderService orderService; Test void testSubmitOrderDeductStock() { Integer goodsId 1; int before goodsRepository.findById(goodsId).get().getStock(); OrderDTO dto new OrderDTO(); dto.setUserId(1001L); dto.setAddrId(5L); dto.setItems(Arrays.asList(new OrderItemDTO(goodsId, 2))); orderService.submit(dto); int after goodsRepository.findById(goodsId).get().getStock(); assertTrue(库存应减少2, before - after 2); } }测试类事务会自动回滚不会污染数据库。如果这个测试通过再分别验证超卖场景并发下提交 3 个各购买 2 件的订单库存只有 5最终应只有 2 个订单成功。这一步能同时验证事务边界和 SQL 的原子操作用得对不对。迁移完之后你会发现原项目的业务逻辑完全保留只是骨架从 SSH 换成了 Spring Boot这对面试讲“重构老项目”是很好的素材。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门