Add bindSynchronizedResource with automatic unbinding after transaction completion
Closes gh-35182
This commit is contained in:
parent
0a705f467d
commit
a3e9f0c16d
|
@ -158,13 +158,73 @@ public abstract class TransactionSynchronizationManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind the given resource for the given key to the current thread.
|
* Bind the given resource for the given key to the current thread.
|
||||||
|
* <p><b>Note: Any bound resource needs to get explicitly unbound through
|
||||||
|
* {@link #unbindResource}. For automatic unbinding after transaction
|
||||||
|
* completion, use {@link #bindSynchronizedResource} instead.</b>
|
||||||
* @param key the key to bind the value to (usually the resource factory)
|
* @param key the key to bind the value to (usually the resource factory)
|
||||||
* @param value the value to bind (usually the active resource object)
|
* @param value the value to bind (usually the active resource object)
|
||||||
* @throws IllegalStateException if there is already a value bound to the thread
|
* @throws IllegalStateException if there is already a value bound to the thread
|
||||||
* @see ResourceTransactionManager#getResourceFactory()
|
* @see ResourceTransactionManager#getResourceFactory()
|
||||||
|
* @see #bindSynchronizedResource
|
||||||
*/
|
*/
|
||||||
public static void bindResource(Object key, Object value) throws IllegalStateException {
|
public static void bindResource(Object key, Object value) throws IllegalStateException {
|
||||||
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
|
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
|
||||||
|
Object oldValue = doBindResource(actualKey, value);
|
||||||
|
if (oldValue != null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind the given resource for the given key to the current thread,
|
||||||
|
* synchronizing it with the current transaction for automatic unbinding
|
||||||
|
* after transaction completion.
|
||||||
|
* <p>This is effectively a programmatic way to register a transaction-scoped
|
||||||
|
* resource, similar to the BeanFactory-driven {@link SimpleTransactionScope}.
|
||||||
|
* <p>An existing value bound for the given key will be preserved and re-bound
|
||||||
|
* after transaction completion, restoring the state before this bind call.
|
||||||
|
* @param key the key to bind the value to (usually the resource factory)
|
||||||
|
* @param value the value to bind (usually the active resource object)
|
||||||
|
* @throws IllegalStateException if transaction synchronization is not active
|
||||||
|
* @since 7.0
|
||||||
|
* @see #bindResource
|
||||||
|
* @see #registerSynchronization
|
||||||
|
*/
|
||||||
|
public static void bindSynchronizedResource(Object key, Object value) throws IllegalStateException {
|
||||||
|
Set<TransactionSynchronization> synchs = synchronizations.get();
|
||||||
|
if (synchs == null) {
|
||||||
|
throw new IllegalStateException("Transaction synchronization is not active");
|
||||||
|
}
|
||||||
|
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
|
||||||
|
Object oldValue = doBindResource(actualKey, value);
|
||||||
|
synchs.add(new TransactionSynchronization() {
|
||||||
|
@Override
|
||||||
|
public void suspend() {
|
||||||
|
doUnbindResource(actualKey);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void resume() {
|
||||||
|
Object existingValue = doBindResource(actualKey, value);
|
||||||
|
if (existingValue != null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Unexpected value [" + existingValue + "] for key [" + actualKey + "] bound on resume");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(int status) {
|
||||||
|
doUnbindResource(actualKey);
|
||||||
|
if (oldValue != null) {
|
||||||
|
doBindResource(actualKey, oldValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually bind the given resource for the given key to the current thread.
|
||||||
|
*/
|
||||||
|
private static @Nullable Object doBindResource(Object actualKey, Object value) {
|
||||||
Assert.notNull(value, "Value must not be null");
|
Assert.notNull(value, "Value must not be null");
|
||||||
Map<Object, Object> map = resources.get();
|
Map<Object, Object> map = resources.get();
|
||||||
// set ThreadLocal Map if none found
|
// set ThreadLocal Map if none found
|
||||||
|
@ -177,18 +237,19 @@ public abstract class TransactionSynchronizationManager {
|
||||||
if (oldValue instanceof ResourceHolder resourceHolder && resourceHolder.isVoid()) {
|
if (oldValue instanceof ResourceHolder resourceHolder && resourceHolder.isVoid()) {
|
||||||
oldValue = null;
|
oldValue = null;
|
||||||
}
|
}
|
||||||
if (oldValue != null) {
|
return oldValue;
|
||||||
throw new IllegalStateException(
|
|
||||||
"Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unbind a resource for the given key from the current thread.
|
* Unbind a resource for the given key from the current thread.
|
||||||
|
* <p>This explicit step is only necessary with {@link #bindResource}.
|
||||||
|
* For automatic unbinding, consider {@link #bindSynchronizedResource}.
|
||||||
* @param key the key to unbind (usually the resource factory)
|
* @param key the key to unbind (usually the resource factory)
|
||||||
* @return the previously bound value (usually the active resource object)
|
* @return the previously bound value (usually the active resource object)
|
||||||
* @throws IllegalStateException if there is no value bound to the thread
|
* @throws IllegalStateException if there is no value bound to the thread
|
||||||
* @see ResourceTransactionManager#getResourceFactory()
|
* @see ResourceTransactionManager#getResourceFactory()
|
||||||
|
* @see #bindResource
|
||||||
|
* @see #unbindResourceIfPossible
|
||||||
*/
|
*/
|
||||||
public static Object unbindResource(Object key) throws IllegalStateException {
|
public static Object unbindResource(Object key) throws IllegalStateException {
|
||||||
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
|
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
|
||||||
|
@ -201,8 +262,12 @@ public abstract class TransactionSynchronizationManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unbind a resource for the given key from the current thread.
|
* Unbind a resource for the given key from the current thread.
|
||||||
|
* <p>This explicit step is only necessary with {@link #bindResource}.
|
||||||
|
* For automatic unbinding, consider {@link #bindSynchronizedResource}.
|
||||||
* @param key the key to unbind (usually the resource factory)
|
* @param key the key to unbind (usually the resource factory)
|
||||||
* @return the previously bound value, or {@code null} if none bound
|
* @return the previously bound value, or {@code null} if none bound
|
||||||
|
* @see #bindResource
|
||||||
|
* @see #unbindResource
|
||||||
*/
|
*/
|
||||||
public static @Nullable Object unbindResourceIfPossible(Object key) {
|
public static @Nullable Object unbindResourceIfPossible(Object key) {
|
||||||
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
|
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.springframework.transaction.testfixture.CallCountingTransactionManage
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
|
@ -54,13 +55,11 @@ class SimpleTransactionScopeTests {
|
||||||
|
|
||||||
context.refresh();
|
context.refresh();
|
||||||
|
|
||||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
|
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> context.getBean(TestBean.class))
|
||||||
context.getBean(TestBean.class))
|
.withCauseInstanceOf(IllegalStateException.class);
|
||||||
.withCauseInstanceOf(IllegalStateException.class);
|
|
||||||
|
|
||||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
|
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> context.getBean(DerivedTestBean.class))
|
||||||
context.getBean(DerivedTestBean.class))
|
.withCauseInstanceOf(IllegalStateException.class);
|
||||||
.withCauseInstanceOf(IllegalStateException.class);
|
|
||||||
|
|
||||||
TestBean bean1;
|
TestBean bean1;
|
||||||
DerivedTestBean bean2;
|
DerivedTestBean bean2;
|
||||||
|
@ -99,13 +98,11 @@ class SimpleTransactionScopeTests {
|
||||||
assertThat(bean2b.wasDestroyed()).isTrue();
|
assertThat(bean2b.wasDestroyed()).isTrue();
|
||||||
assertThat(TransactionSynchronizationManager.getResourceMap()).isEmpty();
|
assertThat(TransactionSynchronizationManager.getResourceMap()).isEmpty();
|
||||||
|
|
||||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
|
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> context.getBean(TestBean.class))
|
||||||
context.getBean(TestBean.class))
|
.withCauseInstanceOf(IllegalStateException.class);
|
||||||
.withCauseInstanceOf(IllegalStateException.class);
|
|
||||||
|
|
||||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
|
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> context.getBean(DerivedTestBean.class))
|
||||||
context.getBean(DerivedTestBean.class))
|
.withCauseInstanceOf(IllegalStateException.class);
|
||||||
.withCauseInstanceOf(IllegalStateException.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -175,4 +172,46 @@ class SimpleTransactionScopeTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bindSynchronizedResource() {
|
||||||
|
CallCountingTransactionManager tm = new CallCountingTransactionManager();
|
||||||
|
TransactionTemplate tt = new TransactionTemplate(tm);
|
||||||
|
|
||||||
|
tt.execute(status -> {
|
||||||
|
TestBean tb = new TestBean();
|
||||||
|
TransactionSynchronizationManager.bindSynchronizedResource("tb", tb);
|
||||||
|
assertThat(TransactionSynchronizationManager.hasResource("tb")).isTrue();
|
||||||
|
assertThat(TransactionSynchronizationManager.getResource("tb")).isSameAs(tb);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
assertThat(TransactionSynchronizationManager.hasResource("tb")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bindSynchronizedResourceWithOldValue() {
|
||||||
|
CallCountingTransactionManager tm = new CallCountingTransactionManager();
|
||||||
|
TransactionTemplate tt = new TransactionTemplate(tm);
|
||||||
|
|
||||||
|
TestBean oldValue = new TestBean();
|
||||||
|
TransactionSynchronizationManager.bindResource("tb", oldValue);
|
||||||
|
|
||||||
|
tt.execute(status -> {
|
||||||
|
TestBean tb = new TestBean();
|
||||||
|
TransactionSynchronizationManager.bindSynchronizedResource("tb", tb);
|
||||||
|
assertThat(TransactionSynchronizationManager.hasResource("tb")).isTrue();
|
||||||
|
assertThat(TransactionSynchronizationManager.getResource("tb")).isSameAs(tb);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
assertThat(TransactionSynchronizationManager.hasResource("tb")).isTrue();
|
||||||
|
assertThat(TransactionSynchronizationManager.getResource("tb")).isSameAs(oldValue);
|
||||||
|
TransactionSynchronizationManager.unbindResource("tb");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bindSynchronizedResourceWithoutTransaction() {
|
||||||
|
assertThatIllegalStateException().isThrownBy(
|
||||||
|
() -> TransactionSynchronizationManager.bindSynchronizedResource("tb", new TestBean()));
|
||||||
|
assertThat(TransactionSynchronizationManager.hasResource("tb")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue