Spring OpenSessionInView 模式详解:原理、配置、问题与最佳实践

Spring OpenSessionInView 模式详解:原理、配置、问题与最佳实践 一、OpenSessionInView 模式概述在 Spring Hibernate 集成开发中当设置了lazytrue延迟加载时如果在读取父数据后 Hibernate 自动关闭了 Session那么在需要使用子数据时系统会抛出LazyInitializationException错误。Spring 提供的OpenSessionInViewFilter和OpenSessionInViewInterceptor正是为了解决这一问题而设计的。OpenSessionInViewFilter 的核心作用保持 Session 状态直到 request 将全部页面发送到客户端从而解决延迟加载带来的问题。二、配置方式与注意事项2.1 两种配置方式Spring 提供了两种配置方式OpenSessionInViewFilter在web.xml中配置OpenSessionInViewInterceptor在 Spring 配置文件中配置两者功能相同只是配置位置不同。2.2 Web.xml 配置示例filter filter-nameopensession/filter-name filter-class org.springframework.orm.hibernate3.support.OpenSessionInViewFilter /filter-class init-param param-namesingleSession/param-name param-valuetrue/param-value /init-param init-param param-namesessionFactoryBeanName/param-name param-valuemySessionFactory/param-value /init-param /filter filter filter-namewebwork/filter-name filter-classcom.opensymphony.webwork.dispatcher.FilterDispatcher/filter-class /filter filter-mapping filter-nameopensession/filter-name url-pattern/*/url-pattern /filter-mapping filter-mapping filter-namewebwork/filter-name url-pattern/*/url-pattern /filter-mapping重要提示在web.xml中配置时OpenSessionInViewFilter必须在 Webwork 的 filter 前面否则系统会报错。2.3 singleSession 参数详解singleSession参数应该设置为true表示一个 request 只能打开一个 Session。如果设置为falseSession 可以被打开多个这时在 update、delete 操作时可能会出现打开多个 Session 的异常。性能考量当设置为true时系统的性能可能会因为用户的网络状况受到影响。因为 request 在生成页面完成后Session 才会被释放。如果用户的网络状况比较差连接池中的连接会迟迟不被回收造成内存增加系统性能受损。三、工作原理与流程3.1 基本工作原理在没有使用 Spring 提供的 Open Session In View 情况下需要在 Service或 Dao层里把 Session 关闭。所以如果lazy loading为true要在应用层内把关系集合都初始化如company.getEmployees()否则 Hibernate 会抛出Session already closed Exception。Open Session In View 提供了一种简便的方法较好地解决了 lazy loading 问题。它在 request 把 Session 绑定到当前线程期间一直保持 Hibernate Session 在 open 状态使 Session 在 request 的整个期间都可以使用。这样在 View 层里 PO 也可以 lazy loading 数据如${company.employees}。当 View 层逻辑完成后才会通过 Filter 的doFilter方法或 Interceptor 的postHandle方法自动关闭 Session。3.2 OpenSessionInViewFilter 核心方法分析查看OpenSessionInViewFilter的关键方法protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { SessionFactory sessionFactory lookupSessionFactory(); logger.debug(Opening Hibernate Session in OpenSessionInViewFilter); Session session getSession(sessionFactory); TransactionSynchronizationManager.bindResource( sessionFactory, new SessionHolder(session)); try { filterChain.doFilter(request, response); } finally { TransactionSynchronizationManager.unbindResource(sessionFactory); logger.debug(Closing Hibernate Session in OpenSessionInViewFilter); closeSession(session, sessionFactory); } } protected Session getSession(SessionFactory sessionFactory) throws DataAccessResourceFailureException { Session session SessionFactoryUtils.getSession(sessionFactory, true); session.setFlushMode(FlushMode.NEVER); return session; } protected void closeSession(Session session, SessionFactory sessionFactory) throws CleanupFailureDataAccessException { SessionFactoryUtils.closeSessionIfNecessary(session, sessionFactory); }可以看到OpenSessionInViewFilter在getSession的时候会把获取回来的 Session 的 flush mode 设为FlushMode.NEVER。然后把该 SessionFactory 绑定到TransactionSynchronizationManager使 request 的整个过程都使用同一个 Session。在请求过后再解除该 SessionFactory 的绑定最后closeSessionIfNecessary根据该 Session 是否已和 transaction 绑定来决定是否关闭 Session。3.3 FlushMode 控制机制public static void closeSessionIfNecessary(Session session, SessionFactory sessionFactory) throws CleanupFailureDataAccessException { if (session null || TransactionSynchronizationManager.hasResource(sessionFactory)) { return; } logger.debug(Closing Hibernate session); try { session.close(); } catch (JDBCException ex) { // SQLException underneath throw new CleanupFailureDataAccessException( Could not close Hibernate session, ex.getSQLException()); } catch (HibernateException ex) { throw new CleanupFailureDataAccessException( Could not close Hibernate session, ex); } }在这个过程中如果HibernateTemplate发现当前 Session 有不是 readOnly 的 transaction就会获取到FlushMode.AUTO的 Session使方法拥有写权限。关键点如果有不是 readOnly 的 transaction就可以由Flush.NEVER转为Flush.AUTO拥有 insert、update、delete 操作权限。如果没有 transaction并且没有另外人为地设置 flush mode 的话则整个doFilter的过程都是Flush.NEVER。所以受 transaction 保护的方法有写权限没受保护的则没有。四、常见问题与解决方案4.1 常见错误Write operations are not allowed in read-only mode很多人在使用 OpenSessionInView 过程中会遇到以下错误org.springframework.dao.InvalidDataAccessApiUsageException: Write operations are not allowed in read-only mode (FlushMode.NEVER) - turn your Session into FlushMode.AUTO or remove readOnly marker from transaction definition分析原因OpenSessionInViewFilter在把 Session 绑在当前线程上的时候会把 Session 的 flush mode 设为FlushMode.NEVER。因此如果某个方法没有事务或者有只读事务则不能对 Session 做 insert、update、delete 操作除非事先把 Session 的 flush mode 手动设为 AUTO。4.2 解决方案方案一配置 Spring 事务声明采用 Spring 的事务声明使方法受 transaction 控制bean idbaseTransaction classorg.springframework.transaction.interceptor.TransactionProxyFactoryBean abstracttrue property nametransactionManager reftransactionManager/ property nameproxyTargetClass valuetrue/ property nametransactionAttributes props prop keyget*PROPAGATION_REQUIRED,readOnly/prop prop keyfind*PROPAGATION_REQUIRED,readOnly/prop prop keyload*PROPAGATION_REQUIRED,readOnly/prop prop keysave*PROPAGATION_REQUIRED/prop prop keyadd*PROPAGATION_REQUIRED/prop prop keyupdate*PROPAGATION_REQUIRED/prop prop keyremove*PROPAGATION_REQUIRED/prop /props /property /bean bean iduserService parentbaseTransaction property nametarget bean classcom.phopesoft.security.service.impl.UserServiceImpl/ /property /bean对于上例以 save、add、update、remove 开头的方法拥有可写的事务。如果当前有某个方法如命名为importExcel()则因没有 transaction 而没有写权限这时若方法内有 insert、update、delete 操作的话则需要手动设置 flush mode 为Flush.AUTOsession.setFlushMode(FlushMode.AUTO); session.save(user); session.flush();方案二将 singleSession 设为 false将singleSession设为false这样只需要修改web.xml。缺点是 Hibernate Session 的实例可能会大增使用的 JDBC Connection 量也会大增如果 Connection Pool 的maxPoolSize设得太小很容易就出问题。注意singleSession默认为true若设为false则等于没有使用 OpenSessionInView。方案三在控制器中自行管理 Session 的 FlushMode麻烦的是每个有 Modify 的 Method 都要多几行代码session.setFlushMode(FlushMode.AUTO); session.update(user); session.flush();方案四扩展 OpenSessionInViewFilter继承OpenSessionInViewFilter重写protected Session getSession(SessionFactory sessionFactory)方法将 FlushMode 直接改为 AUTO。五、性能与风险考量5.1 执行流程分析尽管 Open Session In View 看起来还不错其实副作用不少。回顾上面OpenSessionInViewFilter的doFilterInternal方法代码这个方法实际上是被父类的doFilter调用的。因此我们可以大致了解 OpenSessionInViewFilter 的调用流程request请求→ open session 并开始 transaction → controller → ViewJsp→ 结束 transaction 并 close session5.2 潜在风险一切看起来很正确尤其是在本地开发测试的时候没出现问题。但试想如果流程中的某一步被阻塞的话那在这期间 connection 就一直被占用而不释放。最有可能被阻塞的就是在写 Jsp 这步页面内容大response.write 的时间长网速慢服务器与用户间传输时间久当大量这样的情况出现时就有连接池连接不足造成页面假死现象。5.3 使用建议由于OpenSessionInViewFilter把 Session 绑在当前线程上导致 Session 的生命周期比事务要长这期间所有事务性操作都在复用这同一个 Session由此产生了一些怪问题。重要结论Open Session In View 是个双刃剑放在公网上内容多流量大的网站请慎用。六、总结OpenSessionInView 模式是解决 Hibernate 延迟加载问题的有效方案但它也带来了性能风险和复杂性。在实际使用中需要权衡利弊适用场景小型应用、内部系统、开发测试环境慎用场景高并发公网应用、大流量网站最佳实践合理配置事务、监控连接池使用情况、考虑替代方案如 DTO 模式通过合理的配置和事务管理可以在享受延迟加载便利性的同时最大限度地降低 OpenSessionInView 带来的风险。七、参考资料为了帮助读者更深入地理解 OpenSessionInView 模式及其相关技术以下提供一些有价值的参考资料官方文档Spring Framework 官方文档 - Open Session In ViewRedirecting...Hibernate 官方文档 - Session 管理https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#sessionsSpring Boot 数据访问文档https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.sql.jpa-and-spring-data社区讨论与最佳实践Stack Overflow - OpenSessionInViewFilter 讨论https://stackoverflow.com/questions/1103363/why-is-hibernate-open-session-in-view-considered-a-bad-practiceSpring 社区论坛 - OSIV 性能问题Allow configurable await time in TestCallableInterceptor in test mvc [SPR-9875] · Issue #14508 · spring-projects/spring-framework · GitHubVlad Mihalcea 博客 - OSIV 反模式The Open Session In View Anti-Pattern - Vlad Mihalcea替代方案推荐DTOData Transfer Object模式Martin Fowler 的 DTO 模式介绍Data Transfer ObjectSpring 项目中使用 DTO 的最佳实践Entity To DTO Conversion for a Spring REST API | BaeldungMapStruct - DTO 映射工具MapStruct – Java bean mappings, the easy way!Fetch Join 与实体图Entity GraphJPA 2.1 实体图特性JPA Entity Graph | BaeldungHibernate Fetch Join 策略https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#fetching-strategiesSpring Data JPA 投影ProjectionsSpring Data JPA 投影文档Spring Data JPA :: Spring Data JPA接口投影与类投影示例Spring Data JPA Projections | Baeldung性能优化相关连接池配置与监控HikariCP 配置指南GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last. · GitHubDruid 连接池监控GitHub - alibaba/druid: 阿里云计算平台DataWorks(https://help.aliyun.com/document_detail/137663.html) 团队出品为监控而生的数据库连接池 · GitHubSpring Boot Actuator 监控端点https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html这些参考资料涵盖了从官方文档到社区最佳实践以及替代方案的详细实现可以帮助读者在实际项目中做出更明智的技术决策。