SSH整合实战:Struts2+Hibernate+Spring超市系统解析 简介这份基于Struts2HibernateSpring框架的超市信息管理系统源码面向正在学习Java EE三大框架整合的开发者、高校学生及课程设计人群。项目完整涵盖商品浏览、购物车管理、订单处理、用户登录与权限控制等典型电商业务模块真实演示了Struts2控制请求流转、Hibernate完成对象关系映射、Spring负责业务层解耦与事务管理的协同方式。资源包共171个文件约3.22MB主要包含24个Java源码文件、16个JSP页面、22个XML配置文件并配有CSS、JavaScript、GIF/PNG图片等前端资源便于直接部署与二次开发。压缩包内目录分层合理除核心业务源码外还包含相应class文件与数据库映射文件可帮助读者结合前端页面与后端代码快速定位逻辑。通过研读源码能有效提升SSH整合开发能力也可作为毕业设计或课程设计的完整参考。目前已有79人学习下载。1. 这套 Struts2HibernateSpring 超市系统源码先看它到底解决什么问题超市信息管理系统是 SSH 框架最常见的落地练习有登录、有商品的增删改查、有库存和销售流水几乎能覆盖 Struts2、Hibernate、Spring 三者的全部配合场景。但很多人打开这类源码包时第一关不是业务代码而是框架版本连不上Tomcat 能起页面一提交就 404SQL 能打印页面一访问集合就报 LazyInitializationException。问题不在业务逻辑在三个框架的整合方式。Struts2 负责请求路由Hibernate 负责把商品、类别这些表变成对象Spring 负责在中间管理 Service、DAO 和事务。这篇我把这套 SSH 方案拆成可复现的骨架先讲环境怎么搭再讲 one-to-many 映射怎么配置、Action 怎么从 Spring 拿服务、事务边界放哪最后给老代码的改造落点。2. 先搭 Struts2HibernateSpring 骨架用 Maven 和 web.xml 把三个框架接起来2.1 依赖版本搭配为什么老源码喜欢在 lib 目录塞 jar解压这类超市系统源码第一眼看到的往往不是 pom.xml而是 WebContent/WEB-INF/lib 下十几 MB 的 jar 包。这种方式在当年可以省掉 Maven 配置但带来的问题很直接同一个 jar 有多份版本、Spring 和 Hibernate 版本不匹配、Struts2 插件缺失。只要 Tomcat 启动时打印 ClassNotFoundException 或 NoSuchMethodError第一排查点就是 lib 目录。我建议拿到源码后先不急着改业务代码而是用 Maven 重新管理依赖保证每个关键 jar 只有一份。下面这组版本组合适合 Struts2HibernateSpring 的典型工程稳定且互相兼容框架Maven 坐标推荐版本Struts2 核心org.apache.struts2:struts2-core2.5.5Struts2-Spring 插件org.apache.struts2:struts2-spring-plugin2.5.5Hibernate 核心org.hibernate:hibernate-core5.2.12.FinalSpring Weborg.springframework:spring-web4.3.18.RELEASESpring ORMorg.springframework:spring-orm4.3.18.RELEASE注意三个配合点struts2-spring-plugin 的版本必须和 struts2-core 保持一致否则过滤器初始化时会因为类版本不一致直接报错Spring 4.3 对 Hibernate 5.2 的支持已经很成熟LocalSessionFactoryBean、HibernateTransactionManager都能用如果源码包里的 Hibernate 是 3.xSpring 侧要改成org.springframework.orm.hibernate3.LocalSessionFactoryBean包名差一个数字类就完全不同。提示如果不用 Maven手动放 jar 时重点检查 hibernate-core、spring-orm、struts2-core、struts2-spring-plugin 四个文件其余工具包重复一两个版本通常不会致命。2.2 web.xml 先定过滤器顺序SSH 整合的 web.xml 里通常有四个角色Spring 的 ContextLoaderListener、编码过滤器、OpenSessionInViewFilter、Struts2 的 StrutsPrepareAndExecuteFilter。它们不是随意堆在 web.xml 里就行过滤器顺序直接决定页面访问懒加载对象会不会报错。web-app xmlnshttp://java.sun.com/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd version2.5 context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener filter filter-nameencoding/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencoding/filter-name url-pattern/*/url-pattern /filter-mapping filter filter-nameopenSession/filter-name filter-classorg.springframework.orm.hibernate5.support.OpenSessionInViewFilter/filter-class /filter filter-mapping filter-nameopenSession/filter-name url-pattern/*/url-pattern /filter-mapping filter filter-namestruts2/filter-name filter-classorg.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter/filter-class /filter filter-mapping filter-namestruts2/filter-name url-pattern/*/url-pattern /filter-mapping welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list /web-app这段配置是整套工程的门户。ContextLoaderListener 会按照contextConfigLocation加载 Spring 容器这个容器要能被 Struts2 的 Action 访问后面必须在 struts.xml 里设置struts.objectFactoryspring编码过滤器放在最前面保证 POST 提交的中文不乱码OpenSessionInViewFilter 必须放在 Struts2 过滤器之前它会在请求进 Action 前把 Hibernate Session 绑定到当前线程等 JSP 渲染完再关闭这样页面遍历商品类别时才不会抛 LazyInitializationException。如果反过来把 Struts2 过滤器放在最前请求在 Action 里拿到的数据没问题但一旦页面访问category.products这类懒加载集合Session 已经关了JSP 直接报错。再有Struts2 2.5 的过滤器类是org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter2.3 及更早版本是org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter中间多了.ng照着旧博客抄配置时最容易在这里栽跟头。2.3 源码包中常见的目录结构这类超市管理系统的源码结构大同小异按二层分包组织虽然不算现代但职责非常清楚src/main/java ├── com/example/supershop/action # Struts2 动作类 ├── com/example/supershop/service # Spring 业务接口与实现 ├── com/example/supershop/dao # Hibernate 数据访问层 ├── com/example/supershop/entity # 实体类与 hbm 映射文件 └── com/example/supershop/util # 分页、MD5 等工具类 src/main/resources ├── applicationContext.xml # Spring 容器 ├── struts.xml # Struts2 路由 └── jdbc.properties # 数据源配置明确这个结构的意义在于遇到报错时能快速定位。启动报错看 applicationContext.xml 和实体映射点击页面没反应看 struts.xml 的 namespace 和 action name业务逻辑不对看 service 实现SQL 不对看 dao 里的 HQL 或 Criteria 写法。源码包拿到手先按这个结构归位比直接在 Tomcat 里跑更有效率。3. 用 Hibernate 的 one-to-many 把商品和类别映射清楚3.1 商品-类别先定主键和表关系超市系统的核心数据模型是商品、类别、库存和销售记录。其中商品和类别是典型的一对多关系一个类别下有多个商品一个商品只属于一个类别。这个关系在 SSH 老工程里通常用两张表表达外键放在多的一方也就是商品表。create table t_category ( id int primary key auto_increment, category_name varchar(50) not null ); create table t_product ( id int primary key auto_increment, product_name varchar(100) not null, price decimal(10,2), category_id int not null, constraint fk_product_category foreign key (category_id) references t_category(id) );外键约束的引入不只是为了数据库一致性更重要的是它决定了 Hibernate 映射里inverse和cascade的配置方向关联关系的维护端在商品表类别表只是被关联。很多新手把一对多映射配置成两边都维护外键结果保存时 Hibernate 会额外发出多条 update 语句甚至造成主键冲突。3.2 双向关联映射Category 一端 setProduct 一端 many-to-one实体类对应的映射文件是这个项目里最值得看的配置。hibernate 配置 one-to-many 时我的习惯是配置成双向关联但在类别一端的set上设置inversetrue让商品端来维护外键。!-- Category.hbm.xml -- class namecom.example.supershop.entity.Category tablet_category id nameid generator classnative/ /id property namename columncategory_name/ set nameproducts inversetrue cascadesave-update lazytrue key columncategory_id/ one-to-many classcom.example.supershop.entity.Product/ /set /class!-- Product.hbm.xml -- class namecom.example.supershop.entity.Product tablet_product id nameid generator classnative/ /id property namename columnproduct_name/ property nameprice columnprice/ many-to-one namecategory classcom.example.supershop.entity.Category columncategory_id not-nulltrue lazyproxy/ /class这里几个参数是实际排错的关键。inversetrue表示关系的维护权交给商品端Category 对象的 products 集合只是读取用不参与外键更新cascadesave-update表示保存类别时如果集合里有新商品会一起持久化但我在业务代码里通常不依赖这个级联而是显式在 Service 里设置商品的外键逻辑更直白many-to-one的lazyproxy表示商品加载时类别只返回一个代理对象等到真正访问product.getCategory().getName()才发 SQL这是 one-to-many 关联最常见的懒加载行为。映射文件配置好之后Spring 要把映射资源加载到 SessionFactory。下面这段是 applicationContext.xml 里必配的 sessionFactory注意 mappingResources 的路径要和 hbm.xml 的包路径一致bean idsessionFactory classorg.springframework.orm.hibernate5.LocalSessionFactoryBean property namedataSource refdataSource/ property namemappingResources list valuecom/example/supershop/entity/Category.hbm.xml/value valuecom/example/supershop/entity/Product.hbm.xml/value /list /property property namehibernateProperties props prop keyhibernate.dialectorg.hibernate.dialect.MySQL5InnoDBDialect/prop prop keyhibernate.show_sqltrue/prop prop keyhibernate.format_sqltrue/prop /props /property /bean3.3 用 SessionFactory 的当前会话执行保存Spring 整合 Hibernate 之后DAO 里不再手动 openSession而是通过sessionFactory.getCurrentSession()获取和事务绑定的 Session。只有开启事务这个方法才能拿到 Session事务结束Session 自动关闭。这样写的好处是避免 Session 泄漏也让 Spring 的事务切面真正控制数据库连接的生命周期。Repository public class ProductDao { Autowired private SessionFactory sessionFactory; public void save(Product product) { sessionFactory.getCurrentSession().save(product); } }对应的 Service 在保存商品时只需要把商品挂到已有的 Category 上然后保存商品即可。因为inversetrue的关系这里不需要去products集合里 addService public class ProductService { Autowired private ProductDao productDao; Autowired private CategoryDao categoryDao; public void addProduct(Product product, Integer categoryId) { Category category categoryDao.findById(categoryId); product.setCategory(category); productDao.save(product); } }getCurrentSession 的调用必须在事务范围内也就是说 Service 方法必须被 Spring 事务管理覆盖。如果没配事务或方法名没匹配到事务规则会看到Could not obtain transaction-synchronized Session for current thread这类报错这是 SSH 工程里最常见的 Hibernate 集成错误。4. 用 Struts2 管请求流转登录、商品查询与进货入库4.1 struts.xml 的 namespace 与 resultStruts2 在这套系统里的定位是 MVC 的控制器层所有页面提交的请求都先进 Struts2 过滤器然后由 struts.xml 决定交给哪个 Action。下面的配置是超市后台最基础的一组路由struts constant namestruts.objectFactory valuespring/ constant namestruts.devMode valuetrue/ package nameshop namespace/admin extendsstruts-default action namelogin classloginAction methodlogin result namesuccess/main.jsp/result result nameerror/login.jsp/result /action action nameproductQuery classproductAction methodquery result namesuccess/admin/product_list.jsp/result /action action namestockIn classstockAction methodstockIn result namesuccess typeredirectActionproductQuery/result result nameerror/admin/stock_in.jsp/result /action /package /struts这段配置做了三件事通过struts.objectFactoryspring让 Spring 接管 Action 的创建注意此时 action 的class属性填的是 Spring 容器里的 bean 名称不是全类名namespace/admin决定了页面 form 的提交地址必须以/admin/开头例如登录提交到admin/loginresult的name必须和 Action 方法返回的字符串完全对应登录失败返回error页面就跳回 login.jsp。stockIn的typeredirectAction是个细节入库成功后跳回商品查询 Action避免用户刷新页面时重复提交入库请求。Struts2 的 result 默认是 dispatcher 转发redirectAction 会发起一次新请求URL 变成/admin/productQuery这在写增删改操作时是必须养成的习惯。4.2 Action 实例为什么必须 prototypeStruts2 的 Action 原本是每个请求创建新实例但把 Action 交给 Spring 管理后容器默认是单例。如果不加Scope(prototype)两个登录请求同时进来可能会互相覆盖 username 和 password 这两个成员变量。这个坑在并发用户一多就会偶现明明 A 用户登录成功页面显示的却是 B 用户的信息。Component(loginAction) Scope(prototype) public class LoginAction implements Action { private String username; private String password; Autowired private UserService userService; public String login() { if (userService.checkLogin(username, password)) { return SUCCESS; } return ERROR; } public String getUsername() { return username; } public void setUsername(String username) { this.username username; } public String getPassword() { return password; } public void setPassword(String password) { this.password password; } }这段代码体现了 SSH 的典型协作方式username 和 password 由 Struts2 的 params 拦截器从 HTTP 请求中取出通过 setter 注入userService 由 Spring 的 Autowired 注入Action 内部不做 Service 的 new 操作。返回 SUCCESS 和 ERROR 都是字符串常量对应 struts.xml 里的 result 配置。提示Spring 管理 Action 时Scope(prototype)不是可选配置。单例 Action 的成员变量会成为共享状态商品查询这种带分页参数的 Action 尤其容易出错。4.3 JSP 如何拿到 Action 里的数据查询商品列表时Action 把结果放到成员变量里JSP 直接通过s:iterator遍历。下面这段代码在商品管理页非常常见table border1 tr th商品名称/th th类别/th th价格/th /tr s:iterator valueproductList statusst tr tds:property valuename//td tds:property valuecategory.name//td tds:property valueprice//td /tr /s:iterator /tables:iterator默认从值栈栈顶查找productList也就是 Action 的getProductList()返回值category.name的访问会触发 Product 到 Category 的懒加载这正是 web.xml 中 OpenSessionInViewFilter 存在的意义。如果把 openSession 过滤器去掉这一行会在 JSP 渲染阶段抛 LazyInitializationException而不是在 Action 里。这套超市系统里Action 入口通常按下面这张表来规划每个 Action 只做一件职责清晰的事Action 名称方法对应功能返回页面loginActionlogin用户登录校验/main.jsp 或 /login.jspproductActionquery商品分页查询/admin/product_list.jspproductActionsave新增或修改商品重定向到 querystockActionstockIn进货入库重定向到 querystockActionsellOut销售出库重定向到 query新加功能时先在这个表里补一行再去写 Action 方法和 JSP整个请求链路不会乱。5. Spring 把 Service 层串起来事务配置与 Struts2HibernateSpring 联调排错5.1 事务边界放在 Service 层事务放在 DAO 层还是 Service 层这是 SSH 工程里最容易含糊的地方。一个入库操作通常包含两步更新库存表、插入进货流水表如果两个 DAO 各自开启事务第一步成功第二步失败时库存已经变了但流水没记录数据对不上。事务边界应该放在 Service 层让一次业务操作里的多个 DAO 调用共用同一个事务。Spring 声明式事务的标准配置如下用 AOP 切面拦截 service 包下所有方法bean idtransactionManager classorg.springframework.orm.hibernate5.HibernateTransactionManager property namesessionFactory refsessionFactory/ /bean tx:advice idtxAdvice transaction-managertransactionManager tx:attributes tx:method namesave* propagationREQUIRED/ tx:method nameadd* propagationREQUIRED/ tx:method nameupdate* propagationREQUIRED/ tx:method namedelete* propagationREQUIRED/ tx:method namequery* read-onlytrue/ /tx:attributes /tx:advice aop:config aop:pointcut idserviceTx expressionexecution(* com.example.supershop.service..*.*(..))/ aop:advisor advice-reftxAdvice pointcut-refserviceTx/ /aop:config这个配置文件的作用是把事务切到 service 包下所有方法。方法名以 save、add、update、delete 开头时使用 REQUIRED 传播行为也就是有事务就加入没有就新建query 开头的方法标记为 read-onlyHibernate 会跳过脏检查性能更好。方法命名规则在 SSH 老工程里特别重要因为事务切面靠方法名匹配如果方法名叫doSomething而内部有写操作事务不会生效数据在异常时无法回滚。5.2 Struts2HibernateSpring 联调最常见的 5 个报错与对策这套框架组合的报错信息往往不在 Tomcat 控制台第一行而是藏在 Caused by 后面。下面是我处理 SSH 工程问题时的排查对照表报错现象直接原因处理方式ClassNotFoundException: LocalSessionFactoryBeanspring-orm 版本与 Hibernate 版本不匹配检查 spring-orm 是否用了 hibernate5 包Hibernate 3 与 5 的类完全不同NoSuchMethodError: SessionFactory.openSessionlib 或 Maven 里存在多个 hibernate-core 版本只保留一个 hibernate-core jar最好用 Maven 排除传递依赖Could not obtain transaction-synchronized SessionService 方法没匹配到事务切面检查方法名是否匹配 txAdvice 的规则检查事务切点包名LazyInitializationException页面访问懒加载集合时 Session 已关闭检查 web.xml 中 OpenSessionInViewFilter 是否在 Struts2 过滤器之前struts.objectFactory 不生效缺少 struts2-spring-plugin引入与 struts2-core 同版本的 struts2-spring-plugin第一条报错最能说明版本问题的隐蔽性hibernate-core 是 5.2 的 jarspring-orm 却是针对 Hibernate 3 的配置类类名都在但方法和签名对不上只会在运行时报错。处理这类问题优先用 Maven 的dependency:tree看依赖来源而不是在 lib 目录里手工删 jar。5.3 这和 Spring Boot 框架的区别在哪我经常把 SSH 和 Spring Boot 对照着看因为两者的学习曲线完全不同。SSH 里你手动把 web.xml、struts.xml、applicationContext.xml 三个配置文件串起来Spring Boot 则默认用内嵌 Tomcat 和自动配置Struts2 在 Spring Boot 里反而很少见大部分项目直接用 Spring MVC 或若依这类基于 Spring Boot 的前后端分离系统。但 SSH 里的几个核心问题在 Spring Boot 里依然存在懒加载边界、事务传播行为、循环依赖。比如 Spring 三级缓存原理解决的就是单例 Bean 循环依赖问题SSH 工程里 Action、Service、DAO 之间经常出现类似结构但因为大家都用字段注入Spring 会先用三级缓存暴露早期引用等属性赋值完成后再补齐依赖。理解这一层再看 Spring Boot 的启动过程会轻松很多。SSH 老工程不是没用它是理解 Spring 容器的入门底板。6. 把 Struts2HibernateSpring 老工程改造成能看懂、能交接的代码6.1 给 Action 抽一个 BaseAction老源码里的 Action 通常每个类都写HttpServletRequest req ServletActionContext.getRequest()代码重复度高。抽一个 BaseAction 放共享成员既能少写几行也让新人一眼看出 Action 能访问什么public class BaseAction implements Action { protected HttpServletRequest request; protected HttpServletResponse response; protected HttpSession session; public String execute() { return SUCCESS; } protected void initRequest() { request ServletActionContext.getRequest(); response ServletActionContext.getResponse(); session request.getSession(); } }子类 Action 只需要在业务方法第一行调用initRequest()后续所有操作都能直接使用 request 和 session。Struts2 的 ServletActionContext 是线程绑定的不推荐把它长期存成静态字段但放在原型 Action 的实例成员里没有问题。6.2 给 DAO 层抽一个泛型 BaseDao超市系统里商品、类别、供应商都有 get、save、delete 这几个标准操作每个 DAO 都写一遍就是几十行重复。抽一个泛型 BaseDao用 Hibernate 的 getCurrentSession 实现通用 CRUDRepository public class BaseDaoT { Autowired private SessionFactory sessionFactory; protected Session getSession() { return sessionFactory.getCurrentSession(); } public void save(T entity) { getSession().save(entity); } public void delete(T entity) { getSession().delete(entity); } SuppressWarnings(unchecked) public T get(ClassT clazz, Integer id) { return (T) getSession().get(clazz, id); } }商品 DAO 继承 BaseDao 后只需要写商品特有的查询方法比如按价格区间查商品、按类别查商品列表。注意泛型 DAO 在 Spring 中需要保证每一个具体产品 DAO 都标注Repository否则按类型注入时会因为找不到唯一 Bean 报错。6.3 交付前的最小验证清单拿到这类 SSH 源码后不要急着去读全部业务代码先按下面的顺序把工程跑通第一Tomcat 启动无 ClassNotFoundException日志里出现 Spring 容器初始化的完成标记第二访问登录页面输入正确账号能进入主页面输入错误账号能返回登录页第三添加一个商品并指定类别数据库 t_product 表出现新记录且 category_id 正确第四手动改坏一条入库 Service 数据让第二个 DAO 操作抛异常确认库存不会减少验证事务回滚生效第五打开商品列表页页面能正常显示类别名称不出现 LazyInitializationException。这个验证清单我通常会直接写进源码包的 README 顶部每次改完代码后按顺序回归一遍比临时翻配置文件管用得多。本文还有配套的精品资源点击获取