☰
SSH三层架构实战:新百货供应链系统与MySQL落地避坑指南
2026/10/6 12:47:25 网站建设 项目流程

简介:这份资源是面向JavaEE初学者与课程设计/毕业设计开发者的SSH框架实战项目,基于Struts+Spring+Hibernate与MySQL实现百货中心供应链管理系统,可用于学习企业级分层开发、理解供应链业务数据流转,也适合作为毕设选题参考。压缩包共16个文件,约64.24MB,包含Retail_Supply_Chain_System.zip源码包、chain_2014-05-05.sql数据库脚本、毕业设计说明书doc文档、项目截图png以及两段mp4项目辅导视频,覆盖部署启动与数据统计、采购管理、合作公司管理等模块演示。系统实现管理员登录、合作公司信息增改查、采购信息维护与数据统计分析等功能,源码与数据库可直接导入运行,配合论文文档与录屏能帮助读者快速理清SSH整合思路、表结构设计与模块调用关系。目前已有221人学习下载,适合需要完整项目案例与排错参考的开发者。

1. 新百货供应链系统:SSH 三层架构 + MySQL 落地的真实门槛

接手一个「新百货中心供应链管理系统」的毕设或二次开发需求时,多数人第一反应是打开 IDE 新建工程,结果卡在 Struts 配置、Spring 注入和 Hibernate 映射这三座大山之间。这个标题指向的是一套典型的 JavaEE SSH(Struts2 + Spring + Hibernate)三层架构 Web 应用,配套 MySQL 数据库脚本,业务域覆盖百货供应链的采购、库存、供应商与商品流转。它解决的是「一套能跑起来、能改、能交差的供应链后台」这个具体诉求,适合正在做课程设计、需要快速理解 SSH 整合套路、或想拿一套完整业务系统练手 MySQL 建模的开发者。源码和数据库 SQL 是这套东西的两个核心交付物,缺一不可——只有代码没有库,跑不起来;只有库没有代码,看不懂业务。下面按「先立架构认知、再动手复现、最后避坑」的顺序拆开讲。

2. SSH 三层架构与供应链业务域的对应关系

2.1 为什么供应链系统偏爱 SSH 这套组合

供应链系统的核心特征是「表单密集、状态流转多、实体关系复杂」。采购单、入库单、出库单、供应商档案、商品分类,这些实体之间存在一对多、多对多的关联,而且每个业务动作都伴随数据库事务。SSH 的分工恰好对上:Struts2 负责接收前端表单请求并做参数校验,Spring 用 IoC 管理 Service 层和 DAO 层的依赖、用 AOP 声明事务边界,Hibernate 把实体关系映射成表关联、把级联操作交给 ORM 处理。

我一般会这样理解三者的边界:Struts2 的 Action 只做「收参数、调 Service、选结果视图」,不写业务判断;Spring 的 Service 层是事务的唯一入口,一个采购入库动作要么全成功要么全回滚;Hibernate 的 DAO 层只做持久化,不掺业务逻辑。这个边界一旦模糊,后期改一个字段会牵动三层,维护成本陡增。

选型理由上,SSH 相比后来的 Spring Boot + MyBatis,配置量大但结构显式,对理解「一个请求怎么穿过三层到达数据库」特别直观。供应链这种业务规则稳定的系统,用 Hibernate 的 HQL 和级联能省掉大量手写 SQL;而 MySQL 作为关系型库,对采购单-明细这种主子表结构支持成熟,事务和索引也够用。

2.2 从数据库 SQL 反推实体与映射

拿到数据库 SQL 后,不要急着写 Java 类。先读建表语句,把主外键关系画出来,再决定 Hibernate 的映射策略。供应链系统常见的表包括:供应商表(supplier)、商品表(product)、采购单主表(purchase_order)、采购单明细表(purchase_order_item)、库存表(inventory)。主外键关系决定了<many-to-one>和<one-to-many>的配置方向。

-- 采购单主表:一个采购单对应多个明细 CREATE TABLE purchase_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, supplier_id BIGINT NOT NULL, status TINYINT DEFAULT 0, -- 0待审核 1已入库 2已取消 total_amount DECIMAL(12,2), create_time DATETIME, CONSTRAINT fk_order_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 采购单明细表:多条明细挂在一个采购单下 CREATE TABLE purchase_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES purchase_order(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

上面两段 SQL 的关键点:order_no加唯一约束防止重复下单;status用 TINYINT 表示状态机,这是供应链系统状态流转的常见做法;外键约束保证明细不会挂到不存在的采购单上。字符集统一用 utf8mb4,避免商品名里的特殊字符乱码。

对应的 Hibernate 映射里,PurchaseOrder实体持有Set<PurchaseOrderItem>,配置cascade="save-update"和inverse="true",让主表控制级联保存、明细表放弃维护外键。这里最容易翻车的是inverse配反,导致明细的外键字段为 null,插入时报约束错误。

2.3 用 Maven 搭出可运行的 SSH 骨架

手工往 WEB-INF/lib 里塞 jar 包是血泪经验里最费时的一步,版本冲突能查一整天。常见做法是用 Maven 管理依赖,把 Struts2、Spring、Hibernate 的版本锁死在一组兼容组合上。

<!-- pom.xml 关键依赖,版本需自行确认兼容性 --> <dependencies> <!-- Struts2 核心 --> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.5.x</version> </dependency> <!-- Spring 整合 Struts2 的插件 --> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-spring-plugin</artifactId> <version>2.5.x</version> </dependency> <!-- Spring 上下文与事务 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.x</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>5.x</version> </dependency> <!-- Hibernate 核心 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.x</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.x</version> </dependency> </dependencies>

依赖说明:struts2-spring-plugin是让 Struts2 的 Action 交给 Spring 管理的桥梁,没有它 Action 里的 Service 注入会是 null;spring-orm提供 Hibernate 的事务集成;MySQL 驱动版本要和数据库服务端匹配,8.x 驱动连 5.7 库需要显式指定时区和useSSL参数。版本号这里不写死,因为不同小版本间 API 有差异,以实际能编译通过为准。

搭好骨架后,applicationContext.xml里配数据源、SessionFactory、事务管理器和 Service 的 bean;struts.xml里配 Action 与结果视图的映射。两个配置文件的 namespace 和 bean id 要对得上,否则启动时报找不到 bean。

3. 从 SQL 脚本到可登录系统的完整复现步骤

3.1 导入数据库 SQL 并核对字符集

第一步是把数据库 SQL 脚本导入 MySQL。用命令行或客户端工具都行,关键是导入后核对字符集和引擎。

# 创建库并指定字符集,避免导入后中文乱码 mysql -u root -p -e "CREATE DATABASE supply_chain DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;" # 导入 SQL 脚本 mysql -u root -p supply_chain < supply_chain.sql # 核对表是否建全、引擎是否为 InnoDB mysql -u root -p -e "USE supply_chain; SHOW TABLES; SHOW TABLE STATUS LIKE 'purchase_order';"

命令含义:第一条建库时显式指定 utf8mb4,比依赖默认字符集可靠;第二条把脚本灌进指定库;第三条用SHOW TABLE STATUS确认引擎是 InnoDB,因为只有 InnoDB 支持外键和事务,MyISAM 会让外键约束失效。如果导入报「Unknown character set」,说明脚本里写了当前 MySQL 版本不支持的字符集名,需要手动替换。

导入后一定要抽查几张核心表的数据量和字段类型,尤其是金额字段用 DECIMAL 而非 FLOAT,状态字段用整型而非字符串。这些细节决定了后面 Java 实体映射时会不会出现精度丢失或类型转换异常。

3.2 配置数据源与 Hibernate 会话工厂

数据源和 SessionFactory 是 SSH 启动时最容易出问题的地方。Spring 配置文件里,数据源用连接池(常见是 Druid 或 C3P0),SessionFactory 挂上 Hibernate 属性。

<!-- applicationContext.xml 片段 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/supply_chain?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false"/> <property name="username" value="root"/> <property name="password" value="你的密码"/> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="packagesToScan" value="com.example.entity"/> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL8Dialect</prop> <prop key="hibernate.show_sql">true</prop> <prop key="hibernate.format_sql">true</prop> <prop key="hibernate.hbm2ddl.auto">validate</prop> </props> </property> </bean>

参数说明:URL 里的serverTimezone必须显式指定,否则 8.x 驱动连接时报时区错误;useSSL=false在本地开发环境避免 SSL 握手问题;hibernate.dialect要和 MySQL 大版本对应,用错方言会导致分页 SQL 生成异常;hbm2ddl.auto设为validate只校验实体与表是否匹配,绝不设update或create,否则可能改坏已有表结构。packagesToScan指向实体包,让 Hibernate 自动扫描注解或映射文件。

3.3 打通一个采购单查询的请求链路

配置就绪后,用一条最简单的查询链路验证三层是否打通:前端请求 → Struts2 Action → Spring Service → Hibernate DAO → MySQL。

// Action 层:只负责接收参数和返回结果 public class PurchaseOrderAction extends ActionSupport { private PurchaseOrderService orderService; // 由 Spring 注入 private List<PurchaseOrder> orderList; public String list() { orderList = orderService.findAll(); // 调 Service,不写业务 return SUCCESS; } // getter/setter 省略 } // Service 层:事务边界在这里 @Service @Transactional public class PurchaseOrderServiceImpl implements PurchaseOrderService { private PurchaseOrderDao orderDao; public List<PurchaseOrder> findAll() { return orderDao.findAll(); // 事务由注解控制 } // setter 注入省略 } // DAO 层:只做持久化 @Repository public class PurchaseOrderDaoImpl extends HibernateDaoSupport implements PurchaseOrderDao { public List<PurchaseOrder> findAll() { return (List<PurchaseOrder>) getHibernateTemplate() .find("from PurchaseOrder order by createTime desc"); } }

逻辑说明:Action 通过struts2-spring-plugin由 Spring 容器创建,orderService自动注入;Service 上的@Transactional声明事务,查询方法用只读事务更优;DAO 继承HibernateDaoSupport拿到HibernateTemplate,用 HQL 查询避免手写 SQL。三层各司其职,任何一层出问题都能单独定位。跑通这条链路后,采购单的新增、审核、入库等动作就是在这套模板上扩展。

3.4 用 DBeaver 或命令行验证数据落库

请求跑通后,别只看页面有没有报错,要回到数据库确认数据真的写进去了。用 DBeaver 连接时如果看不到表,先检查连接的是不是目标库、schema 是否选对。命令行验证更直接:

-- 确认采购单和明细是否成对写入 SELECT o.order_no, o.status, o.total_amount, COUNT(i.id) AS item_count FROM purchase_order o LEFT JOIN purchase_order_item i ON i.order_id = o.id GROUP BY o.id ORDER BY o.create_time DESC LIMIT 10;

这条 SQL 用左连接统计每张采购单的明细条数,能快速发现「主表有记录但明细为空」的级联保存失败问题。如果item_count全是 0,多半是 Hibernate 映射里cascade没配或inverse配反。供应链系统的数据一致性要求高,每次改动后都该用这类核对 SQL 验一遍,而不是只信页面提示。

4. SSH 整合与 MySQL 落地的高频翻车点排查

4.1 启动报 NoSuchMethodError 或 ClassNotFoundException

现象:Tomcat 启动时抛NoSuchMethodError,或某个类找不到,堆栈指向 Struts 或 Spring 内部。

原因:依赖版本冲突。SSH 三件套各自依赖一批公共库(如 javassist、commons-lang、asm),Maven 传递依赖拉进来多个版本,运行时加载了不兼容的那个。

解决:用mvn dependency:tree打印依赖树,找出重复的 groupId+artifactId,在 pom 里用<exclusions>排除旧版本,或统一用<dependencyManagement>锁版本。别靠手动删 jar 包,治标不治本。

4.2 Action 里的 Service 注入为 null

现象:页面能打开,但一提交表单就报空指针,日志显示 Service 字段是 null。

原因:Struts2 的 Action 默认由 Struts 自己 new 出来,不走 Spring 容器,所以@Autowired或 setter 注入不生效。

解决:确认struts2-spring-plugin已引入,且struts.xml里 Action 的class属性写的是 Spring bean id 而不是全限定类名。同时struts.objectFactory要指向 spring。这是 SSH 整合最经典的坑,配一次记一辈子。

4.3 中文乱码从表单一路乱到数据库

现象:页面输入的中文,存进 MySQL 变成问号或乱码。

原因:字符集在三个环节可能不一致——JSP 页面编码、Struts2 请求编码、数据库连接和表字符集。

解决:JSP 页头声明pageEncoding="UTF-8";Struts2 配置常量struts.i18n.encoding=UTF-8;JDBC URL 带characterEncoding=utf8;建库建表用 utf8mb4。四个环节逐个核对,缺一个都会乱。别只改数据库,那样治不了根。

4.4 Hibernate 级联保存时明细外键为 null

现象:保存采购单时主表插入成功,明细表也插入了,但order_id字段是 null,外键约束报错或产生孤儿数据。

原因:一对多映射里inverse属性配错。inverse="false"表示由当前方维护关联,inverse="true"表示放弃维护。主表一方应设inverse="true",让明细一方通过<many-to-one>维护外键。

解决:检查映射文件,主表集合配inverse="true" cascade="save-update",明细的<many-to-one>配not-null="true"。保存时先建立双向引用再 save 主表,让级联生效。

4.5 MySQL 8 驱动连接报时区或 SSL 错误

现象:启动时报The server time zone value is unrecognized或 SSL 相关异常。

原因:MySQL 8.x 驱动默认要求显式时区,且默认尝试 SSL 连接。

解决:JDBC URL 加serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true。生产环境该开 SSL 就开,本地开发关掉省事。allowPublicKeyRetrieval在 caching_sha2_password 认证方式下常需要,不加会报公钥检索失败。

5. 让这套系统真正可维护的两个进阶习惯

第一个习惯是给状态流转加显式校验,而不是靠页面按钮控制。供应链系统的采购单有「待审核→已入库→已取消」这类状态机,如果只在 JSP 上根据状态决定显示哪个按钮,绕过页面直接调接口就能把已取消的单子改成已入库。我一般会在 Service 层写一个状态校验方法,任何状态变更前先判断当前状态是否允许该操作,不允许就抛业务异常。这样即使前端被绕过,数据也不会进入非法状态。验证方法是写几个单元测试,直接调 Service 传非法状态,看是否按预期抛异常。

第二个习惯是给核心查询建索引并用执行计划验证。供应链系统里「按供应商查采购单」「按时间段查入库记录」是高频查询,如果supplier_id、create_time上没索引,数据量一上来查询就慢。建完索引后用EXPLAIN看是否走索引:

EXPLAIN SELECT * FROM purchase_order WHERE supplier_id = 1001 AND create_time BETWEEN '2024-01-01' AND '2024-12-31';

看type列是不是ref或range,key列有没有命中索引。如果type是ALL,说明全表扫描,得调整索引或改写 SQL。索引不是越多越好,写多读少的表加太多索引会拖慢插入,这个度要按实际业务量权衡。

我踩过最深的坑是早期图省事,把hbm2ddl.auto设成update,结果一次实体改名直接把生产表的列改了,数据差点没救回来。从那以后,任何环境都只用validate,表结构变更一律走 SQL 脚本人工审核。这套 SSH 供应链系统结构不复杂,但正因为显式,每个配置项都值得较真。希望帮到你。

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

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

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

立即咨询