2026/10/19 9:33:03

SSH2整合本质:线程上下文与事务边界的重定义

SSH2整合本质:线程上下文与事务边界的重定义 简介本资源是一份面向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 包冲突这是 SSH2Struts2 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-namespringOpenSessionInViewFilter/filter-name filter-classorg.springframework.orm.hibernate4.support.OpenSessionInViewFilter/filter-class init-param param-namesingleSession/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-namespringOpenSessionInViewFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- Struts2 Filter 在后 -- filter filter-namestruts2/filter-name filter-classorg.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter/filter-class /filter提示OpenSessionInViewFilter的singleSessiontrue并非银弹。它强制整个请求周期只用一个 Session虽解决 Lazy 加载问题但若 Service 层手动调用session.close()会导致后续 DAO 操作报Session is closed。真实项目中更推荐singleSessionfalseTransactional显式控制粒度。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 namestruts.objectFactory valuespring /此时 Struts2 不再自己 new Action而是向 Spring 容器索取 Bean。Spring 会返回一个经过TransactionInterceptor增强的代理对象。我们来看关键源码片段来自StrutsSpringObjectFactory.javapublic class StrutsSpringObjectFactory extends SpringObjectFactory { Override public Object buildBean(Class clazz, MapString, 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 nameuser classcom.example.UserActionStruts2 实际调用的是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表示整个请求只一个 Sessionflush 在 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 ThreadLocalSession 与 Transaction 的隐形纽带Hibernate 的getCurrentSession()和 Spring 的TransactionSynchronizationManager都重度依赖ThreadLocal。看SessionFactoryImpl源码// org.hibernate.internal.SessionFactoryImpl.java private final ThreadLocalSession 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()方法内部正是操作一个静态ThreadLocalMapObject, Object resourcesprivate static final ThreadLocalMapObject, Object resources new NamedThreadLocal(Transactional resources);这意味着只要在同一线程内无论 Struts2 的 Interceptor、Spring 的 Service 还是 Hibernate 的 DAO调用getCurrentSession()拿到的都是同一个 Session 实例——前提是它们运行在同一个请求线程中且 Spring 的事务管理器已成功绑定。3.2 代理链Transactional 如何穿透 Struts2 调用栈Transactional的生效依赖 Spring AOP 的代理机制。在 SSH2 中这个代理链实际包含三层层级触发点代理类型职责L1StrutsPrepareAndExecuteFilter#doFilter()Filter代理无启动请求交由 Struts2 处理L2StrutsSpringObjectFactory#getBean()SpringJdkDynamicAopProxy将 Action 包装为代理使其execute()可被拦截L3UserService#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 Beanexecute()返回 →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 加载 ServiceWeb Context 加载 Action!-- web.xml -- context-param param-namecontextConfigLocation/param-name param-value classpath:spring-root.xml !-- DataSource, SessionFactory, Service -- /param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namestruts2/servlet-name servlet-classorg.apache.struts2.dispatcher.ng.servlet.StrutsServlet/servlet-class init-param param-nameconfig/param-name param-valuestruts-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 namestruts.objectFactory valuespring /在UserAction类上添加Transactional而非仅 Service 层确保事务覆盖整个请求若需检查受检异常回滚在Transactional中显式声明rollbackFor Exception.class。4.2 现象LazyInitializationException: failed to lazily initialize a collection原因OpenSessionInViewFilter未启用或singleSessionfalse导致 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不推荐或改用AsyncTransactional(propagation REQUIRES_NEW)新启事务。4.4 现象数据库连接池耗尽Cannot get a connection, pool error Timeout waiting for idle object原因OpenSessionInViewFilter的singleSessiontrue导致 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.objectFactoryspring未配置或UserAction类未加Component/Service注解或contextConfigLocation未扫描到该包。解决检查struts.xml和web.xml配置在UserAction类上添加Component(userAction)确认spring-root.xml中context:component-scan base-packagecom.example.action /已配置。5. 源码级验证从OpenSessionInViewFilter到TransactionSynchronizationManager的调用链实录要真正建立“它为什么能 work”的肌肉记忆必须亲手走一遍源码调用链。这份 SSH2 整合源码包的价值正在于它保留了完整的、可调试的断点路径。以下是以UserAction.execute()为起点逐层下钻的关键断点与逻辑说明。5.1 断点 1OpenSessionInViewFilter.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 断点 2TransactionInterceptor.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 断点 3HibernateTransactionManager.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 { // ✅ 关闭 SessionConnection 归还连接池 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块处理——这正是singleSessiontrue/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 的ControllerRequestMapping把请求映射、参数绑定、视图解析全塞进一个注解里开发者享受便利却再也看不到“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 的TransactionalJpaRepository把这一切封装成黑匣子。当你的系统某天出现LazyInitializationException在 SSH2 里你能精准定位到OpenSessionInViewFilter的singleSession配置在 Spring Data 里你可能要翻遍OpenEntityManagerInViewInterceptor、TransactionAwarePersistenceManagerFactoryProxy、JpaTransactionManager三份文档才能拼出真相。所以我后来带团队做新项目时依然会在原型阶段手写一套最小 SSH2 集成——不是为了怀旧而是把它当作一面镜子照见框架如何接管线程、如何传播上下文、如何兜底异常。从那以后我每次设计微服务网关的请求链路追踪都会下意识问一句“这里的 TraceId该绑定到哪个 ThreadLocal它的生命周期该由哪一层组件负责清理”希望帮到你。本文还有配套的精品资源点击获取