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 <tx:annotation-driven/>

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
This commit is contained in:
Stephane Nicoll
2015-02-02 10:46:28 +01:00
parent f0fca890bb
commit 4741a12fdc
17 changed files with 1161 additions and 34 deletions

View File

@@ -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(

View File

@@ -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.

View File

@@ -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";
}

View File

@@ -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.
* <p>
* 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);
}
}
}

View File

@@ -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
}

View File

@@ -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}.
* <p>
* 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}.
* <p>
* 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.
* <p>Default is "", meaning the event is always handled.
* @see EventListener#condition()
*/
String condition() default "";
}

View File

@@ -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);
}
}

View File

@@ -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");
}

View File

@@ -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 =

View File

@@ -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) {
}
}
}

View File

@@ -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<Class<?>> 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<String, Object> events = new LinkedMultiValueMap<>();
public void addEvent(String phase, Object event) {
this.events.add(phase, event);
}
public List<Object> 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<Object> eventsForPhase = getEvents(phase);
assertEquals("Expected no event for phase '" + phase + "' " +
"but got " + eventsForPhase, 0, eventsForPhase.size());
}
}
public void assertEvents(String phase, Object... expected) {
List<Object> 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<String, List<Object>> 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;
}
}
}