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
+ * 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