From 4741a12fdc43782cc14692f58f3213edc18add7f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 2 Feb 2015 10:46:28 +0100 Subject: [PATCH] Support for transactional event listener Update the application event listener infrastructure to support events that are processed according to a transactional phase. Introduce EventListenerFactory that can be implemented to provide support for additional event listener types. TransactionalEventListener is a new annotation that can be used in lieu of the regular EventListener. Its related factory implementation is registered in the context automatically via @EnableTransactionManagement or By default, a TransactionalEventListener is invoked when the transaction has completed successfully (i.e. AFTER_COMMIT). Additional phases are provided to handle BEFORE_COMMIT and AFTER_ROLLBACK events. If no transaction is running, such listener is not invoked at all unless the `fallbackExecution` flag has been explicitly set. Issue: SPR-12080 --- .../annotation/AnnotationConfigUtils.java | 12 + .../ApplicationListenerMethodAdapter.java | 52 +- .../event/DefaultEventListenerFactory.java | 54 ++ .../context/event/EventListenerFactory.java | 48 ++ .../event/EventListenerMethodProcessor.java | 46 +- .../ClassPathBeanDefinitionScannerTests.java | 31 +- ...actTransactionManagementConfiguration.java | 12 + .../AnnotationDrivenBeanDefinitionParser.java | 12 +- .../TransactionManagementConfigUtils.java | 9 +- ...ionListenerMethodTransactionalAdapter.java | 138 +++++ .../transaction/event/TransactionPhase.java | 59 +++ .../event/TransactionalEventListener.java | 65 +++ .../TransactionalEventListenerFactory.java | 56 ++ ...ationTransactionNamespaceHandlerTests.java | 10 +- .../EnableTransactionManagementTests.java | 14 +- ...stenerMethodTransactionalAdapterTests.java | 81 +++ .../TransactionalEventListenerTests.java | 496 ++++++++++++++++++ 17 files changed, 1161 insertions(+), 34 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/context/event/DefaultEventListenerFactory.java create mode 100644 spring-context/src/main/java/org/springframework/context/event/EventListenerFactory.java create mode 100644 spring-tx/src/main/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapter.java create mode 100644 spring-tx/src/main/java/org/springframework/transaction/event/TransactionPhase.java create mode 100644 spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java create mode 100644 spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListenerFactory.java create mode 100644 spring-tx/src/test/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapterTests.java create mode 100644 spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index a889a920a74..764073b8737 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -30,6 +30,7 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.event.DefaultEventListenerFactory; import org.springframework.context.event.EventListenerMethodProcessor; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.annotation.AnnotationAttributes; @@ -111,6 +112,12 @@ public class AnnotationConfigUtils { public static final String EVENT_LISTENER_PROCESSOR_BEAN_NAME = "org.springframework.context.event.internalEventListenerProcessor"; + /** + * The bean name of the internally managed EventListenerFactory. + */ + public static final String EVENT_LISTENER_FACTORY_BEAN_NAME = + "org.springframework.context.event.internalEventListenerFactory"; + private static final boolean jsr250Present = ClassUtils.isPresent("javax.annotation.Resource", AnnotationConfigUtils.class.getClassLoader()); @@ -195,6 +202,11 @@ public class AnnotationConfigUtils { def.setSource(source); beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME)); } + if (!registry.containsBeanDefinition(EVENT_LISTENER_FACTORY_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(DefaultEventListenerFactory.class); + def.setSource(source); + beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_FACTORY_BEAN_NAME)); + } return beanDefs; } diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index cb40c2d6085..864cbf60149 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -16,6 +16,7 @@ package org.springframework.context.event; +import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @@ -30,6 +31,8 @@ import org.springframework.context.PayloadApplicationEvent; import org.springframework.context.expression.AnnotatedElementKey; import org.springframework.core.BridgeMethodResolver; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; import org.springframework.expression.EvaluationContext; @@ -41,11 +44,11 @@ import org.springframework.util.StringUtils; * {@link GenericApplicationListener} adapter that delegates the processing of * an event to an {@link EventListener} annotated method. * - *

Unwrap the content of a {@link PayloadApplicationEvent} if necessary - * to allow method declaration to define any arbitrary event type. - * - *

If a condition is defined, it is evaluated prior to invoking the - * underlying method. + *

Delegates to {@link #processEvent(ApplicationEvent)} to give a chance to + * sub-classes to deviate from the default. Unwrap the content of a + * {@link PayloadApplicationEvent} if necessary to allow method declaration + * to define any arbitrary event type. If a condition is defined, it is + * evaluated prior to invoking the underlying method. * * @author Stephane Nicoll * @since 4.2 @@ -70,6 +73,8 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe private EventExpressionEvaluator evaluator; + private String condition; + public ApplicationListenerMethodAdapter(String beanName, Class targetClass, Method method) { this.beanName = beanName; this.method = method; @@ -89,6 +94,14 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe @Override public void onApplicationEvent(ApplicationEvent event) { + processEvent(event); + } + + /** + * Process the specified {@link ApplicationEvent}, checking if the condition + * match and handling non-null result, if any. + */ + public void processEvent(ApplicationEvent event) { Object[] args = resolveArguments(event); if (shouldHandle(event, args)) { Object result = doInvoke(args); @@ -131,8 +144,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe if (args == null) { return false; } - EventListener eventListener = AnnotationUtils.findAnnotation(this.method, EventListener.class); - String condition = (eventListener != null ? eventListener.condition() : null); + String condition = getCondition(); if (StringUtils.hasText(condition)) { Assert.notNull(this.evaluator, "Evaluator must no be null."); EvaluationContext evaluationContext = this.evaluator.createEvaluationContext(event, @@ -161,10 +173,14 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe @Override public int getOrder() { - Order order = AnnotationUtils.findAnnotation(this.method, Order.class); + Order order = getMethodAnnotation(Order.class); return (order != null ? order.value() : 0); } + protected A getMethodAnnotation(Class annotationType) { + return AnnotationUtils.findAnnotation(this.method, annotationType); + } + /** * Invoke the event listener method with the given argument values. */ @@ -202,6 +218,26 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe return this.applicationContext.getBean(this.beanName); } + /** + * Return the condition to use. Matches the {@code condition} attribute of the + * {@link EventListener} annotation or any matching attribute on a meta-annotation. + */ + protected String getCondition() { + if (this.condition == null) { + AnnotationAttributes annotationAttributes = AnnotatedElementUtils + .getAnnotationAttributes(this.method, EventListener.class.getName()); + if (annotationAttributes != null) { + String value = annotationAttributes.getString("condition"); + this.condition = (value != null ? value : ""); + } + else { // TODO annotationAttributes null with proxy + EventListener eventListener = getMethodAnnotation(EventListener.class); + this.condition = (eventListener != null ? eventListener.condition() : null); + } + } + return this.condition; + } + /** * Add additional details such as the bean type and method signature to * the given error message. diff --git a/spring-context/src/main/java/org/springframework/context/event/DefaultEventListenerFactory.java b/spring-context/src/main/java/org/springframework/context/event/DefaultEventListenerFactory.java new file mode 100644 index 00000000000..8d6cd686445 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/DefaultEventListenerFactory.java @@ -0,0 +1,54 @@ +/* + * 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.context.event; + +import java.lang.reflect.Method; + +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; + +/** + * Default {@link EventListenerFactory} implementation that supports the + * regular {@link EventListener} annotation. + *

Used as "catch-all" implementation by default. + * + * @author Stephane Nicoll + * @since 4.2 + */ +public class DefaultEventListenerFactory implements EventListenerFactory, Ordered { + + private int order = LOWEST_PRECEDENCE; + + @Override + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + + public boolean supportsMethod(Method method) { + return true; + } + + @Override + public ApplicationListener createApplicationListener(String beanName, Class type, Method method) { + return new ApplicationListenerMethodAdapter(beanName, type, method); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListenerFactory.java b/spring-context/src/main/java/org/springframework/context/event/EventListenerFactory.java new file mode 100644 index 00000000000..188731747dd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/EventListenerFactory.java @@ -0,0 +1,48 @@ +/* + * 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.context.event; + +import java.lang.reflect.Method; + +import org.springframework.context.ApplicationListener; + +/** + * Strategy interface for creating {@link ApplicationListener} for methods + * annotated with {@link EventListener}. + * + * @author Stephane Nicoll + * @since 4.2 + */ +public interface EventListenerFactory { + + /** + * Specify if this factory supports the specified {@link Method}. + * @param method an {@link EventListener} annotated method + * @return {@code true} if this factory supports the specified method + */ + boolean supportsMethod(Method method); + + /** + * Create an {@link ApplicationListener} for the specified method. + * @param beanName the name of the bean + * @param type the target type of the instance + * @param method the {@link EventListener} annotated method + * @return an application listener, suitable to invoke the specified method + */ + ApplicationListener createApplicationListener(String beanName, Class type, Method method); + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java index 8d2bb3e2127..7ecd0985b80 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java @@ -17,8 +17,11 @@ package org.springframework.context.event; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -35,6 +38,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -65,14 +69,27 @@ public class EventListenerMethodProcessor implements SmartInitializingSingleton, } + /** + * Return the {@link EventListenerFactory} instances to use to handle {@link EventListener} + * annotated methods. + */ + protected List getEventListenerFactories() { + Map beans = + this.applicationContext.getBeansOfType(EventListenerFactory.class); + List allFactories = new ArrayList(beans.values()); + AnnotationAwareOrderComparator.sort(allFactories); + return allFactories; + } + @Override public void afterSingletonsInstantiated() { + List factories = getEventListenerFactories(); String[] allBeanNames = this.applicationContext.getBeanNamesForType(Object.class); for (String beanName : allBeanNames) { if (!ScopedProxyUtils.isScopedTarget(beanName)) { Class type = this.applicationContext.getType(beanName); try { - processBean(beanName, type); + processBean(factories, beanName, type); } catch (RuntimeException e) { throw new BeanInitializationException("Failed to process @EventListener " + @@ -82,22 +99,31 @@ public class EventListenerMethodProcessor implements SmartInitializingSingleton, } } - protected void processBean(String beanName, final Class type) { + protected void processBean(List factories, String beanName, final Class type) { Class targetType = getTargetClass(beanName, type); if (!this.nonAnnotatedClasses.contains(targetType)) { final Set annotatedMethods = new LinkedHashSet(1); Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(targetType); for (Method method : methods) { EventListener eventListener = AnnotationUtils.findAnnotation(method, EventListener.class); - if (eventListener != null) { - if (!type.equals(targetType)) { - method = getProxyMethod(type, method); + if (eventListener == null) { + continue; + } + for (EventListenerFactory factory : factories) { + if (factory.supportsMethod(method)) { + if (!type.equals(targetType)) { + method = getProxyMethod(type, method); + } + ApplicationListener applicationListener = + factory.createApplicationListener(beanName, type, method); + if (applicationListener instanceof ApplicationListenerMethodAdapter) { + ((ApplicationListenerMethodAdapter)applicationListener) + .init(this.applicationContext, this.evaluator); + } + this.applicationContext.addApplicationListener(applicationListener); + annotatedMethods.add(method); + break; } - ApplicationListenerMethodAdapter applicationListener = - new ApplicationListenerMethodAdapter(beanName, type, method); - applicationListener.init(this.applicationContext, this.evaluator); - this.applicationContext.addApplicationListener(applicationListener); - annotatedMethods.add(method); } } if (annotatedMethods.isEmpty()) { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java index a125a27e66d..62cb02d7f72 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java @@ -55,7 +55,7 @@ public class ClassPathBeanDefinitionScannerTests { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(11, beanCount); + assertEquals(12, beanCount); assertTrue(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("fooServiceImpl")); assertTrue(context.containsBean("stubFooDao")); @@ -67,6 +67,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)); context.refresh(); FooServiceImpl service = context.getBean("fooServiceImpl", FooServiceImpl.class); assertTrue(context.getDefaultListableBeanFactory().containsSingleton("myNamedComponent")); @@ -99,7 +100,7 @@ public class ClassPathBeanDefinitionScannerTests { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(11, beanCount); + assertEquals(12, beanCount); scanner.scan(BASE_PACKAGE); assertTrue(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("fooServiceImpl")); @@ -219,12 +220,13 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, false); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(6, beanCount); + assertEquals(7, beanCount); assertTrue(context.containsBean("messageBean")); assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)); } @Test @@ -233,7 +235,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, false); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(6, beanCount); + assertEquals(7, beanCount); assertTrue(context.containsBean("messageBean")); assertFalse(context.containsBean("serviceInvocationCounter")); assertFalse(context.containsBean("fooServiceImpl")); @@ -244,6 +246,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)); } @Test @@ -252,7 +255,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(12, beanCount); + assertEquals(13, beanCount); assertTrue(context.containsBean("messageBean")); assertTrue(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("fooServiceImpl")); @@ -263,6 +266,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)); } @Test @@ -271,7 +275,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addExcludeFilter(new AnnotationTypeFilter(Aspect.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(10, beanCount); + assertEquals(11, beanCount); assertFalse(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("fooServiceImpl")); assertTrue(context.containsBean("stubFooDao")); @@ -289,7 +293,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addExcludeFilter(new AssignableTypeFilter(FooService.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(10, beanCount); + assertEquals(11, beanCount); assertFalse(context.containsBean("fooServiceImpl")); assertTrue(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("stubFooDao")); @@ -299,6 +303,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)); } @Test @@ -326,7 +331,7 @@ public class ClassPathBeanDefinitionScannerTests { scanner.addExcludeFilter(new AssignableTypeFilter(FooService.class)); scanner.addExcludeFilter(new AnnotationTypeFilter(Aspect.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(9, beanCount); + assertEquals(10, beanCount); assertFalse(context.containsBean("fooServiceImpl")); assertFalse(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("stubFooDao")); @@ -336,6 +341,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)); } @Test @@ -344,7 +350,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setBeanNameGenerator(new TestBeanNameGenerator()); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(11, beanCount); + assertEquals(12, beanCount); assertFalse(context.containsBean("fooServiceImpl")); assertTrue(context.containsBean("fooService")); assertTrue(context.containsBean("serviceInvocationCounter")); @@ -355,6 +361,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)); } @Test @@ -364,7 +371,7 @@ public class ClassPathBeanDefinitionScannerTests { GenericApplicationContext multiPackageContext = new GenericApplicationContext(); ClassPathBeanDefinitionScanner multiPackageScanner = new ClassPathBeanDefinitionScanner(multiPackageContext); int singlePackageBeanCount = singlePackageScanner.scan(BASE_PACKAGE); - assertEquals(11, singlePackageBeanCount); + assertEquals(12, singlePackageBeanCount); multiPackageScanner.scan(BASE_PACKAGE, "org.springframework.dao.annotation"); // assertTrue(multiPackageBeanCount > singlePackageBeanCount); } @@ -375,7 +382,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int initialBeanCount = context.getBeanDefinitionCount(); int scannedBeanCount = scanner.scan(BASE_PACKAGE); - assertEquals(11, scannedBeanCount); + assertEquals(12, scannedBeanCount); assertEquals(scannedBeanCount, context.getBeanDefinitionCount() - initialBeanCount); int addedBeanCount = scanner.scan("org.springframework.aop.aspectj.annotation"); assertEquals(initialBeanCount + scannedBeanCount + addedBeanCount, context.getBeanDefinitionCount()); @@ -388,7 +395,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setBeanNameGenerator(new TestBeanNameGenerator()); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(11, beanCount); + assertEquals(12, beanCount); context.refresh(); FooServiceImpl fooService = context.getBean("fooService", FooServiceImpl.class); diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java index 91272c6f5b2..35c6d96fffe 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java @@ -19,11 +19,16 @@ package org.springframework.transaction.annotation; import java.util.Collection; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportAware; +import org.springframework.context.annotation.Role; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.event.TransactionalEventListenerFactory; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -32,6 +37,7 @@ import org.springframework.util.CollectionUtils; * Spring's annotation-driven transaction management capability. * * @author Chris Beams + * @author Stephane Nicoll * @since 3.1 * @see EnableTransactionManagement */ @@ -46,6 +52,12 @@ public abstract class AbstractTransactionManagementConfiguration implements Impo protected PlatformTransactionManager txManager; + @Bean(name = TransactionManagementConfigUtils.TRANSACTIONAL_EVENT_LISTENER_FACTORY_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionalEventListenerFactory transactionalEventListenerFactory() { + return new TransactionalEventListenerFactory(); + } + @Override public void setImportMetadata(AnnotationMetadata importMetadata) { this.enableTx = AnnotationAttributes.fromMap( diff --git a/spring-tx/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.java b/spring-tx/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.java index 17def5845ff..ce3300aafac 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-tx/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.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. @@ -26,6 +26,7 @@ import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.transaction.event.TransactionalEventListenerFactory; import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; import org.springframework.transaction.interceptor.TransactionInterceptor; @@ -44,6 +45,7 @@ import org.springframework.transaction.interceptor.TransactionInterceptor; * @author Juergen Hoeller * @author Rob Harrop * @author Chris Beams + * @author Stephane Nicoll * @since 2.0 */ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { @@ -55,6 +57,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { */ @Override public BeanDefinition parse(Element element, ParserContext parserContext) { + registerTransactionalEventListenerFactory(parserContext); String mode = element.getAttribute("mode"); if ("aspectj".equals(mode)) { // mode="aspectj" @@ -84,6 +87,13 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { TxNamespaceHandler.getTransactionManagerName(element)); } + private void registerTransactionalEventListenerFactory(ParserContext parserContext) { + RootBeanDefinition def = new RootBeanDefinition(); + def.setBeanClass(TransactionalEventListenerFactory.class); + parserContext.registerBeanComponent(new BeanComponentDefinition(def, + TransactionManagementConfigUtils.TRANSACTIONAL_EVENT_LISTENER_FACTORY_BEAN_NAME)); + } + /** * Inner class to just introduce an AOP framework dependency when actually in proxy mode. diff --git a/spring-tx/src/main/java/org/springframework/transaction/config/TransactionManagementConfigUtils.java b/spring-tx/src/main/java/org/springframework/transaction/config/TransactionManagementConfigUtils.java index ff95d77aa2a..9b5f9fe8e71 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/config/TransactionManagementConfigUtils.java +++ b/spring-tx/src/main/java/org/springframework/transaction/config/TransactionManagementConfigUtils.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. @@ -20,6 +20,7 @@ package org.springframework.transaction.config; * Configuration constants for internal sharing across subpackages. * * @author Chris Beams + * @author Stephane Nicoll * @since 3.1 */ public abstract class TransactionManagementConfigUtils { @@ -48,4 +49,10 @@ public abstract class TransactionManagementConfigUtils { public static final String TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME = "org.springframework.transaction.aspectj.AspectJTransactionManagementConfiguration"; + /** + * The bean name of the internally managed TransactionalEventListenerFactory. + */ + public static final String TRANSACTIONAL_EVENT_LISTENER_FACTORY_BEAN_NAME = + "org.springframework.transaction.config.internalTransactionalEventListenerFactory"; + } diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapter.java new file mode 100644 index 00000000000..e23a379aa4c --- /dev/null +++ b/spring-tx/src/main/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapter.java @@ -0,0 +1,138 @@ +/* + * 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.event; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.ApplicationListenerMethodAdapter; +import org.springframework.context.event.EventListener; +import org.springframework.context.event.GenericApplicationListener; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * {@link GenericApplicationListener} adapter that delegates the processing of + * an event to a {@link TransactionalEventListener} annotated method. Supports + * the exact same features as any regular {@link EventListener} annotated method + * but is aware of the transactional context of the event publisher. + *

+ * Processing of {@link TransactionalEventListener} is enabled automatically when + * Spring's transaction management is enabled. For other cases, registering a + * bean of type {@link TransactionalEventListenerFactory} is required. + * + * @author Stephane Nicoll + * @since 4.2 + * @see ApplicationListenerMethodAdapter + * @see TransactionalEventListener + */ +class ApplicationListenerMethodTransactionalAdapter extends ApplicationListenerMethodAdapter { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final TransactionalEventListener annotation; + + public ApplicationListenerMethodTransactionalAdapter(String beanName, Class targetClass, Method method) { + super(beanName, targetClass, method); + this.annotation = findAnnotation(method); + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronization transactionSynchronization = createTransactionSynchronization(event); + TransactionSynchronizationManager.registerSynchronization(transactionSynchronization); + } + else if (annotation.fallbackExecution()) { + if (annotation.phase() == TransactionPhase.AFTER_ROLLBACK) { + logger.warn("Processing '" + event + "' as a fallback execution on AFTER_ROLLBACK phase."); + } + processEvent(event); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("No transaction is running, skipping '" + event + "' for '" + this + "'"); + } + } + } + + private TransactionSynchronization createTransactionSynchronization(ApplicationEvent event) { + return new TransactionSynchronizationEventAdapter(this, event, this.annotation.phase()); + } + + static TransactionalEventListener findAnnotation(Method method) { + TransactionalEventListener annotation = AnnotationUtils + .findAnnotation(method, TransactionalEventListener.class); + if (annotation == null) { + throw new IllegalStateException("No TransactionalEventListener annotation found ou '" + method + "'"); + } + return annotation; + } + + + private static class TransactionSynchronizationEventAdapter extends TransactionSynchronizationAdapter { + + private final ApplicationListenerMethodAdapter listener; + + private final ApplicationEvent event; + + private final TransactionPhase phase; + + protected TransactionSynchronizationEventAdapter(ApplicationListenerMethodAdapter listener, + ApplicationEvent event, TransactionPhase phase) { + + this.listener = listener; + this.event = event; + this.phase = phase; + } + + @Override + public void beforeCommit(boolean readOnly) { + if (phase == TransactionPhase.BEFORE_COMMIT) { + processEvent(); + } + } + + @Override + public void afterCompletion(int status) { + if (phase == TransactionPhase.AFTER_COMPLETION) { + processEvent(); + } + else if (phase == TransactionPhase.AFTER_COMMIT && status == STATUS_COMMITTED) { + processEvent(); + } + else if (phase == TransactionPhase.AFTER_ROLLBACK && status == STATUS_ROLLED_BACK) { + processEvent(); + } + } + + @Override + public int getOrder() { + return this.listener.getOrder(); + } + + protected void processEvent() { + this.listener.processEvent(this.event); + } + } + +} diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionPhase.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionPhase.java new file mode 100644 index 00000000000..1f928cee437 --- /dev/null +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionPhase.java @@ -0,0 +1,59 @@ +/* + * 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.event; + +import org.springframework.transaction.support.TransactionSynchronization; + +/** + * The phase at which a transactional event listener applies. + * + * @author Stephane Nicoll + * @since 4.2 + * @see TransactionalEventListener + */ +public enum TransactionPhase { + + /** + * Fire the event before transaction commit. + * @see TransactionSynchronization#beforeCommit(boolean) + */ + BEFORE_COMMIT, + + /** + * Fire the event after the transaction has completed. For + * more fine-grained event, use {@link #AFTER_COMMIT} or + * {@link #AFTER_ROLLBACK} to intercept transaction commit + * or rollback respectively. + * @see TransactionSynchronization#afterCompletion(int) + */ + AFTER_COMPLETION, + + /** + * Fire the event after the commit has completed successfully. + * @see TransactionSynchronization#afterCompletion(int) + * @see TransactionSynchronization#STATUS_COMMITTED + */ + AFTER_COMMIT, + + /** + * Fire the event if the transaction has rolled back. + * @see TransactionSynchronization#afterCompletion(int) + * @see TransactionSynchronization#STATUS_ROLLED_BACK + */ + AFTER_ROLLBACK + +} diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java new file mode 100644 index 00000000000..2cccb399002 --- /dev/null +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java @@ -0,0 +1,65 @@ +/* + * 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.event; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.event.EventListener; + +/** + * An {@link EventListener} that is invoked according to a {@link TransactionPhase}. + *

+ * If the event is not published in the boundaries of a managed transaction, the event + * is discarded unless the {@link #fallbackExecution()} flag is explicitly set. If a + * transaction is running, the event is processed according to its {@link TransactionPhase}. + *

+ * Adding {@link org.springframework.core.annotation.Order @Order} on your annotated method + * allows you to prioritize that listener amongst other listeners running on the same phase. + * + * @author Stephane Nicoll + * @since 4.2 + */ +@EventListener +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TransactionalEventListener { + + /** + * Phase to bind the handling of an event to. If no transaction is in progress, the + * event is not processed at all unless {@link #fallbackExecution()} has been + * enabled explicitly. + */ + TransactionPhase phase() default TransactionPhase.AFTER_COMMIT; + + /** + * Specify if the event should be processed if no transaction is running. + */ + boolean fallbackExecution() default false; + + /** + * Spring Expression Language (SpEL) attribute used for conditioning the event handling. + *

Default is "", meaning the event is always handled. + * @see EventListener#condition() + */ + String condition() default ""; + +} diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListenerFactory.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListenerFactory.java new file mode 100644 index 00000000000..3b3fdd6cec9 --- /dev/null +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListenerFactory.java @@ -0,0 +1,56 @@ +/* + * 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.event; + +import java.lang.reflect.Method; + +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.EventListenerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; + +/** + * {@link EventListenerFactory} implementation that handles {@link TransactionalEventListener} + * annotated method. + * + * @author Stephane Nicoll + * @since 4.2 + */ +public class TransactionalEventListenerFactory implements EventListenerFactory, Ordered { + + private int order = 50; + + @Override + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public boolean supportsMethod(Method method) { + return AnnotationUtils.findAnnotation(method, TransactionalEventListener.class) != null; + } + + @Override + public ApplicationListener createApplicationListener(String beanName, Class type, Method method) { + return new ApplicationListenerMethodTransactionalAdapter(beanName, type, method); + } + +} diff --git a/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.java b/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.java index 91abc1551e4..19406b65d38 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.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. @@ -31,6 +31,8 @@ import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.stereotype.Service; import org.springframework.tests.transaction.CallCountingTransactionManager; +import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.event.TransactionalEventListenerFactory; /** * @author Rob Harrop @@ -99,6 +101,12 @@ public class AnnotationTransactionNamespaceHandlerTests extends TestCase { server.invoke(ObjectName.getInstance("test:type=TestBean"), "doSomething", new Object[0], new String[0])); } + public void testTransactionalEventListenerRegisteredProperly() { + assertTrue(this.context.containsBean(TransactionManagementConfigUtils + .TRANSACTIONAL_EVENT_LISTENER_FACTORY_BEAN_NAME)); + assertEquals(1, this.context.getBeansOfType(TransactionalEventListenerFactory.class).size()); + } + private TransactionalTestBean getTestBean() { return (TransactionalTestBean) context.getBean("testBean"); } diff --git a/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java b/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java index 07893433f65..0469f487323 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.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. @@ -32,6 +32,8 @@ import org.springframework.stereotype.Service; import org.springframework.tests.transaction.CallCountingTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.AnnotationTransactionNamespaceHandlerTests.TransactionalTestBean; +import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.event.TransactionalEventListenerFactory; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -105,6 +107,16 @@ public class EnableTransactionManagementTests { } } + @Test + public void transactionalEventListenerRegisteredProperly() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(EnableTxConfig.class); + ctx.refresh(); + assertTrue(ctx.containsBean(TransactionManagementConfigUtils + .TRANSACTIONAL_EVENT_LISTENER_FACTORY_BEAN_NAME)); + assertEquals(1, ctx.getBeansOfType(TransactionalEventListenerFactory.class).size()); + } + @Test public void spr11915() { AnnotationConfigApplicationContext ctx = diff --git a/spring-tx/src/test/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapterTests.java b/spring-tx/src/test/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapterTests.java new file mode 100644 index 00000000000..5260ad6eac5 --- /dev/null +++ b/spring-tx/src/test/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapterTests.java @@ -0,0 +1,81 @@ +/* + * 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.event; + +import java.lang.reflect.Method; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.util.ReflectionUtils; + +import static org.junit.Assert.*; + +/** + * @author Stephane Nicoll + */ +public class ApplicationListenerMethodTransactionalAdapterTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void noAnnotation() { + Method m = ReflectionUtils.findMethod(PhaseConfigurationTestListener.class, + "noAnnotation", String.class); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("noAnnotation"); + ApplicationListenerMethodTransactionalAdapter.findAnnotation(m); + } + + @Test + public void defaultPhase() { + Method m = ReflectionUtils.findMethod(PhaseConfigurationTestListener.class, "defaultPhase", String.class); + assertPhase(m, TransactionPhase.AFTER_COMMIT); + } + + @Test + public void phaseSet() { + Method m = ReflectionUtils.findMethod(PhaseConfigurationTestListener.class, "phaseSet", String.class); + assertPhase(m, TransactionPhase.AFTER_ROLLBACK); + } + + private void assertPhase(Method method, TransactionPhase expected) { + assertNotNull("Method must not be null", method); + TransactionalEventListener annotation = ApplicationListenerMethodTransactionalAdapter.findAnnotation(method); + assertEquals("Wrong phase for '" + method + "'", expected, annotation.phase()); + } + + + static class PhaseConfigurationTestListener { + + public void noAnnotation(String data) { + } + + @TransactionalEventListener + public void defaultPhase(String data) { + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) + public void phaseSet(String data) { + } + + } + +} diff --git a/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java b/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java new file mode 100644 index 00000000000..c3728c8387e --- /dev/null +++ b/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java @@ -0,0 +1,496 @@ +/* + * 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.event; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.After; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.tests.transaction.CallCountingTransactionManager; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.junit.Assert.*; +import static org.springframework.transaction.event.TransactionPhase.*; + +/** + * @author Stephane Nicoll + */ +public class TransactionalEventListenerTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private ConfigurableApplicationContext context; + + private EventCollector eventCollector; + + private TransactionTemplate transactionTemplate = new TransactionTemplate(new CallCountingTransactionManager()); + + @After + public void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void immediately() { + load(ImmediateTestListener.class); + this.transactionTemplate.execute(status -> { + getContext().publishEvent("test"); + getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "test"); + getEventCollector().assertTotalEventsCount(1); + return null; + + }); + getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "test"); + getEventCollector().assertTotalEventsCount(1); + } + + @Test + public void immediatelyImpactsCurrentTransaction() { + load(ImmediateTestListener.class, BeforeCommitTestListener.class); + try { + this.transactionTemplate.execute(status -> { + getContext().publishEvent("FAIL"); + fail("Should have thrown an exception at this point"); + return null; + }); + } + catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("Test exception")); + assertTrue(e.getMessage().contains(EventCollector.IMMEDIATELY)); + } + getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "FAIL"); + getEventCollector().assertTotalEventsCount(1); + } + + @Test + public void afterCompletionCommit() { + load(AfterCompletionTestListener.class); + this.transactionTemplate.execute(status -> { + getContext().publishEvent("test"); + getEventCollector().assertNoEventReceived(); + return null; + + }); + getEventCollector().assertEvents(EventCollector.AFTER_COMPLETION, "test"); + getEventCollector().assertTotalEventsCount(1); // After rollback not invoked + } + + @Test + public void afterCompletionRollback() { + load(AfterCompletionTestListener.class); + this.transactionTemplate.execute(status -> { + getContext().publishEvent("test"); + getEventCollector().assertNoEventReceived(); + status.setRollbackOnly(); + return null; + + }); + getEventCollector().assertEvents(EventCollector.AFTER_COMPLETION, "test"); + getEventCollector().assertTotalEventsCount(1); // After rollback not invoked + } + + @Test + public void afterCommit() { + load(AfterCompletionExplicitTestListener.class); + this.transactionTemplate.execute(status -> { + getContext().publishEvent("test"); + getEventCollector().assertNoEventReceived(); + return null; + + }); + getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test"); + getEventCollector().assertTotalEventsCount(1); // After rollback not invoked + } + + @Test + public void afterRollback() { + load(AfterCompletionExplicitTestListener.class); + this.transactionTemplate.execute(status -> { + getContext().publishEvent("test"); + getEventCollector().assertNoEventReceived(); + status.setRollbackOnly(); + return null; + }); + getEventCollector().assertEvents(EventCollector.AFTER_ROLLBACK, "test"); + getEventCollector().assertTotalEventsCount(1); // After commit not invoked + } + + @Test + public void beforeCommit() { + load(BeforeCommitTestListener.class); + this.transactionTemplate.execute(status -> { + TransactionSynchronizationManager.registerSynchronization(new EventTransactionSynchronization(10) { + @Override + public void beforeCommit(boolean readOnly) { + getEventCollector().assertNoEventReceived(); // Not seen yet + } + }); + TransactionSynchronizationManager.registerSynchronization(new EventTransactionSynchronization(20) { + @Override + public void beforeCommit(boolean readOnly) { + getEventCollector().assertEvents(EventCollector.BEFORE_COMMIT, "test"); + getEventCollector().assertTotalEventsCount(1); + } + }); + getContext().publishEvent("test"); + getEventCollector().assertNoEventReceived(); + return null; + + }); + getEventCollector().assertEvents(EventCollector.BEFORE_COMMIT, "test"); + getEventCollector().assertTotalEventsCount(1); + } + + @Test + public void beforeCommitWithException() { // Validates the custom synchronization is invoked + load(BeforeCommitTestListener.class); + try { + this.transactionTemplate.execute(status -> { + TransactionSynchronizationManager.registerSynchronization(new EventTransactionSynchronization(10) { + @Override + public void beforeCommit(boolean readOnly) { + throw new IllegalStateException("test"); + } + }); + getContext().publishEvent("test"); + getEventCollector().assertNoEventReceived(); + return null; + + }); + fail("Should have thrown an exception"); + } + catch (IllegalStateException e) { + // Test exception - ignore + } + getEventCollector().assertNoEventReceived(); // Before commit not invoked + } + + @Test + public void regularTransaction() { + load(ImmediateTestListener.class, BeforeCommitTestListener.class, AfterCompletionExplicitTestListener.class); + this.transactionTemplate.execute(status -> { + TransactionSynchronizationManager.registerSynchronization(new EventTransactionSynchronization(10) { + @Override + public void beforeCommit(boolean readOnly) { + getEventCollector().assertTotalEventsCount(1); // Immediate event + getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "test"); + } + }); + TransactionSynchronizationManager.registerSynchronization(new EventTransactionSynchronization(20) { + @Override + public void beforeCommit(boolean readOnly) { + getEventCollector().assertEvents(EventCollector.BEFORE_COMMIT, "test"); + getEventCollector().assertTotalEventsCount(2); + } + }); + getContext().publishEvent("test"); + getEventCollector().assertTotalEventsCount(1); + return null; + + }); + getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test"); + getEventCollector().assertTotalEventsCount(3); // Immediate, before commit, after commit + } + + @Test + public void noTransaction() { + load(BeforeCommitTestListener.class, AfterCompletionTestListener.class, + AfterCompletionExplicitTestListener.class); + this.context.publishEvent("test"); + getEventCollector().assertTotalEventsCount(0); + } + + @Test + public void noTransactionWithFallbackExecution() { + load(FallbackExecutionTestListener.class); + this.context.publishEvent("test"); + this.eventCollector.assertEvents(EventCollector.BEFORE_COMMIT, "test"); + this.eventCollector.assertEvents(EventCollector.AFTER_COMMIT, "test"); + this.eventCollector.assertEvents(EventCollector.AFTER_ROLLBACK, "test"); + this.eventCollector.assertEvents(EventCollector.AFTER_COMPLETION, "test"); + getEventCollector().assertTotalEventsCount(4); + } + + @Test + public void conditionFoundOnTransactionalEventListener() { + load(ImmediateTestListener.class); + this.transactionTemplate.execute(status -> { + getContext().publishEvent("SKIP"); + getEventCollector().assertNoEventReceived(); + return null; + + }); + getEventCollector().assertNoEventReceived(); + } + + @Test + @Ignore("not an event listener if not tagged") + public void afterCommitMetaAnnotation() { + load(AfterCommitMetaAnnotationTestListener.class); + this.transactionTemplate.execute(status -> { + getContext().publishEvent("test"); + getEventCollector().assertNoEventReceived(); + return null; + + }); + getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test"); + getEventCollector().assertTotalEventsCount(1); + } + + @Test + @Ignore("not an event listener if not tagged + condition found on wrong annotation") + public void conditionFoundOnMetaAnnotation() { + load(AfterCommitMetaAnnotationTestListener.class); + this.transactionTemplate.execute(status -> { + getContext().publishEvent("SKIP"); + getEventCollector().assertNoEventReceived(); + return null; + + }); + getEventCollector().assertNoEventReceived(); + } + + @Configuration + static class BasicConfiguration { + + @Bean // set automatically with tx management + public TransactionalEventListenerFactory transactionalEventListenerFactory() { + return new TransactionalEventListenerFactory(); + } + + @Bean + public EventCollector eventCollector() { + return new EventCollector(); + } + } + + + protected EventCollector getEventCollector() { + return eventCollector; + } + + protected ConfigurableApplicationContext getContext() { + return context; + } + + private void load(Class... classes) { + List> allClasses = new ArrayList<>(); + allClasses.add(BasicConfiguration.class); + allClasses.addAll(Arrays.asList(classes)); + doLoad(allClasses.toArray(new Class[allClasses.size()])); + } + + private void doLoad(Class... classes) { + this.context = new AnnotationConfigApplicationContext(classes); + this.eventCollector = this.context.getBean(EventCollector.class); + } + + static class EventCollector { + + public static final String IMMEDIATELY = "IMMEDIATELY"; + + public static final String BEFORE_COMMIT = "BEFORE_COMMIT"; + + public static final String AFTER_COMPLETION = "AFTER_COMPLETION"; + + public static final String AFTER_COMMIT = "AFTER_COMMIT"; + + public static final String AFTER_ROLLBACK = "AFTER_ROLLBACK"; + + public static final String[] ALL_PHASES = {IMMEDIATELY, BEFORE_COMMIT, AFTER_COMMIT, AFTER_ROLLBACK}; + + + private final MultiValueMap events = new LinkedMultiValueMap<>(); + + public void addEvent(String phase, Object event) { + this.events.add(phase, event); + } + + public List getEvents(String phase) { + return this.events.getOrDefault(phase, Collections.emptyList()); + } + + public void assertNoEventReceived(String... phases) { + if (phases.length == 0) { // All values if none set + phases = ALL_PHASES; + } + for (String phase : phases) { + List eventsForPhase = getEvents(phase); + assertEquals("Expected no event for phase '" + phase + "' " + + "but got " + eventsForPhase, 0, eventsForPhase.size()); + } + } + + public void assertEvents(String phase, Object... expected) { + List actual = getEvents(phase); + assertEquals("wrong number of events for phase '" + phase + "'", expected.length, actual.size()); + for (int i = 0; i < expected.length; i++) { + assertEquals("Wrong event for phase '" + phase + "' at index " + i, expected[i], actual.get(i)); + } + } + + public void assertTotalEventsCount(int number) { + int size = 0; + for (Map.Entry> entry : this.events.entrySet()) { + size += entry.getValue().size(); + } + assertEquals("Wrong number of total events (" + this.events.size() + ") " + + "registered phase(s)", number, size); + } + + } + + static abstract class BaseTransactionalTestListener { + + static final String FAIL_MSG = "FAIL"; + + @Autowired + private EventCollector eventCollector; + + public void handleEvent(String phase, String data) { + this.eventCollector.addEvent(phase, data); + if (FAIL_MSG.equals(data)) { + throw new IllegalStateException("Test exception on phase '" + phase + "'"); + } + } + + } + + @Component + static class ImmediateTestListener extends BaseTransactionalTestListener { + + @EventListener(condition = "!'SKIP'.equals(#data)") + public void handleImmediately(String data) { + handleEvent(EventCollector.IMMEDIATELY, data); + } + } + + @Component + static class AfterCompletionTestListener extends BaseTransactionalTestListener { + + @TransactionalEventListener(phase = AFTER_COMPLETION) + public void handleAfterCompletion(String data) { + handleEvent(EventCollector.AFTER_COMPLETION, data); + } + } + + @Component + static class AfterCompletionExplicitTestListener extends BaseTransactionalTestListener { + + @TransactionalEventListener(phase = AFTER_COMMIT) + public void handleAfterCommit(String data) { + handleEvent(EventCollector.AFTER_COMMIT, data); + } + + @TransactionalEventListener(phase = AFTER_ROLLBACK) + public void handleAfterRollback(String data) { + handleEvent(EventCollector.AFTER_ROLLBACK, data); + } + } + + @Component + static class BeforeCommitTestListener extends BaseTransactionalTestListener { + + @TransactionalEventListener(phase = BEFORE_COMMIT) + @Order(15) + public void handleBeforeCommit(String data) { + handleEvent(EventCollector.BEFORE_COMMIT, data); + } + + } + + @Component + static class FallbackExecutionTestListener extends BaseTransactionalTestListener { + + @TransactionalEventListener(phase = BEFORE_COMMIT, fallbackExecution = true) + public void handleBeforeCommit(String data) { + handleEvent(EventCollector.BEFORE_COMMIT, data); + } + + @TransactionalEventListener(phase = AFTER_COMMIT, fallbackExecution = true) + public void handleAfterCommit(String data) { + handleEvent(EventCollector.AFTER_COMMIT, data); + } + + @TransactionalEventListener(phase = AFTER_ROLLBACK, fallbackExecution = true) + public void handleAfterRollback(String data) { + handleEvent(EventCollector.AFTER_ROLLBACK, data); + } + + @TransactionalEventListener(phase = AFTER_COMPLETION, fallbackExecution = true) + public void handleAfterCompletion(String data) { + handleEvent(EventCollector.AFTER_COMPLETION, data); + } + } + + @TransactionalEventListener(phase = AFTER_COMMIT, condition = "!'SKIP'.equals(#p0)") + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @interface AfterCommitEventListener { + } + + @Component + static class AfterCommitMetaAnnotationTestListener extends BaseTransactionalTestListener { + + @AfterCommitEventListener + public void handleAfterCommit(String data) { + handleEvent(EventCollector.AFTER_COMMIT, data); + } + + } + + static class EventTransactionSynchronization extends TransactionSynchronizationAdapter { + private final int order; + + EventTransactionSynchronization(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return order; + } + } + +}