diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java index f67d848b085b..8fe8a37a2ea4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java +++ b/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"); * you may not use this file except in compliance with the License. @@ -478,9 +478,7 @@ public boolean isRollbackOnly() { @Override public void flush() { - if (TransactionSynchronizationManager.isSynchronizationActive()) { - TransactionSynchronizationUtils.triggerFlush(); - } + TransactionSynchronizationUtils.triggerFlush(); } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java index 02f87c4d6901..81cc8142e1c8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java @@ -127,7 +127,7 @@ void testTransactionCommitWithAutoCommitFalseAndLazyConnectionAndStatementCreate } 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); @@ -136,7 +136,7 @@ private void doTestTransactionCommitRestoringAutoCommit( given(con.getWarnings()).willThrow(new SQLException()); } - final DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); + DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); tm = createTransactionManager(dsToUse); TransactionTemplate tt = new TransactionTemplate(tm); assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).isFalse(); @@ -214,7 +214,7 @@ void testTransactionRollbackWithAutoCommitFalseAndLazyConnectionAndCreateStateme } 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); @@ -222,13 +222,13 @@ private void doTestTransactionRollbackRestoringAutoCommit( 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); TransactionTemplate tt = new TransactionTemplate(tm); assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); - final RuntimeException ex = new RuntimeException("Application exception"); + RuntimeException ex = new RuntimeException("Application exception"); assertThatRuntimeException().isThrownBy(() -> tt.execute(new TransactionCallbackWithoutResult() { @Override @@ -276,7 +276,7 @@ void testTransactionRollbackOnly() { ConnectionHolder conHolder = new ConnectionHolder(con, true); TransactionSynchronizationManager.bindResource(ds, conHolder); - final RuntimeException ex = new RuntimeException("Application exception"); + RuntimeException ex = new RuntimeException("Application exception"); try { tt.execute(new TransactionCallbackWithoutResult() { @Override @@ -328,7 +328,7 @@ private void doTestParticipatingTransactionWithRollbackOnly(boolean failEarly) t try { assertThat(ts.isNewTransaction()).isTrue(); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { @@ -383,8 +383,8 @@ void testParticipatingTransactionWithIncompatibleIsolationLevel() throws Excepti assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { - final TransactionTemplate tt = new TransactionTemplate(tm); - final TransactionTemplate tt2 = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt2 = new TransactionTemplate(tm); tt2.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); tt.execute(new TransactionCallbackWithoutResult() { @@ -416,9 +416,9 @@ void testParticipatingTransactionWithIncompatibleReadOnly() throws Exception { assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setReadOnly(true); - final TransactionTemplate tt2 = new TransactionTemplate(tm); + TransactionTemplate tt2 = new TransactionTemplate(tm); tt2.setReadOnly(false); tt.execute(new TransactionCallbackWithoutResult() { @@ -446,10 +446,10 @@ void testParticipatingTransactionWithTransactionStartedFromSynch() throws Except assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - final TestTransactionSynchronization synch = + TestTransactionSynchronization synch = new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) { @Override protected void doAfterCompletion(int status) { @@ -483,15 +483,15 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testParticipatingTransactionWithDifferentConnectionObtainedFromSynch() throws Exception { DataSource ds2 = mock(); - final Connection con2 = mock(); + Connection con2 = mock(); given(ds2.getConnection()).willReturn(con2); assertThat(TransactionSynchronizationManager.hasResource(ds)).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) { @Override protected void doAfterCompletion(int status) { @@ -529,12 +529,12 @@ void testParticipatingTransactionWithRollbackOnlyAndInnerSynch() throws Exceptio assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); TransactionStatus ts = tm.getTransaction(new DefaultTransactionDefinition()); - final TestTransactionSynchronization synch = + TestTransactionSynchronization synch = new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_UNKNOWN); assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> { assertThat(ts.isNewTransaction()).isTrue(); - final TransactionTemplate tt = new TransactionTemplate(tm2); + TransactionTemplate tt = new TransactionTemplate(tm2); tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { @@ -569,7 +569,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationRequiresNewWithExistingTransaction() throws Exception { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -607,14 +607,14 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationRequiresNewWithExistingTransactionAndUnrelatedDataSource() throws Exception { Connection con2 = mock(); - final DataSource ds2 = mock(); + DataSource ds2 = mock(); given(ds2.getConnection()).willReturn(con2); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); PlatformTransactionManager tm2 = createTransactionManager(ds2); - final TransactionTemplate tt2 = new TransactionTemplate(tm2); + TransactionTemplate tt2 = new TransactionTemplate(tm2); tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); @@ -655,16 +655,16 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationRequiresNewWithExistingTransactionAndUnrelatedFailingDataSource() throws Exception { - final DataSource ds2 = mock(); + DataSource ds2 = mock(); SQLException failure = new SQLException(); given(ds2.getConnection()).willThrow(failure); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); DataSourceTransactionManager tm2 = createTransactionManager(ds2); tm2.setTransactionSynchronization(DataSourceTransactionManager.SYNCHRONIZATION_NEVER); - final TransactionTemplate tt2 = new TransactionTemplate(tm2); + TransactionTemplate tt2 = new TransactionTemplate(tm2); tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); @@ -699,7 +699,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationNotSupportedWithExistingTransaction() throws Exception { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -740,7 +740,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationNeverWithExistingTransaction() throws Exception { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -806,11 +806,10 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationSupportsAndRequiresNewWithEarlyAccess() throws Exception { - final Connection con1 = mock(); - final Connection con2 = mock(); + Connection con1 = mock(); + Connection con2 = mock(); given(ds.getConnection()).willReturn(con1, con2); - final TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); @@ -1132,7 +1131,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { void testTransactionAwareDataSourceProxyWithSuspension() throws Exception { given(con.getAutoCommit()).willReturn(true); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); @@ -1141,7 +1140,7 @@ void testTransactionAwareDataSourceProxyWithSuspension() throws Exception { protected void doInTransactionWithoutResult(TransactionStatus status) { // something transactional assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); - final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); try { assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); // should be ignored @@ -1190,7 +1189,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { void testTransactionAwareDataSourceProxyWithSuspensionAndReobtaining() throws Exception { given(con.getAutoCommit()).willReturn(true); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); @@ -1199,7 +1198,7 @@ void testTransactionAwareDataSourceProxyWithSuspensionAndReobtaining() throws Ex protected void doInTransactionWithoutResult(TransactionStatus status) { // something transactional assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); - final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); dsProxy.setReobtainTransactionalConnections(true); try { assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); @@ -1395,7 +1394,7 @@ void testExistingTransactionWithPropagationNestedTwice() throws Exception { doTestExistingTransactionWithPropagationNested(2); } - private void doTestExistingTransactionWithPropagationNested(final int count) throws Exception { + private void doTestExistingTransactionWithPropagationNested(int count) throws Exception { DatabaseMetaData md = mock(); Savepoint sp = mock(); @@ -1405,7 +1404,7 @@ private void doTestExistingTransactionWithPropagationNested(final int count) thr 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); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1417,6 +1416,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); for (int i = 0; i < count; i++) { tt.execute(new TransactionCallbackWithoutResult() { @Override @@ -1427,8 +1428,11 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNested()).isTrue(); assertThat(status.hasSavepoint()).isTrue(); + assertThat(synch.savepointCalled).isTrue(); } }); + assertThat(synch.savepointRollbackCalled).isFalse(); + synch.savepointCalled = false; } assertThat(status.hasTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue(); @@ -1452,7 +1456,7 @@ void testExistingTransactionWithPropagationNestedAndRollback() throws Exception given(con.getMetaData()).willReturn(md); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1464,6 +1468,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { @@ -1473,9 +1479,12 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNested()).isTrue(); assertThat(status.hasSavepoint()).isTrue(); + assertThat(synch.savepointCalled).isTrue(); + assertThat(synch.savepointRollbackCalled).isFalse(); status.setRollbackOnly(); } }); + assertThat(synch.savepointRollbackCalled).isTrue(); assertThat(status.hasTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); @@ -1499,7 +1508,7 @@ void testExistingTransactionWithPropagationNestedAndRequiredRollback() throws Ex given(con.getMetaData()).willReturn(md); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1511,6 +1520,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); assertThatIllegalStateException().isThrownBy(() -> tt.execute(new TransactionCallbackWithoutResult() { @Override @@ -1521,6 +1532,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNested()).isTrue(); assertThat(status.hasSavepoint()).isTrue(); + assertThat(synch.savepointCalled).isTrue(); + assertThat(synch.savepointRollbackCalled).isFalse(); TransactionTemplate ntt = new TransactionTemplate(tm); ntt.execute(new TransactionCallbackWithoutResult() { @Override @@ -1536,6 +1549,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run }); } })); + assertThat(synch.savepointRollbackCalled).isTrue(); assertThat(status.hasTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); @@ -1559,7 +1573,7 @@ void testExistingTransactionWithPropagationNestedAndRequiredRollbackOnly() throw given(con.getMetaData()).willReturn(md); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1571,6 +1585,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> tt.execute(new TransactionCallbackWithoutResult() { @Override @@ -1581,6 +1597,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNested()).isTrue(); assertThat(status.hasSavepoint()).isTrue(); + assertThat(synch.savepointCalled).isTrue(); + assertThat(synch.savepointRollbackCalled).isFalse(); TransactionTemplate ntt = new TransactionTemplate(tm); ntt.execute(new TransactionCallbackWithoutResult() { @Override @@ -1596,6 +1614,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run }); } })); + assertThat(synch.savepointRollbackCalled).isTrue(); assertThat(status.hasTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); @@ -1619,7 +1638,7 @@ void testExistingTransactionWithManualSavepoint() throws Exception { given(con.getMetaData()).willReturn(md); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1631,8 +1650,12 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); Object savepoint = status.createSavepoint(); + assertThat(synch.savepointCalled).isTrue(); status.releaseSavepoint(savepoint); + assertThat(synch.savepointRollbackCalled).isFalse(); } }); @@ -1652,7 +1675,7 @@ void testExistingTransactionWithManualSavepointAndRollback() throws Exception { given(con.getMetaData()).willReturn(md); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1664,8 +1687,13 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); Object savepoint = status.createSavepoint(); + assertThat(synch.savepointCalled).isTrue(); + assertThat(synch.savepointRollbackCalled).isFalse(); status.rollbackToSavepoint(savepoint); + assertThat(synch.savepointRollbackCalled).isTrue(); } }); @@ -1677,7 +1705,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testTransactionWithPropagationNested() throws Exception { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1703,7 +1731,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testTransactionWithPropagationNestedAndRollback() throws Exception { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1805,4 +1833,24 @@ protected void doAfterCompletion(int status) { } } + + 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; + } + } + } diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/AbstractTransactionStatus.java b/spring-tx/src/main/java/org/springframework/transaction/support/AbstractTransactionStatus.java index e0ce47b45bfe..fa7fcff4ae8f 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/AbstractTransactionStatus.java +++ b/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"); * you may not use this file except in compliance with the License. @@ -138,14 +138,19 @@ protected Object getSavepoint() { * Create a savepoint and hold it for the transaction. * @throws org.springframework.transaction.NestedTransactionNotSupportedException * if the underlying transaction does not support savepoints + * @see SavepointManager#createSavepoint */ 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 * and release the savepoint right afterwards. + * @see SavepointManager#rollbackToSavepoint + * @see SavepointManager#releaseSavepoint */ public void rollbackToHeldSavepoint() throws TransactionException { Object savepoint = getSavepoint(); @@ -153,6 +158,7 @@ public void rollbackToHeldSavepoint() throws TransactionException { throw new TransactionUsageException( "Cannot roll back to savepoint - no savepoint associated with current transaction"); } + TransactionSynchronizationUtils.triggerSavepointRollback(savepoint); getSavepointManager().rollbackToSavepoint(savepoint); getSavepointManager().releaseSavepoint(savepoint); setSavepoint(null); @@ -160,6 +166,7 @@ public void rollbackToHeldSavepoint() throws TransactionException { /** * Release the savepoint that is held for the transaction. + * @see SavepointManager#releaseSavepoint */ public void releaseHeldSavepoint() throws TransactionException { Object savepoint = getSavepoint(); @@ -184,7 +191,9 @@ public void releaseHeldSavepoint() throws TransactionException { */ @Override public Object createSavepoint() throws TransactionException { - return getSavepointManager().createSavepoint(); + Object savepoint = getSavepointManager().createSavepoint(); + TransactionSynchronizationUtils.triggerSavepoint(savepoint); + return savepoint; } /** @@ -195,6 +204,7 @@ public Object createSavepoint() throws TransactionException { */ @Override public void rollbackToSavepoint(Object savepoint) throws TransactionException { + TransactionSynchronizationUtils.triggerSavepointRollback(savepoint); getSavepointManager().rollbackToSavepoint(savepoint); } diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronization.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronization.java index bdf94d9567fb..36f1dd761a66 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronization.java +++ b/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"); * you may not use this file except in compliance with the License. @@ -88,6 +88,34 @@ default void resume() { 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}. + *

This synchronization callback is invoked right after 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. + *

This synchronization callback is invoked right before 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"). * Can e.g. flush transactional O/R Mapping sessions to the database. diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java index 33854e60f00b..71c4745ca61e 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java @@ -81,8 +81,38 @@ public static Object unwrapResourceIfNecessary(Object resource) { * @see TransactionSynchronization#flush() */ public static void triggerFlush() { - for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) { - synchronization.flush(); + if (TransactionSynchronizationManager.isSynchronizationActive()) { + 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); + } } }