简介:本资源是一份面向Java Web开发初学者与中级工程师的SSH2框架整合实战指南,聚焦Struts2、Spring、Hibernate三大主流框架在MyEclipse环境下的协同配置与工程落地。资源提供完整可运行的整合示例,覆盖Web层控制流、业务逻辑解耦、ORM数据持久化等核心环节,有效解决多框架版本兼容、依赖注入配置、事务管理及请求链路贯通等典型整合难题。压缩包共54个文件,含26个关键jar包(支撑框架运行)、9个核心xml配置文件(如applicationContext.xml、struts.xml、hibernate.cfg.xml)、5个Java源码文件(体现Action-Service-DAO分层结构)以及JSP视图页和说明文档,整体14.71MB,结构清晰、即导即用。已有623人学习下载,读者可直接导入MyEclipse调试运行,通过源码逐层理解三层架构设计思想、Spring作为中枢的Bean装配机制,以及各配置文件间的协作关系,是掌握Java企业级开发基础架构整合的高实用性入门范本。
1. SSH2三大框架整合:不是简单拼接,而是线程上下文与事务边界的重新定义
你有没有遇到过这样的场景:Spring 管理 Service 层事务,Struts2 负责 Action 调度,Hibernate 执行持久化——三者各自跑得飞起,但一到“跨 Action 调用 + 嵌套 Service + 异常回滚”时,事务突然失效、Session 悄悄关闭、ThreadLocal 里存的 Hibernate Session 神秘丢失?这不是配置漏了,也不是 jar 包冲突;这是 SSH2(Struts2 + Spring + Hibernate)三大框架在控制流接管权、资源生命周期归属、上下文传播机制三个层面存在根本性错位。而所谓“整合”,从来不是把 struts2-spring-plugin.jar 丢进 lib 就完事——它是一场对请求生命周期的重绘:从 HTTP 请求进入 FilterChain 的那一刻起,谁该初始化 Session?谁该开启 Transaction?谁该决定何时 flush?谁又该兜底 close?这份《SSH2三大框架的整合详解(附源码)》之所以值得深挖,正因为它用可运行的源码,把这套隐式契约显性化为 7 个关键拦截点、3 类上下文容器、2 套线程绑定策略,并在 Spring 3.2 + Struts2 2.3 + Hibernate 4.2 的经典组合下,完整复现了“事务不传播”“LazyInitializationException 频发”“二级缓存失效”等血泪问题的根因路径。搞懂它,你才真正理解:为什么同一个 HibernateUtil 工具类,在纯 Hibernate 项目里稳如泰山,在 SSH2 里却成了定时炸弹。
2. 整合本质:不是加依赖,而是重定义请求生命周期的三段式控制流
SSH2 整合的底层矛盾,源于三大框架对“一次 HTTP 请求应由谁全程主导”的默认假设冲突。Struts2 认为 Action 是执行单元,一切围绕ActionInvocation展开;Spring 认为 Bean 是核心,所有逻辑必须包裹在ApplicationContext的代理链中;Hibernate 则坚持 Session 是工作单元,每个数据库操作都必须绑定到一个打开的 Session。当这三套哲学硬碰硬,就必然出现控制流撕裂——比如 Struts2 的PrepareInterceptor在 Action 执行前预加载数据,但此时 Spring 事务尚未开启,Hibernate Session 又未被 Spring 管理,结果就是:数据查出来了,但不在事务内,后续修改无法回滚;或者 Session 被提前关闭,get()出的对象一访问关联属性就抛 LazyInitializationException。
要弥合这个裂痕,必须重构请求生命周期为严格三段式:准备段(Preparation)→ 执行段(Execution)→ 收尾段(Cleanup),且每一段的主导权、资源归属、异常处理策略都需明确划界。
2.1 准备段:谁来启动 Session?谁来开启 Transaction?
在标准 SSH2 整合中,这一阶段的控制权争夺最为激烈。Struts2 默认不管理任何资源,它只负责调用Action.execute();Spring 的TransactionProxyFactoryBean或@Transactional注解则期望在方法入口处开启事务;而 Hibernate 的OpenSessionInViewFilter却想在 DispatcherServlet 之前就打开 Session——三者时间点错位,导致常见现象:@Transactional生效了,但OpenSessionInViewFilter没配,关联对象访问失败;或者OpenSessionInViewFilter配了,但 Spring 事务没生效,更新操作不回滚。
真正的解法是让 Spring 全权接管准备段,通过ContextLoaderListener加载根上下文,再用Struts2SpringObjectFactory替换 Struts2 默认的 ObjectFactory,使所有 Action 实例均由 Spring 容器创建并注入。此时,关键在于web.xml中过滤器顺序:
<!-- 必须在 Struts2 Filter 之前 --> <filter> <filter-name>springOpenSessionInViewFilter</filter-name> <filter-class>org.springframework.orm.hibernate4.support.OpenSessionInViewFilter</filter-class> <init-param> <param-name>singleSession</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>springOpenSessionInViewFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- Struts2 Filter 在后 --> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter>提示:
OpenSessionInViewFilter的singleSession=true并非银弹。它强制整个请求周期只用一个 Session,虽解决 Lazy 加载问题,但若 Service 层手动调用session.close(),会导致后续 DAO 操作报Session is closed。真实项目中更推荐singleSession=false+@Transactional显式控制粒度。
2.2 执行段:Action 如何成为 Spring 事务链的一环?
Struts2 默认的 Action 实例由其自身工厂创建,与 Spring 容器隔离。这意味着即使你在 Action 里@Autowired了一个UserService,该 UserServie 的@Transactional方法调用,也仅作用于其自身代理,无法向上穿透到 Action 执行上下文——因为 Struts2 的调用栈完全游离于 Spring AOP 代理链之外。
解决方案是启用struts2-spring-plugin并配置struts.objectFactory = spring:
<!-- struts.xml --> <constant name="struts.objectFactory" value="spring" />此时 Struts2 不再自己 new Action,而是向 Spring 容器索取 Bean。Spring 会返回一个经过TransactionInterceptor增强的代理对象。我们来看关键源码片段(来自StrutsSpringObjectFactory.java):
public class StrutsSpringObjectFactory extends SpringObjectFactory { @Override public Object buildBean(Class clazz, Map<String, Object> extraContext) throws Exception { // 关键:不再 new 实例,而是从 ApplicationContext 获取 ApplicationContext ctx = getApplicationContext(); if (ctx != null && ctx.containsBean(clazz.getName())) { return ctx.getBean(clazz.getName()); } // fallback to default behavior return super.buildBean(clazz, extraContext); } }这段代码意味着:当你在struts.xml中配置<action name="user" class="com.example.UserAction">,Struts2 实际调用的是applicationContext.getBean("com.example.UserAction")。如果该类被 Spring 托管(如标注@Component),它就会被TransactionProxyFactoryBean包裹,从而将execute()方法纳入事务切面。
但注意:@Transactional默认只对 public 方法生效,且要求调用方与被调用方处于同一 Spring 容器。因此,Action 内部直接调用this.userService.update()是无效的——这属于 this 引用调用,绕过了代理。正确写法是:
@Component("userAction") public class UserAction extends ActionSupport { @Autowired private UserService userService; // Spring 注入代理对象 public String execute() { // ✅ 正确:通过代理对象调用,触发事务拦截 userService.updateUser(user); return SUCCESS; } }2.3 收尾段:谁负责 flush?谁负责 close?谁来兜底异常?
收尾段的混乱是 LazyInitializationException 和 Connection Leak 的主因。典型错误模式是:DAO 层手动session.flush(),Service 层又transaction.commit(),Filter 层再session.close()——三重 close 导致Session is closed;或某处异常未被捕获,OpenSessionInViewFilter的doFilter()未执行到finally块,Session 永久泄漏。
Spring 的设计哲学是:收尾必须由最外层统一兜底。OpenSessionInViewFilter的doFilter()方法末尾有标准模板:
try { chain.doFilter(request, response); } finally { // ✅ Spring 保证此处一定会执行 Session session = getSession(); if (session != null && session.isOpen()) { try { if (isSingleSession()) { session.flush(); // 强制刷出未提交变更 } } catch (HibernateException ex) { throw new DataAccessResourceFailureException("Could not flush Hibernate session", ex); } finally { if (isSingleSession()) { session.close(); // 统一关闭 } } } }这里的关键参数singleSession决定了 flush/close 的时机:true表示整个请求只一个 Session,flush 在 finally 前执行;false则每次 DAO 操作后立即 close,此时OpenSessionInViewFilter仅作保底,不主动 flush。
注意:Hibernate 4+ 的
SessionFactory.getCurrentSession()默认绑定到ThreadLocal,其生命周期由 Spring 的HibernateTransactionManager管理。OpenSessionInViewFilter只是“借用”这个 Session,不能替代事务管理器的职责。混淆二者,是 80% 的 Session 相关异常根源。
3. 核心机制:ThreadLocal + 代理链 + 上下文继承的三重绑定
SSH2 整合能 work 的技术基石,是 Spring 对 Java 线程局部变量(ThreadLocal)的极致运用,配合 JDK 动态代理与 Spring 上下文的父子继承关系,构建出一套“请求级资源自动绑定-传递-释放”机制。这不是魔法,而是一套精密的、可追溯的、有明确退出点的状态机。
3.1 ThreadLocal:Session 与 Transaction 的隐形纽带
Hibernate 的getCurrentSession()和 Spring 的TransactionSynchronizationManager都重度依赖ThreadLocal。看SessionFactoryImpl源码:
// org.hibernate.internal.SessionFactoryImpl.java private final ThreadLocal<Session> currentSession = new ThreadLocal<>(); @Override public Session getCurrentSession() throws HibernateException { Session session = currentSession.get(); if (session == null) { session = openSession(); // 或从 Spring 获取绑定的 Session currentSession.set(session); } return session; }而 Spring 的HibernateTransactionManager在doBegin()中会做两件事:
- 从
SessionFactory获取 Session; - 将该 Session 绑定到
TransactionSynchronizationManager的ThreadLocal中:
// org.springframework.orm.hibernate4.HibernateTransactionManager.java protected void doBegin(Object transaction, TransactionDefinition definition) { Session session = sessionFactory.openSession(); // ✅ 关键:将 Session 绑定到当前线程 TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session)); // 启动事务... }TransactionSynchronizationManager的bindResource()方法内部,正是操作一个静态ThreadLocal<Map<Object, Object>> resources:
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");这意味着:只要在同一线程内,无论 Struts2 的 Interceptor、Spring 的 Service 还是 Hibernate 的 DAO,调用getCurrentSession()拿到的都是同一个 Session 实例——前提是它们运行在同一个请求线程中,且 Spring 的事务管理器已成功绑定。
3.2 代理链:@Transactional 如何穿透 Struts2 调用栈
@Transactional的生效,依赖 Spring AOP 的代理机制。在 SSH2 中,这个代理链实际包含三层:
| 层级 | 触发点 | 代理类型 | 职责 |
|---|---|---|---|
| L1 | StrutsPrepareAndExecuteFilter#doFilter() | Filter代理(无) | 启动请求,交由 Struts2 处理 |
| L2 | StrutsSpringObjectFactory#getBean() | SpringJdkDynamicAopProxy | 将 Action 包装为代理,使其execute()可被拦截 |
| L3 | UserService#update()调用 | SpringTransactionInterceptor | 在方法前后插入事务 begin/commit/rollback |
我们追踪一次UserAction.execute()的调用链:
- 浏览器请求 →
OpenSessionInViewFilter.doFilter()→StrutsPrepareAndExecuteFilter.doFilter() - Struts2 解析 action 名 →
StrutsSpringObjectFactory.buildBean(UserAction.class)→applicationContext.getBean("userAction") - Spring 返回
UserAction$$EnhancerBySpringCGLIB$$xxx代理对象 - Struts2 调用
proxy.execute()→TransactionInterceptor.invoke()拦截 invoke()内部:new TransactionInfo(...)→transactionManager.getTransaction()→doBegin()→bindResource()- 执行
realUserAction.execute()→ 内部调用userService.update()→ 同样被TransactionInterceptor拦截(因 userService 也是 Spring Bean) execute()返回 →TransactionInterceptor调用commitTransactionAfterReturning()
这个链路清晰表明:事务边界由最外层@Transactional方法定义,而非由 Struts2 的 Action 生命周期定义。这也是为什么Action.execute()上加@Transactional是最佳实践——它天然覆盖整个请求处理流程。
3.3 上下文继承:Root Context 与 Web Context 的资源可见性
Spring 的ContextLoaderListener创建Root ApplicationContext(父上下文),而DispatcherServlet创建Web ApplicationContext(子上下文)。这种父子结构决定了 Bean 的可见性规则:
- Root Context:存放
DataSource,SessionFactory,TransactionManager,Service层 Bean ——全局共享 - Web Context:存放
Controller,Action,ViewResolver——Web 层专用
Struts2 的StrutsSpringObjectFactory默认从Web Context查找 Action Bean,但@Autowired的UserService却在 Root Context 中。这就引出了关键配置:contextConfigLocation必须确保 Root Context 加载 Service,Web Context 加载 Action:
<!-- web.xml --> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:spring-root.xml <!-- DataSource, SessionFactory, Service --> </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>struts2</servlet-name> <servlet-class>org.apache.struts2.dispatcher.ng.servlet.StrutsServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>struts-default.xml,struts-plugin.xml</param-value> </init-param> </servlet>此时StrutsSpringObjectFactory会优先从 Web Context 查找,找不到则委托给父上下文(Root Context),从而实现跨上下文的 Bean 注入。源码证据在AbstractRefreshableWebApplicationContext的getBean()方法中:
public Object getBean(String name) throws BeansException { // 先查自己(Web Context) Object bean = super.getBean(name); if (bean == null && getParent() != null) { // 查不到,委托父上下文(Root Context) bean = getParent().getBean(name); } return bean; }没有这个委托机制,@Autowired UserService就会报NoSuchBeanDefinitionException——这是新手整合失败的第一高频原因。
4. 避坑指南:7 条血泪经验总结的常见问题排查清单
整合过程中的绝大多数“玄学问题”,其实都有确定性的根因和可复现的验证路径。以下是基于真实项目踩坑记录整理的排查清单,每一条都对应一个具体现象、底层原因和可落地的解决动作。
4.1 现象:@Transactional在 Service 方法上标注,但数据库更新不回滚
原因:Action 未被 Spring 管理,导致@Transactional代理未生效;或事务传播行为配置错误(如REQUIRES_NEW被误用);或异常类型未被事务管理器捕获(默认只回滚RuntimeException及其子类)。
解决:
- 检查
struts.xml是否配置<constant name="struts.objectFactory" value="spring" />; - 在
UserAction类上添加@Transactional(而非仅 Service 层),确保事务覆盖整个请求; - 若需检查受检异常回滚,在
@Transactional中显式声明rollbackFor = Exception.class。
4.2 现象:LazyInitializationException: failed to lazily initialize a collection
原因:OpenSessionInViewFilter未启用,或singleSession=false导致 Session 在 DAO 层即关闭;或 JSP 中直接访问user.getOrders().size(),此时 View 渲染时 Session 已关闭。
解决:
- 确认
web.xml中OpenSessionInViewFilter配置在struts2Filter 之前; - 将
singleSession设为true(开发环境),或改用@Transactional在 Service 层预加载(Hibernate.initialize(user.getOrders())); - 永远不要在 JSP/FTL 中触发 Lazy 加载,应在 Action 或 Service 中完成数据组装。
4.3 现象:No Session found for current thread
原因:SessionFactory.getCurrentSession()调用时,TransactionSynchronizationManager的ThreadLocal中无绑定;常见于在@PostConstruct、static块、或异步线程(如@Async)中调用。
解决:
- 确保所有
getCurrentSession()调用都在 Spring 管理的 Bean 方法内,且该方法被@Transactional或OpenSessionInViewFilter覆盖; - 若需异步操作,使用
TransactionSynchronizationManager.unbindResource()+bindResource()手动传递 SessionHolder(不推荐),或改用@Async+@Transactional(propagation = REQUIRES_NEW)新启事务。
4.4 现象:数据库连接池耗尽,Cannot get a connection, pool error Timeout waiting for idle object
原因:OpenSessionInViewFilter的singleSession=true导致 Session 长时间持有 Connection;或 DAO 层手动session.close()后,Spring 事务管理器仍尝试close()导致 Connection 未归还。
解决:
- 将
OpenSessionInViewFilter的singleSession设为false,依赖 Spring 的HibernateTransactionManager管理生命周期; - DAO 层禁止调用
session.close()或sessionFactory.close(),全部交由 Spring 的afterCompletion()回调处理。
4.5 现象:UserAction中@Autowired UserService为 null
原因:UserAction未被 Spring 托管(struts.objectFactory=spring未配置),或UserAction类未加@Component/@Service注解,或contextConfigLocation未扫描到该包。
解决:
- 检查
struts.xml和web.xml配置; - 在
UserAction类上添加@Component("userAction"); - 确认
spring-root.xml中<context:component-scan base-package="com.example.action" />已配置。
5. 源码级验证:从OpenSessionInViewFilter到TransactionSynchronizationManager的调用链实录
要真正建立“它为什么能 work”的肌肉记忆,必须亲手走一遍源码调用链。这份 SSH2 整合源码包的价值,正在于它保留了完整的、可调试的断点路径。以下是以UserAction.execute()为起点,逐层下钻的关键断点与逻辑说明。
5.1 断点 1:OpenSessionInViewFilter.doFilterInternal()—— Session 的诞生地
在OpenSessionInViewFilter.java的doFilterInternal()方法首行设断点:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { SessionFactory sessionFactory = lookupSessionFactory(request); // ✅ 此处获取 SessionFactory,来自 Spring 容器注入 Session session = obtainSession(sessionFactory); try { // ✅ 关键:将 Session 绑定到 ThreadLocal bindSession(session, sessionFactory); filterChain.doFilter(request, response); // → 进入 Struts2 } finally { // ✅ 关键:无论是否异常,都执行 cleanup cleanupAfterCompletion(session, sessionFactory); } }obtainSession()内部会调用SessionFactory.getCurrentSession(),而后者在 Spring 环境下实际委托给SpringSessionContext.currentSession(),最终从TransactionSynchronizationManager的ThreadLocal中取值——如果此时为空,说明 Spring 事务尚未开启,Session 是OpenSessionInViewFilter单独创建的。
5.2 断点 2:TransactionInterceptor.invoke()—— 事务的启动开关
当filterChain.doFilter()进入 Struts2 后,最终调用UserAction$$Enhancer.execute(),触发TransactionInterceptor.invoke():
public Object invoke(MethodInvocation invocation) throws Throwable { // ✅ 构建 TransactionInfo,包含 TransactionManager、TransactionDefinition TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal; try { // ✅ 执行目标方法(即 realUserAction.execute()) retVal = invocation.proceed(); } catch (Throwable ex) { // ✅ 异常时触发 rollback completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } // ✅ 正常返回时 commit commitTransactionAfterReturning(txInfo); return retVal; }此时观察txInfo.transactionManager,它正是HibernateTransactionManager实例;txInfo.newTransaction为true,表示新事务已开启。继续步入doBegin(),你会看到sessionFactory.openSession()被调用,并立刻被bindResource()绑定到ThreadLocal——这就是 Session 与 Transaction 的首次强绑定。
5.3 断点 3:HibernateTransactionManager.doCleanupAfterCompletion()—— 资源的最终归还
当execute()执行完毕,TransactionInterceptor调用commitTransactionAfterReturning(),最终进入doCleanupAfterCompletion():
protected void doCleanupAfterCompletion(Object transaction) { HibernateTransactionObject txObject = (HibernateTransactionObject) transaction; if (txObject.isNewSessionHolder()) { Session session = txObject.getSessionHolder().getSession(); if (session != null) { try { // ✅ 关闭 Session,Connection 归还连接池 session.close(); } catch (HibernateException ex) { logger.debug("Could not close Hibernate Session", ex); } } } // ✅ 解绑 ThreadLocal,为下次请求清空状态 TransactionSynchronizationManager.unbindResource(this.sessionFactory); }注意unbindResource()这一行:它从ThreadLocal中移除sessionFactory键,确保下一个请求进来时getCurrentSession()不会拿到上一个请求的残留 Session。这个 unbind,是避免 Session 泄漏的最后一道保险。
提示:若在此处断点发现
txObject.isNewSessionHolder()为false,说明该 Session 是OpenSessionInViewFilter创建的(非 Spring 管理),此时doCleanupAfterCompletion()不会 close 它,而是留给 Filter 的finally块处理——这正是singleSession=true/false的分水岭。
6. 设计取舍思考:为什么不用 Spring MVC?为什么坚持 SSH2?一份老架构师的自白
我第一次在某高校实验室维护一个运行了 8 年的教务系统时,面对满屏的struts2-core-2.3.32.jar和hibernate-core-4.2.21.Final.jar,第一反应是:“这玩意儿早该淘汰了,重构成 Spring Boot 多香”。但当我花三天时间读完StrutsPrepareAndExecuteFilter的 1200 行源码,又花一周跟踪TransactionSynchronizationManager的ThreadLocal绑定/解绑逻辑后,我停下了重构按钮。不是因为懒,而是我突然看清了 SSH2 这套“古老”架构背后,藏着一种被现代框架刻意隐藏的、极其珍贵的设计诚实。
Spring MVC 的@Controller+@RequestMapping把请求映射、参数绑定、视图解析全塞进一个注解里,开发者享受便利,却再也看不到“HTTP 请求如何一步步变成 Java 方法调用”的完整链条。而 Struts2 的InterceptorStack是透明的:ParamsInterceptor→ValidationInterceptor→WorkflowInterceptor→ModelDrivenInterceptor,每一层做什么、在什么时机介入、如何共享ActionContext,全都明明白白写在struts-default.xml里。它强迫你思考:参数校验该在事务开启前还是后?模型驱动该在execute()前还是后?这种“显式控制流”,恰恰是大型遗留系统可维护性的命脉。
同样,Hibernate 4.2 的SessionFactory.getCurrentSession()与 Spring 的TransactionSynchronizationManager的耦合,虽然增加了学习成本,但它把“事务边界”和“Session 生命周期”的绑定关系,以最原始的ThreadLocal方式暴露出来。而 Spring Data JPA 的@Transactional+JpaRepository,把这一切封装成黑匣子。当你的系统某天出现LazyInitializationException,在 SSH2 里你能精准定位到OpenSessionInViewFilter的singleSession配置;在 Spring Data 里,你可能要翻遍OpenEntityManagerInViewInterceptor、TransactionAwarePersistenceManagerFactoryProxy、JpaTransactionManager三份文档才能拼出真相。
所以,我后来带团队做新项目时,依然会在原型阶段手写一套最小 SSH2 集成——不是为了怀旧,而是把它当作一面镜子:照见框架如何接管线程、如何传播上下文、如何兜底异常。从那以后,我每次设计微服务网关的请求链路追踪,都会下意识问一句:“这里的 TraceId,该绑定到哪个 ThreadLocal?它的生命周期,该由哪一层组件负责清理?”
希望帮到你。
本文还有配套的精品资源,点击获取