From f1c7dc4f4bcf0661b645134a55720f95b693ab4f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 18 Jun 2015 00:29:46 +0200 Subject: [PATCH] Introduced SimpleTransactionScope (analogous to SimpleThreadScope) Issue: SPR-13085 --- .../context/support/SimpleThreadScope.java | 13 +- .../support/SimpleTransactionScope.java | 111 +++++++++++++++ .../support/SimpleTransactionScopeTests.java | 127 ++++++++++++++++++ 3 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 spring-tx/src/main/java/org/springframework/transaction/support/SimpleTransactionScope.java create mode 100644 spring-tx/src/test/java/org/springframework/transaction/support/SimpleTransactionScopeTests.java diff --git a/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java b/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java index 34454f236c..14357c9f74 100644 --- a/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java +++ b/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -29,9 +29,14 @@ import org.springframework.core.NamedThreadLocal; /** * A simple thread-backed {@link Scope} implementation. * - *

Note: {@code SimpleThreadScope} does not clean up - * any objects associated with it. As such, it is typically preferable to - * use {@link org.springframework.web.context.request.RequestScope RequestScope} + *

NOTE: This thread scope is not registered by default in common contexts. + * Instead, you need to explicitly assign it to a scope key in your setup, either through + * {@link org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope} + * or through a {@link org.springframework.beans.factory.config.CustomScopeConfigurer} bean. + * + *

{@code SimpleThreadScope} does not clean up any objects associated with it. + * As such, it is typically preferable to use + * {@link org.springframework.web.context.request.RequestScope RequestScope} * in web environments. * *

For an implementation of a thread-based {@code Scope} with support for diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/SimpleTransactionScope.java b/spring-tx/src/main/java/org/springframework/transaction/support/SimpleTransactionScope.java new file mode 100644 index 0000000000..60ad8426a1 --- /dev/null +++ b/spring-tx/src/main/java/org/springframework/transaction/support/SimpleTransactionScope.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.transaction.support; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.Scope; + +/** + * A simple transaction-backed {@link Scope} implementation, delegating to + * {@link TransactionSynchronizationManager}'s resource binding mechanism. + * + *

NOTE: Like {@link org.springframework.context.support.SimpleThreadScope}, + * this transaction scope is not registered by default in common contexts. Instead, + * you need to explicitly assign it to a scope key in your setup, either through + * {@link org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope} + * or through a {@link org.springframework.beans.factory.config.CustomScopeConfigurer} bean. + * + * @author Juergen Hoeller + * @since 4.2 + * @see org.springframework.context.support.SimpleThreadScope + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope + * @see org.springframework.beans.factory.config.CustomScopeConfigurer + */ +public class SimpleTransactionScope implements Scope { + + @Override + public Object get(String name, ObjectFactory objectFactory) { + ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) TransactionSynchronizationManager.getResource(this); + if (scopedObjects == null) { + scopedObjects = new ScopedObjectsHolder(); + TransactionSynchronizationManager.registerSynchronization(new CleanupSynchronization()); + TransactionSynchronizationManager.bindResource(this, scopedObjects); + } + Object scopedObject = scopedObjects.scopedInstances.get(name); + if (scopedObject == null) { + scopedObject = objectFactory.getObject(); + scopedObjects.scopedInstances.put(name, scopedObject); + } + return scopedObject; + } + + @Override + public Object remove(String name) { + ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) TransactionSynchronizationManager.getResource(this); + if (scopedObjects != null) { + scopedObjects.destructionCallbacks.remove(name); + return scopedObjects.scopedInstances.remove(name); + } + else { + return null; + } + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) TransactionSynchronizationManager.getResource(this); + if (scopedObjects != null) { + scopedObjects.destructionCallbacks.put(name, callback); + } + } + + @Override + public Object resolveContextualObject(String key) { + return null; + } + + @Override + public String getConversationId() { + return TransactionSynchronizationManager.getCurrentTransactionName(); + } + + + static class ScopedObjectsHolder { + + final Map scopedInstances = new HashMap(); + + final Map destructionCallbacks = new LinkedHashMap(); + } + + + private class CleanupSynchronization extends TransactionSynchronizationAdapter { + + @Override + public void afterCompletion(int status) { + ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) + TransactionSynchronizationManager.unbindResourceIfPossible(SimpleTransactionScope.this); + for (Runnable callback : scopedObjects.destructionCallbacks.values()) { + callback.run(); + } + } + } + +} diff --git a/spring-tx/src/test/java/org/springframework/transaction/support/SimpleTransactionScopeTests.java b/spring-tx/src/test/java/org/springframework/transaction/support/SimpleTransactionScopeTests.java new file mode 100644 index 0000000000..a014f97f2b --- /dev/null +++ b/spring-tx/src/test/java/org/springframework/transaction/support/SimpleTransactionScopeTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.transaction.support; + +import org.junit.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.tests.sample.beans.DerivedTestBean; +import org.springframework.tests.sample.beans.TestBean; + +import static org.junit.Assert.*; + +/** + * @author Juergen Hoeller + */ +public class SimpleTransactionScopeTests { + + @Test + public void getFromScope() throws Exception { + GenericApplicationContext context = new GenericApplicationContext(); + context.getBeanFactory().registerScope("tx", new SimpleTransactionScope()); + + GenericBeanDefinition bd1 = new GenericBeanDefinition(); + bd1.setBeanClass(TestBean.class); + bd1.setScope("tx"); + bd1.setPrimary(true); + context.registerBeanDefinition("txScopedObject1", bd1); + + GenericBeanDefinition bd2 = new GenericBeanDefinition(); + bd2.setBeanClass(DerivedTestBean.class); + bd2.setScope("tx"); + context.registerBeanDefinition("txScopedObject2", bd2); + + context.refresh(); + + try { + context.getBean(TestBean.class); + fail("Should have thrown BeanCreationException"); + } + catch (BeanCreationException ex) { + // expected - no synchronization active + assertTrue(ex.getCause() instanceof IllegalStateException); + } + + try { + context.getBean(DerivedTestBean.class); + fail("Should have thrown BeanCreationException"); + } + catch (BeanCreationException ex) { + // expected - no synchronization active + assertTrue(ex.getCause() instanceof IllegalStateException); + } + + TestBean bean1 = null; + DerivedTestBean bean2 = null; + DerivedTestBean bean2a = null; + DerivedTestBean bean2b = null; + + TransactionSynchronizationManager.initSynchronization(); + try { + bean1 = context.getBean(TestBean.class); + assertSame(bean1, context.getBean(TestBean.class)); + + bean2 = context.getBean(DerivedTestBean.class); + assertSame(bean2, context.getBean(DerivedTestBean.class)); + context.getBeanFactory().destroyScopedBean("txScopedObject2"); + assertFalse(TransactionSynchronizationManager.hasResource("txScopedObject2")); + assertTrue(bean2.wasDestroyed()); + + bean2a = context.getBean(DerivedTestBean.class); + assertSame(bean2a, context.getBean(DerivedTestBean.class)); + assertNotSame(bean2, bean2a); + context.getBeanFactory().getRegisteredScope("tx").remove("txScopedObject2"); + assertFalse(TransactionSynchronizationManager.hasResource("txScopedObject2")); + assertFalse(bean2a.wasDestroyed()); + + bean2b = context.getBean(DerivedTestBean.class); + assertSame(bean2b, context.getBean(DerivedTestBean.class)); + assertNotSame(bean2, bean2a); + assertNotSame(bean2, bean2b); + assertNotSame(bean2a, bean2b); + } + finally { + TransactionSynchronizationUtils.triggerAfterCompletion(TransactionSynchronization.STATUS_COMMITTED); + TransactionSynchronizationManager.clearSynchronization(); + } + + assertFalse(bean2a.wasDestroyed()); + assertTrue(bean2b.wasDestroyed()); + assertTrue(TransactionSynchronizationManager.getResourceMap().isEmpty()); + + try { + context.getBean(TestBean.class); + fail("Should have thrown IllegalStateException"); + } + catch (BeanCreationException ex) { + // expected - no synchronization active + assertTrue(ex.getCause() instanceof IllegalStateException); + } + + try { + context.getBean(DerivedTestBean.class); + fail("Should have thrown IllegalStateException"); + } + catch (BeanCreationException ex) { + // expected - no synchronization active + assertTrue(ex.getCause() instanceof IllegalStateException); + } + } + +}