From 524588ef93a7fe21a62b022ef8eb24893689fe19 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 23 Feb 2024 11:53:05 +0100 Subject: [PATCH] Avoid transaction listener execution without transaction management setup Closes gh-32319 --- .../ApplicationListenerMethodAdapter.java | 17 ++++++- .../context/event/EventListener.java | 10 +++- ...ionalApplicationListenerMethodAdapter.java | 7 +-- .../event/TransactionalEventListener.java | 14 +++--- .../TransactionalEventListenerTests.java | 46 ++++++++++++++++++- 5 files changed, 79 insertions(+), 15 deletions(-) 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 030f598767b..a3591df8afd 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 @@ -91,6 +91,8 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe @Nullable private final String condition; + private final boolean defaultExecution; + private final int order; @Nullable @@ -119,6 +121,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe EventListener ann = AnnotatedElementUtils.findMergedAnnotation(this.targetMethod, EventListener.class); this.declaredEventTypes = resolveDeclaredEventTypes(method, ann); this.condition = (ann != null ? ann.condition() : null); + this.defaultExecution = (ann == null || ann.defaultExecution()); this.order = resolveOrder(this.targetMethod); String id = (ann != null ? ann.id() : ""); this.listenerId = (!id.isEmpty() ? id : null); @@ -166,7 +169,9 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe @Override public void onApplicationEvent(ApplicationEvent event) { - processEvent(event); + if (isDefaultExecution()) { + processEvent(event); + } } @Override @@ -227,6 +232,16 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe 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 diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListener.java b/spring-context/src/main/java/org/springframework/context/event/EventListener.java index 28ebecfa4ea..71652574b40 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListener.java +++ b/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"); * you may not use this file except in compliance with the License. @@ -132,6 +132,14 @@ public @interface EventListener { */ 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 * signature of the declaring method (e.g. "mypackage.MyClass.myMethod()"). diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java index 0ac4138bf3b..a61b99abd73 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java +++ b/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"); * 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 boolean fallbackExecution; - private final List callbacks = new CopyOnWriteArrayList<>(); @@ -68,7 +66,6 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi throw new IllegalStateException("No TransactionalEventListener annotation found on method: " + method); } this.transactionPhase = eventAnn.phase(); - this.fallbackExecution = eventAnn.fallbackExecution(); } @@ -91,7 +88,7 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi logger.debug("Registered transaction synchronization for " + event); } } - else if (this.fallbackExecution) { + else if (isDefaultExecution()) { if (getTransactionPhase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { logger.warn("Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase"); } 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 index 3ade90efc80..579017c9546 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java +++ b/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"); * 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; - /** - * Whether the event should be handled if no transaction is running. - */ - boolean fallbackExecution() default false; - /** * Alias for {@link #classes}. */ @@ -107,6 +102,13 @@ public @interface TransactionalEventListener { @AliasFor(annotation = EventListener.class, attribute = "condition") 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 * signature of the declaring method (e.g. "mypackage.MyClass.myMethod()"). 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 index 193935eb296..17cbf356082 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java @@ -269,8 +269,7 @@ class TransactionalEventListenerTests { @Test void noTransaction() { - load(BeforeCommitTestListener.class, AfterCompletionTestListener.class, - AfterCompletionExplicitTestListener.class); + load(BeforeCommitTestListener.class, AfterCompletionTestListener.class, AfterCompletionExplicitTestListener.class); this.context.publishEvent("test"); getEventCollector().assertTotalEventsCount(0); } @@ -318,6 +317,24 @@ class TransactionalEventListenerTests { 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 void conditionFoundOnTransactionalEventListener() { 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 static class MulticasterWithCustomExecutor {