Skip to content

Commit a3e9f0c

Browse files
committed
Add bindSynchronizedResource with automatic unbinding after transaction completion
Closes gh-35182
1 parent 0a705f4 commit a3e9f0c

File tree

2 files changed

+120
-16
lines changed

2 files changed

+120
-16
lines changed

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

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,73 @@ public static boolean hasResource(Object key) {
158158

159159
/**
160160
* Bind the given resource for the given key to the current thread.
161+
* <p><b>Note: Any bound resource needs to get explicitly unbound through
162+
* {@link #unbindResource}. For automatic unbinding after transaction
163+
* completion, use {@link #bindSynchronizedResource} instead.</b>
161164
* @param key the key to bind the value to (usually the resource factory)
162165
* @param value the value to bind (usually the active resource object)
163166
* @throws IllegalStateException if there is already a value bound to the thread
164167
* @see ResourceTransactionManager#getResourceFactory()
168+
* @see #bindSynchronizedResource
165169
*/
166170
public static void bindResource(Object key, Object value) throws IllegalStateException {
167171
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
172+
Object oldValue = doBindResource(actualKey, value);
173+
if (oldValue != null) {
174+
throw new IllegalStateException(
175+
"Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread");
176+
}
177+
}
178+
179+
/**
180+
* Bind the given resource for the given key to the current thread,
181+
* synchronizing it with the current transaction for automatic unbinding
182+
* after transaction completion.
183+
* <p>This is effectively a programmatic way to register a transaction-scoped
184+
* resource, similar to the BeanFactory-driven {@link SimpleTransactionScope}.
185+
* <p>An existing value bound for the given key will be preserved and re-bound
186+
* after transaction completion, restoring the state before this bind call.
187+
* @param key the key to bind the value to (usually the resource factory)
188+
* @param value the value to bind (usually the active resource object)
189+
* @throws IllegalStateException if transaction synchronization is not active
190+
* @since 7.0
191+
* @see #bindResource
192+
* @see #registerSynchronization
193+
*/
194+
public static void bindSynchronizedResource(Object key, Object value) throws IllegalStateException {
195+
Set<TransactionSynchronization> synchs = synchronizations.get();
196+
if (synchs == null) {
197+
throw new IllegalStateException("Transaction synchronization is not active");
198+
}
199+
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
200+
Object oldValue = doBindResource(actualKey, value);
201+
synchs.add(new TransactionSynchronization() {
202+
@Override
203+
public void suspend() {
204+
doUnbindResource(actualKey);
205+
}
206+
@Override
207+
public void resume() {
208+
Object existingValue = doBindResource(actualKey, value);
209+
if (existingValue != null) {
210+
throw new IllegalStateException(
211+
"Unexpected value [" + existingValue + "] for key [" + actualKey + "] bound on resume");
212+
}
213+
}
214+
@Override
215+
public void afterCompletion(int status) {
216+
doUnbindResource(actualKey);
217+
if (oldValue != null) {
218+
doBindResource(actualKey, oldValue);
219+
}
220+
}
221+
});
222+
}
223+
224+
/**
225+
* Actually bind the given resource for the given key to the current thread.
226+
*/
227+
private static @Nullable Object doBindResource(Object actualKey, Object value) {
168228
Assert.notNull(value, "Value must not be null");
169229
Map<Object, Object> map = resources.get();
170230
// set ThreadLocal Map if none found
@@ -177,18 +237,19 @@ public static void bindResource(Object key, Object value) throws IllegalStateExc
177237
if (oldValue instanceof ResourceHolder resourceHolder && resourceHolder.isVoid()) {
178238
oldValue = null;
179239
}
180-
if (oldValue != null) {
181-
throw new IllegalStateException(
182-
"Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread");
183-
}
240+
return oldValue;
184241
}
185242

186243
/**
187244
* Unbind a resource for the given key from the current thread.
245+
* <p>This explicit step is only necessary with {@link #bindResource}.
246+
* For automatic unbinding, consider {@link #bindSynchronizedResource}.
188247
* @param key the key to unbind (usually the resource factory)
189248
* @return the previously bound value (usually the active resource object)
190249
* @throws IllegalStateException if there is no value bound to the thread
191250
* @see ResourceTransactionManager#getResourceFactory()
251+
* @see #bindResource
252+
* @see #unbindResourceIfPossible
192253
*/
193254
public static Object unbindResource(Object key) throws IllegalStateException {
194255
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
@@ -201,8 +262,12 @@ public static Object unbindResource(Object key) throws IllegalStateException {
201262

202263
/**
203264
* Unbind a resource for the given key from the current thread.
265+
* <p>This explicit step is only necessary with {@link #bindResource}.
266+
* For automatic unbinding, consider {@link #bindSynchronizedResource}.
204267
* @param key the key to unbind (usually the resource factory)
205268
* @return the previously bound value, or {@code null} if none bound
269+
* @see #bindResource
270+
* @see #unbindResource
206271
*/
207272
public static @Nullable Object unbindResourceIfPossible(Object key) {
208273
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);

spring-tx/src/test/java/org/springframework/transaction/support/SimpleTransactionScopeTests.java

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import static org.assertj.core.api.Assertions.assertThat;
3232
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
33+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
3334

3435
/**
3536
* @author Juergen Hoeller
@@ -54,13 +55,11 @@ void getFromScope() {
5455

5556
context.refresh();
5657

57-
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
58-
context.getBean(TestBean.class))
59-
.withCauseInstanceOf(IllegalStateException.class);
58+
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> context.getBean(TestBean.class))
59+
.withCauseInstanceOf(IllegalStateException.class);
6060

61-
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
62-
context.getBean(DerivedTestBean.class))
63-
.withCauseInstanceOf(IllegalStateException.class);
61+
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> context.getBean(DerivedTestBean.class))
62+
.withCauseInstanceOf(IllegalStateException.class);
6463

6564
TestBean bean1;
6665
DerivedTestBean bean2;
@@ -99,13 +98,11 @@ void getFromScope() {
9998
assertThat(bean2b.wasDestroyed()).isTrue();
10099
assertThat(TransactionSynchronizationManager.getResourceMap()).isEmpty();
101100

102-
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
103-
context.getBean(TestBean.class))
104-
.withCauseInstanceOf(IllegalStateException.class);
101+
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> context.getBean(TestBean.class))
102+
.withCauseInstanceOf(IllegalStateException.class);
105103

106-
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
107-
context.getBean(DerivedTestBean.class))
108-
.withCauseInstanceOf(IllegalStateException.class);
104+
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> context.getBean(DerivedTestBean.class))
105+
.withCauseInstanceOf(IllegalStateException.class);
109106
}
110107

111108
@Test
@@ -175,4 +172,46 @@ void getWithTransactionManager() {
175172
}
176173
}
177174

175+
@Test
176+
void bindSynchronizedResource() {
177+
CallCountingTransactionManager tm = new CallCountingTransactionManager();
178+
TransactionTemplate tt = new TransactionTemplate(tm);
179+
180+
tt.execute(status -> {
181+
TestBean tb = new TestBean();
182+
TransactionSynchronizationManager.bindSynchronizedResource("tb", tb);
183+
assertThat(TransactionSynchronizationManager.hasResource("tb")).isTrue();
184+
assertThat(TransactionSynchronizationManager.getResource("tb")).isSameAs(tb);
185+
return null;
186+
});
187+
assertThat(TransactionSynchronizationManager.hasResource("tb")).isFalse();
188+
}
189+
190+
@Test
191+
void bindSynchronizedResourceWithOldValue() {
192+
CallCountingTransactionManager tm = new CallCountingTransactionManager();
193+
TransactionTemplate tt = new TransactionTemplate(tm);
194+
195+
TestBean oldValue = new TestBean();
196+
TransactionSynchronizationManager.bindResource("tb", oldValue);
197+
198+
tt.execute(status -> {
199+
TestBean tb = new TestBean();
200+
TransactionSynchronizationManager.bindSynchronizedResource("tb", tb);
201+
assertThat(TransactionSynchronizationManager.hasResource("tb")).isTrue();
202+
assertThat(TransactionSynchronizationManager.getResource("tb")).isSameAs(tb);
203+
return null;
204+
});
205+
assertThat(TransactionSynchronizationManager.hasResource("tb")).isTrue();
206+
assertThat(TransactionSynchronizationManager.getResource("tb")).isSameAs(oldValue);
207+
TransactionSynchronizationManager.unbindResource("tb");
208+
}
209+
210+
@Test
211+
void bindSynchronizedResourceWithoutTransaction() {
212+
assertThatIllegalStateException().isThrownBy(
213+
() -> TransactionSynchronizationManager.bindSynchronizedResource("tb", new TestBean()));
214+
assertThat(TransactionSynchronizationManager.hasResource("tb")).isFalse();
215+
}
216+
178217
}

0 commit comments

Comments
 (0)