Browse Source

Avoid transaction listener execution without transaction management setup

Closes gh-32319
pull/32412/head
Juergen Hoeller 2 years ago
parent
commit
524588ef93
  1. 17
      spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java
  2. 10
      spring-context/src/main/java/org/springframework/context/event/EventListener.java
  3. 7
      spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java
  4. 14
      spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java
  5. 46
      spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java

17
spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java

@ -91,6 +91,8 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
@Nullable @Nullable
private final String condition; private final String condition;
private final boolean defaultExecution;
private final int order; private final int order;
@Nullable @Nullable
@ -119,6 +121,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
EventListener ann = AnnotatedElementUtils.findMergedAnnotation(this.targetMethod, EventListener.class); EventListener ann = AnnotatedElementUtils.findMergedAnnotation(this.targetMethod, EventListener.class);
this.declaredEventTypes = resolveDeclaredEventTypes(method, ann); this.declaredEventTypes = resolveDeclaredEventTypes(method, ann);
this.condition = (ann != null ? ann.condition() : null); this.condition = (ann != null ? ann.condition() : null);
this.defaultExecution = (ann == null || ann.defaultExecution());
this.order = resolveOrder(this.targetMethod); this.order = resolveOrder(this.targetMethod);
String id = (ann != null ? ann.id() : ""); String id = (ann != null ? ann.id() : "");
this.listenerId = (!id.isEmpty() ? id : null); this.listenerId = (!id.isEmpty() ? id : null);
@ -166,7 +169,9 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
@Override @Override
public void onApplicationEvent(ApplicationEvent event) { public void onApplicationEvent(ApplicationEvent event) {
processEvent(event); if (isDefaultExecution()) {
processEvent(event);
}
} }
@Override @Override
@ -227,6 +232,16 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
return ClassUtils.getQualifiedMethodName(method) + sj; return ClassUtils.getQualifiedMethodName(method) + sj;
} }
/**
* Return whether default execution is applicable for the target listener.
* @since 6.2
* @see #onApplicationEvent
* @see EventListener#defaultExecution()
*/
protected boolean isDefaultExecution() {
return this.defaultExecution;
}
/** /**
* Process the specified {@link ApplicationEvent}, checking if the condition * Process the specified {@link ApplicationEvent}, checking if the condition

10
spring-context/src/main/java/org/springframework/context/event/EventListener.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -132,6 +132,14 @@ public @interface EventListener {
*/ */
String condition() default ""; String condition() default "";
/**
* Whether the event should be handled by default, without any special
* pre-conditions such as an active transaction. Declared here for overriding
* in composed annotations such as {@code TransactionalEventListener}.
* @since 6.2
*/
boolean defaultExecution() default true;
/** /**
* An optional identifier for the listener, defaulting to the fully-qualified * An optional identifier for the listener, defaulting to the fully-qualified
* signature of the declaring method (e.g. "mypackage.MyClass.myMethod()"). * signature of the declaring method (e.g. "mypackage.MyClass.myMethod()").

7
spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -49,8 +49,6 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi
private final TransactionPhase transactionPhase; private final TransactionPhase transactionPhase;
private final boolean fallbackExecution;
private final List<SynchronizationCallback> callbacks = new CopyOnWriteArrayList<>(); private final List<SynchronizationCallback> callbacks = new CopyOnWriteArrayList<>();
@ -68,7 +66,6 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi
throw new IllegalStateException("No TransactionalEventListener annotation found on method: " + method); throw new IllegalStateException("No TransactionalEventListener annotation found on method: " + method);
} }
this.transactionPhase = eventAnn.phase(); this.transactionPhase = eventAnn.phase();
this.fallbackExecution = eventAnn.fallbackExecution();
} }
@ -91,7 +88,7 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi
logger.debug("Registered transaction synchronization for " + event); logger.debug("Registered transaction synchronization for " + event);
} }
} }
else if (this.fallbackExecution) { else if (isDefaultExecution()) {
if (getTransactionPhase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { if (getTransactionPhase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) {
logger.warn("Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase"); logger.warn("Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase");
} }

14
spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -77,11 +77,6 @@ public @interface TransactionalEventListener {
*/ */
TransactionPhase phase() default TransactionPhase.AFTER_COMMIT; TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;
/**
* Whether the event should be handled if no transaction is running.
*/
boolean fallbackExecution() default false;
/** /**
* Alias for {@link #classes}. * Alias for {@link #classes}.
*/ */
@ -107,6 +102,13 @@ public @interface TransactionalEventListener {
@AliasFor(annotation = EventListener.class, attribute = "condition") @AliasFor(annotation = EventListener.class, attribute = "condition")
String condition() default ""; String condition() default "";
/**
* Whether the event should be handled if no transaction is running.
* @see EventListener#defaultExecution()
*/
@AliasFor(annotation = EventListener.class, attribute = "defaultExecution")
boolean fallbackExecution() default false;
/** /**
* An optional identifier for the listener, defaulting to the fully-qualified * An optional identifier for the listener, defaulting to the fully-qualified
* signature of the declaring method (e.g. "mypackage.MyClass.myMethod()"). * signature of the declaring method (e.g. "mypackage.MyClass.myMethod()").

46
spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java

@ -269,8 +269,7 @@ class TransactionalEventListenerTests {
@Test @Test
void noTransaction() { void noTransaction() {
load(BeforeCommitTestListener.class, AfterCompletionTestListener.class, load(BeforeCommitTestListener.class, AfterCompletionTestListener.class, AfterCompletionExplicitTestListener.class);
AfterCompletionExplicitTestListener.class);
this.context.publishEvent("test"); this.context.publishEvent("test");
getEventCollector().assertTotalEventsCount(0); getEventCollector().assertTotalEventsCount(0);
} }
@ -318,6 +317,24 @@ class TransactionalEventListenerTests {
getEventCollector().assertTotalEventsCount(4); getEventCollector().assertTotalEventsCount(4);
} }
@Test
void noTransactionManagementWithFallbackExecution() {
doLoad(PlainConfiguration.class, 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
void noTransactionManagementWithoutFallbackExecution() {
doLoad(PlainConfiguration.class, BeforeCommitTestListener.class, AfterCommitMetaAnnotationTestListener.class);
this.context.publishEvent("test");
this.eventCollector.assertNoEventReceived();
}
@Test @Test
void conditionFoundOnTransactionalEventListener() { void conditionFoundOnTransactionalEventListener() {
load(ImmediateTestListener.class); load(ImmediateTestListener.class);
@ -401,6 +418,31 @@ class TransactionalEventListenerTests {
} }
@Configuration
static class PlainConfiguration {
@Bean
public EventCollector eventCollector() {
return new EventCollector();
}
@Bean
public TestBean testBean(ApplicationEventPublisher eventPublisher) {
return new TestBean(eventPublisher);
}
@Bean
public CallCountingTransactionManager transactionManager() {
return new CallCountingTransactionManager();
}
@Bean
public TransactionTemplate transactionTemplate() {
return new TransactionTemplate(transactionManager());
}
}
@Configuration @Configuration
static class MulticasterWithCustomExecutor { static class MulticasterWithCustomExecutor {

Loading…
Cancel
Save