From 5cbb1fc498be36bd804edd88af2f8d381d4d9222 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 9 Aug 2014 18:28:30 +0200 Subject: [PATCH] SpringSessionContext for Hibernate 4 supports lazily bound Session for propagation SUPPORTS Issue: SPR-9020 --- .../orm/hibernate4/SessionFactoryUtils.java | 6 ++- .../orm/hibernate4/SpringSessionContext.java | 48 ++++++++++++++---- .../SpringSessionSynchronization.java | 17 ++++++- .../HibernateTransactionManagerTests.java | 49 +++++++++++++++++-- 4 files changed, 102 insertions(+), 18 deletions(-) diff --git a/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SessionFactoryUtils.java b/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SessionFactoryUtils.java index de372bcf606..571e900023b 100644 --- a/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SessionFactoryUtils.java +++ b/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SessionFactoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -102,7 +102,9 @@ public abstract class SessionFactoryUtils { public static DataSource getDataSource(SessionFactory sessionFactory) { if (getConnectionProviderMethod != null && sessionFactory instanceof SessionFactoryImplementor) { Wrapped cp = (Wrapped) ReflectionUtils.invokeMethod(getConnectionProviderMethod, sessionFactory); - return cp.unwrap(DataSource.class); + if (cp != null) { + return cp.unwrap(DataSource.class); + } } return null; } diff --git a/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SpringSessionContext.java b/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SpringSessionContext.java index 35180d2e3c2..d70e2897f92 100644 --- a/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SpringSessionContext.java +++ b/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SpringSessionContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,11 @@ package org.springframework.orm.hibernate4; import java.lang.reflect.Method; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; +import org.apache.commons.logging.LogFactory; import org.hibernate.FlushMode; import org.hibernate.HibernateException; import org.hibernate.Session; @@ -44,6 +48,8 @@ public class SpringSessionContext implements CurrentSessionContext { private final SessionFactoryImplementor sessionFactory; + private TransactionManager transactionManager; + private CurrentSessionContext jtaSessionContext; @@ -56,13 +62,14 @@ public class SpringSessionContext implements CurrentSessionContext { try { Object jtaPlatform = sessionFactory.getServiceRegistry().getService(ConfigurableJtaPlatform.jtaPlatformClass); Method rtmMethod = ConfigurableJtaPlatform.jtaPlatformClass.getMethod("retrieveTransactionManager"); - Object transactionManager = ReflectionUtils.invokeMethod(rtmMethod, jtaPlatform); - if (transactionManager != null) { + this.transactionManager = (TransactionManager) ReflectionUtils.invokeMethod(rtmMethod, jtaPlatform); + if (this.transactionManager != null) { this.jtaSessionContext = new SpringJtaSessionContext(sessionFactory); } } catch (Exception ex) { - throw new IllegalStateException("Could not introspect Hibernate JtaPlatform for SpringJtaSessionContext", ex); + LogFactory.getLog(SpringSessionContext.class).warn( + "Could not introspect Hibernate JtaPlatform for SpringJtaSessionContext", ex); } } @@ -82,7 +89,7 @@ public class SpringSessionContext implements CurrentSessionContext { if (TransactionSynchronizationManager.isSynchronizationActive() && !sessionHolder.isSynchronizedWithTransaction()) { TransactionSynchronizationManager.registerSynchronization( - new SpringSessionSynchronization(sessionHolder, this.sessionFactory)); + new SpringSessionSynchronization(sessionHolder, this.sessionFactory, false)); sessionHolder.setSynchronizedWithTransaction(true); // Switch to FlushMode.AUTO, as we have to assume a thread-bound Session // with FlushMode.MANUAL, which needs to allow flushing within the transaction. @@ -95,15 +102,36 @@ public class SpringSessionContext implements CurrentSessionContext { } return session; } - else if (this.jtaSessionContext != null) { - Session session = this.jtaSessionContext.currentSession(); - if (TransactionSynchronizationManager.isSynchronizationActive()) { - TransactionSynchronizationManager.registerSynchronization(new SpringFlushSynchronization(session)); + + if (this.transactionManager != null) { + try { + if (this.transactionManager.getStatus() == Status.STATUS_ACTIVE) { + Session session = this.jtaSessionContext.currentSession(); + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new SpringFlushSynchronization(session)); + } + return session; + } } + catch (SystemException ex) { + throw new HibernateException("JTA TransactionManager found but status check failed", ex); + } + } + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + Session session = this.sessionFactory.openSession(); + if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + session.setFlushMode(FlushMode.MANUAL); + } + SessionHolder sessionHolder = new SessionHolder(session); + TransactionSynchronizationManager.registerSynchronization( + new SpringSessionSynchronization(sessionHolder, this.sessionFactory, true)); + TransactionSynchronizationManager.bindResource(this.sessionFactory, sessionHolder); + sessionHolder.setSynchronizedWithTransaction(true); return session; } else { - throw new HibernateException("No Session found for current thread"); + throw new HibernateException("Could not obtain transaction-synchronized Session for current thread"); } } diff --git a/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SpringSessionSynchronization.java b/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SpringSessionSynchronization.java index e738e2762ae..91807fde918 100644 --- a/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SpringSessionSynchronization.java +++ b/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/SpringSessionSynchronization.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,12 +39,15 @@ class SpringSessionSynchronization implements TransactionSynchronization, Ordere private final SessionFactory sessionFactory; + private final boolean newSession; + private boolean holderActive = true; - public SpringSessionSynchronization(SessionHolder sessionHolder, SessionFactory sessionFactory) { + public SpringSessionSynchronization(SessionHolder sessionHolder, SessionFactory sessionFactory, boolean newSession) { this.sessionHolder = sessionHolder; this.sessionFactory = sessionFactory; + this.newSession = newSession; } private Session getCurrentSession() { @@ -111,6 +114,12 @@ class SpringSessionSynchronization implements TransactionSynchronization, Ordere } // Eagerly disconnect the Session here, to make release mode "on_close" work nicely. session.disconnect(); + + // Unbind at this point if it's a new Session... + if (this.newSession) { + TransactionSynchronizationManager.unbindResource(this.sessionFactory); + this.holderActive = false; + } } @Override @@ -128,6 +137,10 @@ class SpringSessionSynchronization implements TransactionSynchronization, Ordere } finally { this.sessionHolder.setSynchronizedWithTransaction(false); + // Call close() at this point if it's a new Session... + if (this.newSession) { + SessionFactoryUtils.closeSession(this.sessionHolder.getSession()); + } } } diff --git a/spring-orm-hibernate4/src/test/java/org/springframework/orm/hibernate4/HibernateTransactionManagerTests.java b/spring-orm-hibernate4/src/test/java/org/springframework/orm/hibernate4/HibernateTransactionManagerTests.java index 1a85dc8f51a..aaca8e9fee4 100644 --- a/spring-orm-hibernate4/src/test/java/org/springframework/orm/hibernate4/HibernateTransactionManagerTests.java +++ b/spring-orm-hibernate4/src/test/java/org/springframework/orm/hibernate4/HibernateTransactionManagerTests.java @@ -23,7 +23,6 @@ import java.sql.Savepoint; import java.util.ArrayList; import java.util.List; import java.util.Properties; - import javax.sql.DataSource; import javax.transaction.TransactionManager; import javax.transaction.TransactionSynchronizationRegistry; @@ -38,13 +37,12 @@ import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.HSQLDialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.transaction.internal.jta.CMTTransactionFactory; import org.hibernate.exception.ConstraintViolationException; - import org.junit.After; import org.junit.Test; - import org.mockito.InOrder; import org.springframework.beans.factory.BeanFactory; @@ -495,6 +493,49 @@ public class HibernateTransactionManagerTests { ordered.verify(session).close(); } + @Test + public void testTransactionWithPropagationSupportsAndCurrentSession() throws Exception { + final SessionFactoryImplementor sf = mock(SessionFactoryImplementor.class); + final Session session = mock(Session.class); + + given(sf.openSession()).willReturn(session); + given(session.getSessionFactory()).willReturn(sf); + given(session.getFlushMode()).willReturn(FlushMode.MANUAL); + + LocalSessionFactoryBean lsfb = new LocalSessionFactoryBean() { + @Override + protected SessionFactory buildSessionFactory(LocalSessionFactoryBuilder sfb) { + return sf; + } + }; + lsfb.afterPropertiesSet(); + final SessionFactory sfProxy = lsfb.getObject(); + + PlatformTransactionManager tm = new HibernateTransactionManager(sfProxy); + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + assertTrue("Hasn't thread session", !TransactionSynchronizationManager.hasResource(sfProxy)); + + tt.execute(new TransactionCallback() { + @Override + public Object doInTransaction(TransactionStatus status) { + assertTrue("Hasn't thread session", !TransactionSynchronizationManager.hasResource(sfProxy)); + assertTrue("Is not new transaction", !status.isNewTransaction()); + assertFalse(TransactionSynchronizationManager.isCurrentTransactionReadOnly()); + assertFalse(TransactionSynchronizationManager.isActualTransactionActive()); + Session session = new SpringSessionContext(sf).currentSession(); + assertTrue("Has thread session", TransactionSynchronizationManager.hasResource(sfProxy)); + session.flush(); + return null; + } + }); + + assertTrue("Hasn't thread session", !TransactionSynchronizationManager.hasResource(sfProxy)); + InOrder ordered = inOrder(session); + ordered.verify(session).flush(); + ordered.verify(session).close(); + } + @Test public void testTransactionWithPropagationSupportsAndInnerTransaction() throws Exception { final SessionFactory sf = mock(SessionFactory.class); @@ -735,7 +776,7 @@ public class HibernateTransactionManagerTests { catch (DataIntegrityViolationException ex) { // expected assertEquals(rootCause, ex.getCause()); - assertTrue(ex.getMessage().indexOf("mymsg") != -1); + assertTrue(ex.getMessage().contains("mymsg")); } assertTrue("Hasn't thread session", !TransactionSynchronizationManager.hasResource(sf));