Browse Source

Expose savepoint callbacks on TransactionSynchronization

Closes gh-30509
pull/32367/head
Juergen Hoeller 2 years ago
parent
commit
861ef88d9f
  1. 6
      spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java
  2. 134
      spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java
  3. 16
      spring-tx/src/main/java/org/springframework/transaction/support/AbstractTransactionStatus.java
  4. 30
      spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronization.java
  5. 34
      spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java

6
spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -478,9 +478,7 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
@Override @Override
public void flush() { public void flush() {
if (TransactionSynchronizationManager.isSynchronizationActive()) { TransactionSynchronizationUtils.triggerFlush();
TransactionSynchronizationUtils.triggerFlush();
}
} }
} }

134
spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java

@ -127,7 +127,7 @@ public class DataSourceTransactionManagerTests {
} }
private void doTestTransactionCommitRestoringAutoCommit( private void doTestTransactionCommitRestoringAutoCommit(
boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { boolean autoCommit, boolean lazyConnection, boolean createStatement) throws Exception {
given(con.getAutoCommit()).willReturn(autoCommit); given(con.getAutoCommit()).willReturn(autoCommit);
@ -136,7 +136,7 @@ public class DataSourceTransactionManagerTests {
given(con.getWarnings()).willThrow(new SQLException()); given(con.getWarnings()).willThrow(new SQLException());
} }
final DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds);
tm = createTransactionManager(dsToUse); tm = createTransactionManager(dsToUse);
TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).isFalse();
@ -214,7 +214,7 @@ public class DataSourceTransactionManagerTests {
} }
private void doTestTransactionRollbackRestoringAutoCommit( private void doTestTransactionRollbackRestoringAutoCommit(
boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { boolean autoCommit, boolean lazyConnection, boolean createStatement) throws Exception {
given(con.getAutoCommit()).willReturn(autoCommit); given(con.getAutoCommit()).willReturn(autoCommit);
@ -222,13 +222,13 @@ public class DataSourceTransactionManagerTests {
given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED);
} }
final DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds);
tm = createTransactionManager(dsToUse); tm = createTransactionManager(dsToUse);
TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
final RuntimeException ex = new RuntimeException("Application exception"); RuntimeException ex = new RuntimeException("Application exception");
assertThatRuntimeException().isThrownBy(() -> assertThatRuntimeException().isThrownBy(() ->
tt.execute(new TransactionCallbackWithoutResult() { tt.execute(new TransactionCallbackWithoutResult() {
@Override @Override
@ -276,7 +276,7 @@ public class DataSourceTransactionManagerTests {
ConnectionHolder conHolder = new ConnectionHolder(con, true); ConnectionHolder conHolder = new ConnectionHolder(con, true);
TransactionSynchronizationManager.bindResource(ds, conHolder); TransactionSynchronizationManager.bindResource(ds, conHolder);
final RuntimeException ex = new RuntimeException("Application exception"); RuntimeException ex = new RuntimeException("Application exception");
try { try {
tt.execute(new TransactionCallbackWithoutResult() { tt.execute(new TransactionCallbackWithoutResult() {
@Override @Override
@ -328,7 +328,7 @@ public class DataSourceTransactionManagerTests {
try { try {
assertThat(ts.isNewTransaction()).isTrue(); assertThat(ts.isNewTransaction()).isTrue();
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.execute(new TransactionCallbackWithoutResult() { tt.execute(new TransactionCallbackWithoutResult() {
@Override @Override
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
@ -383,8 +383,8 @@ public class DataSourceTransactionManagerTests {
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> {
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
final TransactionTemplate tt2 = new TransactionTemplate(tm); TransactionTemplate tt2 = new TransactionTemplate(tm);
tt2.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); tt2.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
tt.execute(new TransactionCallbackWithoutResult() { tt.execute(new TransactionCallbackWithoutResult() {
@ -416,9 +416,9 @@ public class DataSourceTransactionManagerTests {
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> {
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setReadOnly(true); tt.setReadOnly(true);
final TransactionTemplate tt2 = new TransactionTemplate(tm); TransactionTemplate tt2 = new TransactionTemplate(tm);
tt2.setReadOnly(false); tt2.setReadOnly(false);
tt.execute(new TransactionCallbackWithoutResult() { tt.execute(new TransactionCallbackWithoutResult() {
@ -446,10 +446,10 @@ public class DataSourceTransactionManagerTests {
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
final TestTransactionSynchronization synch = TestTransactionSynchronization synch =
new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) { new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) {
@Override @Override
protected void doAfterCompletion(int status) { protected void doAfterCompletion(int status) {
@ -483,15 +483,15 @@ public class DataSourceTransactionManagerTests {
@Test @Test
void testParticipatingTransactionWithDifferentConnectionObtainedFromSynch() throws Exception { void testParticipatingTransactionWithDifferentConnectionObtainedFromSynch() throws Exception {
DataSource ds2 = mock(); DataSource ds2 = mock();
final Connection con2 = mock(); Connection con2 = mock();
given(ds2.getConnection()).willReturn(con2); given(ds2.getConnection()).willReturn(con2);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
final TestTransactionSynchronization synch = TestTransactionSynchronization synch =
new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) { new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) {
@Override @Override
protected void doAfterCompletion(int status) { protected void doAfterCompletion(int status) {
@ -529,12 +529,12 @@ public class DataSourceTransactionManagerTests {
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
TransactionStatus ts = tm.getTransaction(new DefaultTransactionDefinition()); TransactionStatus ts = tm.getTransaction(new DefaultTransactionDefinition());
final TestTransactionSynchronization synch = TestTransactionSynchronization synch =
new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_UNKNOWN); new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_UNKNOWN);
assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> { assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> {
assertThat(ts.isNewTransaction()).isTrue(); assertThat(ts.isNewTransaction()).isTrue();
final TransactionTemplate tt = new TransactionTemplate(tm2); TransactionTemplate tt = new TransactionTemplate(tm2);
tt.execute(new TransactionCallbackWithoutResult() { tt.execute(new TransactionCallbackWithoutResult() {
@Override @Override
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
@ -569,7 +569,7 @@ public class DataSourceTransactionManagerTests {
@Test @Test
void testPropagationRequiresNewWithExistingTransaction() throws Exception { void testPropagationRequiresNewWithExistingTransaction() throws Exception {
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
@ -607,14 +607,14 @@ public class DataSourceTransactionManagerTests {
@Test @Test
void testPropagationRequiresNewWithExistingTransactionAndUnrelatedDataSource() throws Exception { void testPropagationRequiresNewWithExistingTransactionAndUnrelatedDataSource() throws Exception {
Connection con2 = mock(); Connection con2 = mock();
final DataSource ds2 = mock(); DataSource ds2 = mock();
given(ds2.getConnection()).willReturn(con2); given(ds2.getConnection()).willReturn(con2);
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
PlatformTransactionManager tm2 = createTransactionManager(ds2); PlatformTransactionManager tm2 = createTransactionManager(ds2);
final TransactionTemplate tt2 = new TransactionTemplate(tm2); TransactionTemplate tt2 = new TransactionTemplate(tm2);
tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
@ -655,16 +655,16 @@ public class DataSourceTransactionManagerTests {
@Test @Test
void testPropagationRequiresNewWithExistingTransactionAndUnrelatedFailingDataSource() throws Exception { void testPropagationRequiresNewWithExistingTransactionAndUnrelatedFailingDataSource() throws Exception {
final DataSource ds2 = mock(); DataSource ds2 = mock();
SQLException failure = new SQLException(); SQLException failure = new SQLException();
given(ds2.getConnection()).willThrow(failure); given(ds2.getConnection()).willThrow(failure);
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
DataSourceTransactionManager tm2 = createTransactionManager(ds2); DataSourceTransactionManager tm2 = createTransactionManager(ds2);
tm2.setTransactionSynchronization(DataSourceTransactionManager.SYNCHRONIZATION_NEVER); tm2.setTransactionSynchronization(DataSourceTransactionManager.SYNCHRONIZATION_NEVER);
final TransactionTemplate tt2 = new TransactionTemplate(tm2); TransactionTemplate tt2 = new TransactionTemplate(tm2);
tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
@ -699,7 +699,7 @@ public class DataSourceTransactionManagerTests {
@Test @Test
void testPropagationNotSupportedWithExistingTransaction() throws Exception { void testPropagationNotSupportedWithExistingTransaction() throws Exception {
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
@ -740,7 +740,7 @@ public class DataSourceTransactionManagerTests {
@Test @Test
void testPropagationNeverWithExistingTransaction() throws Exception { void testPropagationNeverWithExistingTransaction() throws Exception {
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
@ -806,11 +806,10 @@ public class DataSourceTransactionManagerTests {
@Test @Test
void testPropagationSupportsAndRequiresNewWithEarlyAccess() throws Exception { void testPropagationSupportsAndRequiresNewWithEarlyAccess() throws Exception {
final Connection con1 = mock(); Connection con1 = mock();
final Connection con2 = mock(); Connection con2 = mock();
given(ds.getConnection()).willReturn(con1, con2); given(ds.getConnection()).willReturn(con1, con2);
final
TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
@ -1132,7 +1131,7 @@ public class DataSourceTransactionManagerTests {
void testTransactionAwareDataSourceProxyWithSuspension() throws Exception { void testTransactionAwareDataSourceProxyWithSuspension() throws Exception {
given(con.getAutoCommit()).willReturn(true); given(con.getAutoCommit()).willReturn(true);
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
@ -1141,7 +1140,7 @@ public class DataSourceTransactionManagerTests {
protected void doInTransactionWithoutResult(TransactionStatus status) { protected void doInTransactionWithoutResult(TransactionStatus status) {
// something transactional // something transactional
assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con);
final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds);
try { try {
assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con);
// should be ignored // should be ignored
@ -1190,7 +1189,7 @@ public class DataSourceTransactionManagerTests {
void testTransactionAwareDataSourceProxyWithSuspensionAndReobtaining() throws Exception { void testTransactionAwareDataSourceProxyWithSuspensionAndReobtaining() throws Exception {
given(con.getAutoCommit()).willReturn(true); given(con.getAutoCommit()).willReturn(true);
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
@ -1199,7 +1198,7 @@ public class DataSourceTransactionManagerTests {
protected void doInTransactionWithoutResult(TransactionStatus status) { protected void doInTransactionWithoutResult(TransactionStatus status) {
// something transactional // something transactional
assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con);
final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds);
dsProxy.setReobtainTransactionalConnections(true); dsProxy.setReobtainTransactionalConnections(true);
try { try {
assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con);
@ -1395,7 +1394,7 @@ public class DataSourceTransactionManagerTests {
doTestExistingTransactionWithPropagationNested(2); doTestExistingTransactionWithPropagationNested(2);
} }
private void doTestExistingTransactionWithPropagationNested(final int count) throws Exception { private void doTestExistingTransactionWithPropagationNested(int count) throws Exception {
DatabaseMetaData md = mock(); DatabaseMetaData md = mock();
Savepoint sp = mock(); Savepoint sp = mock();
@ -1405,7 +1404,7 @@ public class DataSourceTransactionManagerTests {
given(con.setSavepoint(ConnectionHolder.SAVEPOINT_NAME_PREFIX + i)).willReturn(sp); given(con.setSavepoint(ConnectionHolder.SAVEPOINT_NAME_PREFIX + i)).willReturn(sp);
} }
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
@ -1417,6 +1416,8 @@ public class DataSourceTransactionManagerTests {
assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue();
assertThat(status.isNested()).isFalse(); assertThat(status.isNested()).isFalse();
assertThat(status.hasSavepoint()).isFalse(); assertThat(status.hasSavepoint()).isFalse();
TestSavepointSynchronization synch = new TestSavepointSynchronization();
TransactionSynchronizationManager.registerSynchronization(synch);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
tt.execute(new TransactionCallbackWithoutResult() { tt.execute(new TransactionCallbackWithoutResult() {
@Override @Override
@ -1427,8 +1428,11 @@ public class DataSourceTransactionManagerTests {
assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNewTransaction()).isFalse();
assertThat(status.isNested()).isTrue(); assertThat(status.isNested()).isTrue();
assertThat(status.hasSavepoint()).isTrue(); assertThat(status.hasSavepoint()).isTrue();
assertThat(synch.savepointCalled).isTrue();
} }
}); });
assertThat(synch.savepointRollbackCalled).isFalse();
synch.savepointCalled = false;
} }
assertThat(status.hasTransaction()).isTrue(); assertThat(status.hasTransaction()).isTrue();
assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue();
@ -1452,7 +1456,7 @@ public class DataSourceTransactionManagerTests {
given(con.getMetaData()).willReturn(md); given(con.getMetaData()).willReturn(md);
given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp);
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
@ -1464,6 +1468,8 @@ public class DataSourceTransactionManagerTests {
assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue();
assertThat(status.isNested()).isFalse(); assertThat(status.isNested()).isFalse();
assertThat(status.hasSavepoint()).isFalse(); assertThat(status.hasSavepoint()).isFalse();
TestSavepointSynchronization synch = new TestSavepointSynchronization();
TransactionSynchronizationManager.registerSynchronization(synch);
tt.execute(new TransactionCallbackWithoutResult() { tt.execute(new TransactionCallbackWithoutResult() {
@Override @Override
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
@ -1473,9 +1479,12 @@ public class DataSourceTransactionManagerTests {
assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNewTransaction()).isFalse();
assertThat(status.isNested()).isTrue(); assertThat(status.isNested()).isTrue();
assertThat(status.hasSavepoint()).isTrue(); assertThat(status.hasSavepoint()).isTrue();
assertThat(synch.savepointCalled).isTrue();
assertThat(synch.savepointRollbackCalled).isFalse();
status.setRollbackOnly(); status.setRollbackOnly();
} }
}); });
assertThat(synch.savepointRollbackCalled).isTrue();
assertThat(status.hasTransaction()).isTrue(); assertThat(status.hasTransaction()).isTrue();
assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue();
assertThat(status.isNested()).isFalse(); assertThat(status.isNested()).isFalse();
@ -1499,7 +1508,7 @@ public class DataSourceTransactionManagerTests {
given(con.getMetaData()).willReturn(md); given(con.getMetaData()).willReturn(md);
given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp);
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
@ -1511,6 +1520,8 @@ public class DataSourceTransactionManagerTests {
assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue();
assertThat(status.isNested()).isFalse(); assertThat(status.isNested()).isFalse();
assertThat(status.hasSavepoint()).isFalse(); assertThat(status.hasSavepoint()).isFalse();
TestSavepointSynchronization synch = new TestSavepointSynchronization();
TransactionSynchronizationManager.registerSynchronization(synch);
assertThatIllegalStateException().isThrownBy(() -> assertThatIllegalStateException().isThrownBy(() ->
tt.execute(new TransactionCallbackWithoutResult() { tt.execute(new TransactionCallbackWithoutResult() {
@Override @Override
@ -1521,6 +1532,8 @@ public class DataSourceTransactionManagerTests {
assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNewTransaction()).isFalse();
assertThat(status.isNested()).isTrue(); assertThat(status.isNested()).isTrue();
assertThat(status.hasSavepoint()).isTrue(); assertThat(status.hasSavepoint()).isTrue();
assertThat(synch.savepointCalled).isTrue();
assertThat(synch.savepointRollbackCalled).isFalse();
TransactionTemplate ntt = new TransactionTemplate(tm); TransactionTemplate ntt = new TransactionTemplate(tm);
ntt.execute(new TransactionCallbackWithoutResult() { ntt.execute(new TransactionCallbackWithoutResult() {
@Override @Override
@ -1536,6 +1549,7 @@ public class DataSourceTransactionManagerTests {
}); });
} }
})); }));
assertThat(synch.savepointRollbackCalled).isTrue();
assertThat(status.hasTransaction()).isTrue(); assertThat(status.hasTransaction()).isTrue();
assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue();
assertThat(status.isNested()).isFalse(); assertThat(status.isNested()).isFalse();
@ -1559,7 +1573,7 @@ public class DataSourceTransactionManagerTests {
given(con.getMetaData()).willReturn(md); given(con.getMetaData()).willReturn(md);
given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp);
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
@ -1571,6 +1585,8 @@ public class DataSourceTransactionManagerTests {
assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue();
assertThat(status.isNested()).isFalse(); assertThat(status.isNested()).isFalse();
assertThat(status.hasSavepoint()).isFalse(); assertThat(status.hasSavepoint()).isFalse();
TestSavepointSynchronization synch = new TestSavepointSynchronization();
TransactionSynchronizationManager.registerSynchronization(synch);
assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() ->
tt.execute(new TransactionCallbackWithoutResult() { tt.execute(new TransactionCallbackWithoutResult() {
@Override @Override
@ -1581,6 +1597,8 @@ public class DataSourceTransactionManagerTests {
assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNewTransaction()).isFalse();
assertThat(status.isNested()).isTrue(); assertThat(status.isNested()).isTrue();
assertThat(status.hasSavepoint()).isTrue(); assertThat(status.hasSavepoint()).isTrue();
assertThat(synch.savepointCalled).isTrue();
assertThat(synch.savepointRollbackCalled).isFalse();
TransactionTemplate ntt = new TransactionTemplate(tm); TransactionTemplate ntt = new TransactionTemplate(tm);
ntt.execute(new TransactionCallbackWithoutResult() { ntt.execute(new TransactionCallbackWithoutResult() {
@Override @Override
@ -1596,6 +1614,7 @@ public class DataSourceTransactionManagerTests {
}); });
} }
})); }));
assertThat(synch.savepointRollbackCalled).isTrue();
assertThat(status.hasTransaction()).isTrue(); assertThat(status.hasTransaction()).isTrue();
assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue();
assertThat(status.isNested()).isFalse(); assertThat(status.isNested()).isFalse();
@ -1619,7 +1638,7 @@ public class DataSourceTransactionManagerTests {
given(con.getMetaData()).willReturn(md); given(con.getMetaData()).willReturn(md);
given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp);
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
@ -1631,8 +1650,12 @@ public class DataSourceTransactionManagerTests {
assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue();
assertThat(status.isNested()).isFalse(); assertThat(status.isNested()).isFalse();
assertThat(status.hasSavepoint()).isFalse(); assertThat(status.hasSavepoint()).isFalse();
TestSavepointSynchronization synch = new TestSavepointSynchronization();
TransactionSynchronizationManager.registerSynchronization(synch);
Object savepoint = status.createSavepoint(); Object savepoint = status.createSavepoint();
assertThat(synch.savepointCalled).isTrue();
status.releaseSavepoint(savepoint); status.releaseSavepoint(savepoint);
assertThat(synch.savepointRollbackCalled).isFalse();
} }
}); });
@ -1652,7 +1675,7 @@ public class DataSourceTransactionManagerTests {
given(con.getMetaData()).willReturn(md); given(con.getMetaData()).willReturn(md);
given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp);
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
@ -1664,8 +1687,13 @@ public class DataSourceTransactionManagerTests {
assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue();
assertThat(status.isNested()).isFalse(); assertThat(status.isNested()).isFalse();
assertThat(status.hasSavepoint()).isFalse(); assertThat(status.hasSavepoint()).isFalse();
TestSavepointSynchronization synch = new TestSavepointSynchronization();
TransactionSynchronizationManager.registerSynchronization(synch);
Object savepoint = status.createSavepoint(); Object savepoint = status.createSavepoint();
assertThat(synch.savepointCalled).isTrue();
assertThat(synch.savepointRollbackCalled).isFalse();
status.rollbackToSavepoint(savepoint); status.rollbackToSavepoint(savepoint);
assertThat(synch.savepointRollbackCalled).isTrue();
} }
}); });
@ -1677,7 +1705,7 @@ public class DataSourceTransactionManagerTests {
@Test @Test
void testTransactionWithPropagationNested() throws Exception { void testTransactionWithPropagationNested() throws Exception {
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
@ -1703,7 +1731,7 @@ public class DataSourceTransactionManagerTests {
@Test @Test
void testTransactionWithPropagationNestedAndRollback() throws Exception { void testTransactionWithPropagationNestedAndRollback() throws Exception {
final TransactionTemplate tt = new TransactionTemplate(tm); TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED);
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
@ -1805,4 +1833,24 @@ public class DataSourceTransactionManagerTests {
} }
} }
private static class TestSavepointSynchronization implements TransactionSynchronization {
public boolean savepointCalled;
public boolean savepointRollbackCalled;
@Override
public void savepoint(Object savepoint) {
assertThat(this.savepointCalled).isFalse();
this.savepointCalled = true;
}
@Override
public void savepointRollback(Object savepoint) {
assertThat(this.savepointRollbackCalled).isFalse();
this.savepointRollbackCalled = true;
}
}
} }

16
spring-tx/src/main/java/org/springframework/transaction/support/AbstractTransactionStatus.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -138,14 +138,19 @@ public abstract class AbstractTransactionStatus implements TransactionStatus {
* Create a savepoint and hold it for the transaction. * Create a savepoint and hold it for the transaction.
* @throws org.springframework.transaction.NestedTransactionNotSupportedException * @throws org.springframework.transaction.NestedTransactionNotSupportedException
* if the underlying transaction does not support savepoints * if the underlying transaction does not support savepoints
* @see SavepointManager#createSavepoint
*/ */
public void createAndHoldSavepoint() throws TransactionException { public void createAndHoldSavepoint() throws TransactionException {
setSavepoint(getSavepointManager().createSavepoint()); Object savepoint = getSavepointManager().createSavepoint();
TransactionSynchronizationUtils.triggerSavepoint(savepoint);
setSavepoint(savepoint);
} }
/** /**
* Roll back to the savepoint that is held for the transaction * Roll back to the savepoint that is held for the transaction
* and release the savepoint right afterwards. * and release the savepoint right afterwards.
* @see SavepointManager#rollbackToSavepoint
* @see SavepointManager#releaseSavepoint
*/ */
public void rollbackToHeldSavepoint() throws TransactionException { public void rollbackToHeldSavepoint() throws TransactionException {
Object savepoint = getSavepoint(); Object savepoint = getSavepoint();
@ -153,6 +158,7 @@ public abstract class AbstractTransactionStatus implements TransactionStatus {
throw new TransactionUsageException( throw new TransactionUsageException(
"Cannot roll back to savepoint - no savepoint associated with current transaction"); "Cannot roll back to savepoint - no savepoint associated with current transaction");
} }
TransactionSynchronizationUtils.triggerSavepointRollback(savepoint);
getSavepointManager().rollbackToSavepoint(savepoint); getSavepointManager().rollbackToSavepoint(savepoint);
getSavepointManager().releaseSavepoint(savepoint); getSavepointManager().releaseSavepoint(savepoint);
setSavepoint(null); setSavepoint(null);
@ -160,6 +166,7 @@ public abstract class AbstractTransactionStatus implements TransactionStatus {
/** /**
* Release the savepoint that is held for the transaction. * Release the savepoint that is held for the transaction.
* @see SavepointManager#releaseSavepoint
*/ */
public void releaseHeldSavepoint() throws TransactionException { public void releaseHeldSavepoint() throws TransactionException {
Object savepoint = getSavepoint(); Object savepoint = getSavepoint();
@ -184,7 +191,9 @@ public abstract class AbstractTransactionStatus implements TransactionStatus {
*/ */
@Override @Override
public Object createSavepoint() throws TransactionException { public Object createSavepoint() throws TransactionException {
return getSavepointManager().createSavepoint(); Object savepoint = getSavepointManager().createSavepoint();
TransactionSynchronizationUtils.triggerSavepoint(savepoint);
return savepoint;
} }
/** /**
@ -195,6 +204,7 @@ public abstract class AbstractTransactionStatus implements TransactionStatus {
*/ */
@Override @Override
public void rollbackToSavepoint(Object savepoint) throws TransactionException { public void rollbackToSavepoint(Object savepoint) throws TransactionException {
TransactionSynchronizationUtils.triggerSavepointRollback(savepoint);
getSavepointManager().rollbackToSavepoint(savepoint); getSavepointManager().rollbackToSavepoint(savepoint);
} }

30
spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronization.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2020 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -88,6 +88,34 @@ public interface TransactionSynchronization extends Ordered, Flushable {
default void flush() { default void flush() {
} }
/**
* Invoked on creation of a new savepoint, either when a nested transaction
* is started against an existing transaction or on a programmatic savepoint
* via {@link org.springframework.transaction.TransactionStatus}.
* <p>This synchronization callback is invoked right <i>after</i> the creation
* of the resource savepoint, with the given savepoint object already active.
* @param savepoint the associated savepoint object (primarily as a key for
* identifying the savepoint but also castable to the resource savepoint type)
* @since 6.2
* @see org.springframework.transaction.SavepointManager#createSavepoint
* @see org.springframework.transaction.TransactionDefinition#PROPAGATION_NESTED
*/
default void savepoint(Object savepoint) {
}
/**
* Invoked in case of a rollback to the previously created savepoint.
* <p>This synchronization callback is invoked right <i>before</i> the rollback
* of the resource savepoint, with the given savepoint object still active.
* @param savepoint the associated savepoint object (primarily as a key for
* identifying the savepoint but also castable to the resource savepoint type)
* @since 6.2
* @see #savepoint
* @see org.springframework.transaction.SavepointManager#rollbackToSavepoint
*/
default void savepointRollback(Object savepoint) {
}
/** /**
* Invoked before transaction commit (before "beforeCompletion"). * Invoked before transaction commit (before "beforeCompletion").
* Can e.g. flush transactional O/R Mapping sessions to the database. * Can e.g. flush transactional O/R Mapping sessions to the database.

34
spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java

@ -81,8 +81,38 @@ public abstract class TransactionSynchronizationUtils {
* @see TransactionSynchronization#flush() * @see TransactionSynchronization#flush()
*/ */
public static void triggerFlush() { public static void triggerFlush() {
for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) { if (TransactionSynchronizationManager.isSynchronizationActive()) {
synchronization.flush(); for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) {
synchronization.flush();
}
}
}
/**
* Trigger {@code flush} callbacks on all currently registered synchronizations.
* @throws RuntimeException if thrown by a {@code savepoint} callback
* @since 6.2
* @see TransactionSynchronization#savepoint
*/
static void triggerSavepoint(Object savepoint) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) {
synchronization.savepoint(savepoint);
}
}
}
/**
* Trigger {@code flush} callbacks on all currently registered synchronizations.
* @throws RuntimeException if thrown by a {@code savepointRollback} callback
* @since 6.2
* @see TransactionSynchronization#savepointRollback
*/
static void triggerSavepointRollback(Object savepoint) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) {
synchronization.savepointRollback(savepoint);
}
} }
} }

Loading…
Cancel
Save