From 7d4c8a403e411d8e807c1d669a1257fc132ddaf6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 5 Mar 2024 18:08:08 +0100 Subject: [PATCH] Introduce configurable default rollback rules Includes rollbackOn annotation attribute on @EnableTransactionManagement and addDefaultRollbackRule method on AnnotationTransactionAttributeSource, as well as publicMethodsOnly as instance-level flag (also on AnnotationCacheOperationSource). Closes gh-23473 --- .../transaction/declarative/annotations.adoc | 25 +++++- ...JtaTransactionManagementConfiguration.java | 8 +- ...ctJTransactionManagementConfiguration.java | 8 +- .../AnnotationCacheOperationSource.java | 29 +++--- ...actTransactionManagementConfiguration.java | 17 +++- .../AnnotationTransactionAttributeSource.java | 89 ++++++++++++------- .../EnableTransactionManagement.java | 31 +++++-- ...oxyTransactionManagementConfiguration.java | 9 +- .../transaction/annotation/RollbackOn.java | 49 ++++++++++ .../TransactionAnnotationParser.java | 4 +- .../interceptor/RollbackRuleAttribute.java | 10 ++- .../EnableTransactionManagementTests.java | 87 ++++++++++++++++-- 12 files changed, 295 insertions(+), 71 deletions(-) create mode 100644 spring-tx/src/main/java/org/springframework/transaction/annotation/RollbackOn.java diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc index c8a83923b5..56f421e47e 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc @@ -436,9 +436,28 @@ properties of the `@Transactional` annotation: | Optional array of exception name patterns that must not cause rollback. |=== -TIP: See xref:data-access/transaction/declarative/rolling-back.adoc#transaction-declarative-rollback-rules[Rollback rules] for further details -on rollback rule semantics, patterns, and warnings regarding possible unintentional -matches for pattern-based rollback rules. +TIP: See xref:data-access/transaction/declarative/rolling-back.adoc#transaction-declarative-rollback-rules[Rollback rules] +for further details on rollback rule semantics, patterns, and warnings +regarding possible unintentional matches for pattern-based rollback rules. + +[NOTE] +==== +As of 6.2, you can globally change the default rollback behavior: e.g. through +`@EnableTransactionManagement(rollbackOn=ALL_EXCEPTIONS)`, leading to a rollback +for all exceptions raised within a transaction, including any checked exception. +For further customizations, `AnnotationTransactionAttributeSource` provides an +`addDefaultRollbackRule(RollbackRuleAttribute)` method for custom default rules. + +Note that transaction-specific rollback rules override the default behavior but +retain the chosen default for unspecified exceptions. This is the case for +Spring's `@Transactional` as well as JTA's `jakarta.transaction.Transactional`. + +Unless you rely on EJB-style business exceptions with commit behavior, it is +advisable to switch to `ALL_EXCEPTIONS` for a consistent rollback even in case +of a (potentially accidental) checked exception. Also, it is advisable to make +that switch for Kotlin-based applications where there is no enforcement of +checked exceptions at all. +==== Currently, you cannot have explicit control over the name of a transaction, where 'name' means the transaction name that appears in a transaction monitor and in logging output. diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java index 0ed7ffb69e..fc51788cd0 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -23,6 +23,7 @@ import org.springframework.context.annotation.Role; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.TransactionManagementConfigurationSelector; import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.interceptor.TransactionAttributeSource; /** * {@code @Configuration} class that registers the Spring infrastructure beans necessary @@ -35,14 +36,15 @@ import org.springframework.transaction.config.TransactionManagementConfigUtils; * @see EnableTransactionManagement * @see TransactionManagementConfigurationSelector */ -@Configuration +@Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class AspectJJtaTransactionManagementConfiguration extends AspectJTransactionManagementConfiguration { @Bean(name = TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public JtaAnnotationTransactionAspect jtaTransactionAspect() { + public JtaAnnotationTransactionAspect jtaTransactionAspect(TransactionAttributeSource transactionAttributeSource) { JtaAnnotationTransactionAspect txAspect = JtaAnnotationTransactionAspect.aspectOf(); + txAspect.setTransactionAttributeSource(transactionAttributeSource); if (this.txManager != null) { txAspect.setTransactionManager(this.txManager); } diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java index 2c99c30507..4e82c4524a 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -24,6 +24,7 @@ import org.springframework.transaction.annotation.AbstractTransactionManagementC import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.TransactionManagementConfigurationSelector; import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.interceptor.TransactionAttributeSource; /** * {@code @Configuration} class that registers the Spring infrastructure beans necessary @@ -37,14 +38,15 @@ import org.springframework.transaction.config.TransactionManagementConfigUtils; * @see TransactionManagementConfigurationSelector * @see AspectJJtaTransactionManagementConfiguration */ -@Configuration +@Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class AspectJTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration { @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ASPECT_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public AnnotationTransactionAspect transactionAspect() { + public AnnotationTransactionAspect transactionAspect(TransactionAttributeSource transactionAttributeSource) { AnnotationTransactionAspect txAspect = AnnotationTransactionAspect.aspectOf(); + txAspect.setTransactionAttributeSource(transactionAttributeSource); if (this.txManager != null) { txAspect.setTransactionManager(this.txManager); } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java index acc7242121..8f26e688a6 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.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. @@ -19,10 +19,8 @@ package org.springframework.cache.annotation; import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.Set; import org.springframework.cache.interceptor.AbstractFallbackCacheOperationSource; @@ -47,17 +45,17 @@ import org.springframework.util.Assert; @SuppressWarnings("serial") public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperationSource implements Serializable { - private final boolean publicMethodsOnly; - private final Set annotationParsers; + private boolean publicMethodsOnly = true; + /** * Create a default AnnotationCacheOperationSource, supporting public methods * that carry the {@code Cacheable} and {@code CacheEvict} annotations. */ public AnnotationCacheOperationSource() { - this(true); + this.annotationParsers = Collections.singleton(new SpringCacheAnnotationParser()); } /** @@ -66,10 +64,11 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati * @param publicMethodsOnly whether to support only annotated public methods * typically for use with proxy-based AOP), or protected/private methods as well * (typically used with AspectJ class weaving) + * @see #setPublicMethodsOnly */ public AnnotationCacheOperationSource(boolean publicMethodsOnly) { + this(); this.publicMethodsOnly = publicMethodsOnly; - this.annotationParsers = Collections.singleton(new SpringCacheAnnotationParser()); } /** @@ -77,7 +76,6 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati * @param annotationParser the CacheAnnotationParser to use */ public AnnotationCacheOperationSource(CacheAnnotationParser annotationParser) { - this.publicMethodsOnly = true; Assert.notNull(annotationParser, "CacheAnnotationParser must not be null"); this.annotationParsers = Collections.singleton(annotationParser); } @@ -87,9 +85,8 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati * @param annotationParsers the CacheAnnotationParser to use */ public AnnotationCacheOperationSource(CacheAnnotationParser... annotationParsers) { - this.publicMethodsOnly = true; Assert.notEmpty(annotationParsers, "At least one CacheAnnotationParser needs to be specified"); - this.annotationParsers = new LinkedHashSet<>(Arrays.asList(annotationParsers)); + this.annotationParsers = Set.of(annotationParsers); } /** @@ -97,12 +94,21 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati * @param annotationParsers the CacheAnnotationParser to use */ public AnnotationCacheOperationSource(Set annotationParsers) { - this.publicMethodsOnly = true; Assert.notEmpty(annotationParsers, "At least one CacheAnnotationParser needs to be specified"); this.annotationParsers = annotationParsers; } + /** + * Set whether cacheable methods are expected to be public. + *

The default is {@code true}. + * @since 6.2 + */ + public void setPublicMethodsOnly(boolean publicMethodsOnly) { + this.publicMethodsOnly = publicMethodsOnly; + } + + @Override public boolean isCandidateClass(Class targetClass) { for (CacheAnnotationParser parser : this.annotationParsers) { @@ -156,6 +162,7 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati /** * By default, only public methods can be made cacheable. + * @see #setPublicMethodsOnly */ @Override protected boolean allowPublicMethodsOnly() { diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java index 0bcfb3b707..178bd5640d 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java @@ -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. @@ -30,6 +30,8 @@ import org.springframework.lang.Nullable; import org.springframework.transaction.TransactionManager; import org.springframework.transaction.config.TransactionManagementConfigUtils; import org.springframework.transaction.event.TransactionalEventListenerFactory; +import org.springframework.transaction.interceptor.RollbackRuleAttribute; +import org.springframework.transaction.interceptor.TransactionAttributeSource; import org.springframework.util.CollectionUtils; /** @@ -38,6 +40,7 @@ import org.springframework.util.CollectionUtils; * * @author Chris Beams * @author Stephane Nicoll + * @author Juergen Hoeller * @since 3.1 * @see EnableTransactionManagement */ @@ -77,6 +80,18 @@ public abstract class AbstractTransactionManagementConfiguration implements Impo } + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionAttributeSource transactionAttributeSource() { + // Accept protected @Transactional methods on CGLIB proxies, as of 6.0 + AnnotationTransactionAttributeSource tas = new AnnotationTransactionAttributeSource(false); + // Apply default rollback rule, as of 6.2 + if (this.enableTx != null && this.enableTx.getEnum("rollbackOn") == RollbackOn.ALL_EXCEPTIONS) { + tas.addDefaultRollbackRule(RollbackRuleAttribute.ROLLBACK_ON_ALL_EXCEPTIONS); + } + return tas; + } + @Bean(name = TransactionManagementConfigUtils.TRANSACTIONAL_EVENT_LISTENER_FACTORY_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public static TransactionalEventListenerFactory transactionalEventListenerFactory() { diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java index 7fa46bb239..77bed85030 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java @@ -19,13 +19,14 @@ package org.springframework.transaction.annotation; import java.io.Serializable; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import org.springframework.lang.Nullable; import org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource; +import org.springframework.transaction.interceptor.RollbackRuleAttribute; +import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -57,20 +58,23 @@ import org.springframework.util.CollectionUtils; public class AnnotationTransactionAttributeSource extends AbstractFallbackTransactionAttributeSource implements Serializable { - private static final boolean jta12Present; + private static final boolean jtaPresent; private static final boolean ejb3Present; static { ClassLoader classLoader = AnnotationTransactionAttributeSource.class.getClassLoader(); - jta12Present = ClassUtils.isPresent("jakarta.transaction.Transactional", classLoader); + jtaPresent = ClassUtils.isPresent("jakarta.transaction.Transactional", classLoader); ejb3Present = ClassUtils.isPresent("jakarta.ejb.TransactionAttribute", classLoader); } - private final boolean publicMethodsOnly; - private final Set annotationParsers; + private boolean publicMethodsOnly = true; + + @Nullable + private Set defaultRollbackRules; + /** * Create a default AnnotationTransactionAttributeSource, supporting @@ -78,24 +82,10 @@ public class AnnotationTransactionAttributeSource extends AbstractFallbackTransa * or the EJB3 {@link jakarta.ejb.TransactionAttribute} annotation. */ public AnnotationTransactionAttributeSource() { - this(true); - } - - /** - * Create a custom AnnotationTransactionAttributeSource, supporting - * public methods that carry the {@code Transactional} annotation - * or the EJB3 {@link jakarta.ejb.TransactionAttribute} annotation. - * @param publicMethodsOnly whether to support public methods that carry - * the {@code Transactional} annotation only (typically for use - * with proxy-based AOP), or protected/private methods as well - * (typically used with AspectJ class weaving) - */ - public AnnotationTransactionAttributeSource(boolean publicMethodsOnly) { - this.publicMethodsOnly = publicMethodsOnly; - if (jta12Present || ejb3Present) { + if (jtaPresent || ejb3Present) { this.annotationParsers = CollectionUtils.newLinkedHashSet(3); this.annotationParsers.add(new SpringTransactionAnnotationParser()); - if (jta12Present) { + if (jtaPresent) { this.annotationParsers.add(new JtaTransactionAnnotationParser()); } if (ejb3Present) { @@ -107,12 +97,26 @@ public class AnnotationTransactionAttributeSource extends AbstractFallbackTransa } } + /** + * Create a custom AnnotationTransactionAttributeSource, supporting + * public methods that carry the {@code Transactional} annotation + * or the EJB3 {@link jakarta.ejb.TransactionAttribute} annotation. + * @param publicMethodsOnly whether to support public methods that carry + * the {@code Transactional} annotation only (typically for use + * with proxy-based AOP), or protected/private methods as well + * (typically used with AspectJ class weaving) + * @see #setPublicMethodsOnly + */ + public AnnotationTransactionAttributeSource(boolean publicMethodsOnly) { + this(); + this.publicMethodsOnly = publicMethodsOnly; + } + /** * Create a custom AnnotationTransactionAttributeSource. * @param annotationParser the TransactionAnnotationParser to use */ public AnnotationTransactionAttributeSource(TransactionAnnotationParser annotationParser) { - this.publicMethodsOnly = true; Assert.notNull(annotationParser, "TransactionAnnotationParser must not be null"); this.annotationParsers = Collections.singleton(annotationParser); } @@ -122,19 +126,40 @@ public class AnnotationTransactionAttributeSource extends AbstractFallbackTransa * @param annotationParsers the TransactionAnnotationParsers to use */ public AnnotationTransactionAttributeSource(TransactionAnnotationParser... annotationParsers) { - this.publicMethodsOnly = true; Assert.notEmpty(annotationParsers, "At least one TransactionAnnotationParser needs to be specified"); - this.annotationParsers = new LinkedHashSet<>(Arrays.asList(annotationParsers)); + this.annotationParsers = Set.of(annotationParsers); + } + + + /** + * Set whether transactional methods are expected to be public. + *

The default is {@code true}. + * @since 6.2 + * @see #AnnotationTransactionAttributeSource(boolean) + */ + public void setPublicMethodsOnly(boolean publicMethodsOnly) { + this.publicMethodsOnly = publicMethodsOnly; } /** - * Create a custom AnnotationTransactionAttributeSource. - * @param annotationParsers the TransactionAnnotationParsers to use + * Add a default rollback rule, to be applied to all rule-based + * transaction attributes returned by this source. + *

By default, a rollback will be triggered on unchecked exceptions + * but not on checked exceptions. A default rule may override this + * while still respecting any custom rules in the transaction attribute. + * @param rollbackRule a rollback rule overriding the default behavior, + * e.g. {@link RollbackRuleAttribute#ROLLBACK_ON_ALL_EXCEPTIONS} + * @since 6.2 + * @see RuleBasedTransactionAttribute#getRollbackRules() + * @see EnableTransactionManagement#rollbackOn() + * @see Transactional#rollbackFor() + * @see Transactional#noRollbackFor() */ - public AnnotationTransactionAttributeSource(Set annotationParsers) { - this.publicMethodsOnly = true; - Assert.notEmpty(annotationParsers, "At least one TransactionAnnotationParser needs to be specified"); - this.annotationParsers = annotationParsers; + public void addDefaultRollbackRule(RollbackRuleAttribute rollbackRule) { + if (this.defaultRollbackRules == null) { + this.defaultRollbackRules = new LinkedHashSet<>(); + } + this.defaultRollbackRules.add(rollbackRule); } @@ -175,6 +200,9 @@ public class AnnotationTransactionAttributeSource extends AbstractFallbackTransa for (TransactionAnnotationParser parser : this.annotationParsers) { TransactionAttribute attr = parser.parseTransactionAnnotation(element); if (attr != null) { + if (this.defaultRollbackRules != null && attr instanceof RuleBasedTransactionAttribute ruleAttr) { + ruleAttr.getRollbackRules().addAll(this.defaultRollbackRules); + } return attr; } } @@ -183,6 +211,7 @@ public class AnnotationTransactionAttributeSource extends AbstractFallbackTransa /** * By default, only public methods can be made transactional. + * @see #setPublicMethodsOnly */ @Override protected boolean allowPublicMethodsOnly() { diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/EnableTransactionManagement.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/EnableTransactionManagement.java index 1f4a2db53e..4a4ddf426a 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/EnableTransactionManagement.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/EnableTransactionManagement.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -163,10 +163,10 @@ import org.springframework.core.Ordered; public @interface EnableTransactionManagement { /** - * Indicate whether subclass-based (CGLIB) proxies are to be created ({@code true}) as - * opposed to standard Java interface-based proxies ({@code false}). The default is - * {@code false}. Applicable only if {@link #mode()} is set to - * {@link AdviceMode#PROXY}. + * Indicate whether subclass-based (CGLIB) proxies are to be created ({@code true}) + * as opposed to standard Java interface-based proxies ({@code false}). + * The default is {@code false}. Applicable only if {@link #mode()} + * is set to {@link AdviceMode#PROXY}. *

Note that setting this attribute to {@code true} will affect all * Spring-managed beans requiring proxying, not just those marked with * {@code @Transactional}. For example, other beans marked with Spring's @@ -195,4 +195,25 @@ public @interface EnableTransactionManagement { */ int order() default Ordered.LOWEST_PRECEDENCE; + /** + * Indicate the rollback behavior for rule-based transactions without + * custom rollback rules: default is rollback on unchecked exception, + * this can be switched to rollback on any exception (including checked). + *

Note that transaction-specific rollback rules override the default + * behavior but retain the chosen default for unspecified exceptions. + * This is the case for Spring's {@link Transactional} as well as JTA's + * {@link jakarta.transaction.Transactional} when used with Spring here. + *

Unless you rely on EJB-style business exceptions with commit behavior, + * it is advisable to switch to {@link RollbackOn#ALL_EXCEPTIONS} for a + * consistent rollback even in case of a (potentially accidental) checked + * exception. Also, it is advisable to make that switch for Kotlin-based + * applications where there is no enforcement of checked exceptions at all. + * @since 6.2 + * @see Transactional#rollbackFor() + * @see Transactional#noRollbackFor() + * @see jakarta.transaction.Transactional#rollbackOn() + * @see jakarta.transaction.Transactional#dontRollbackOn() + */ + RollbackOn rollbackOn() default RollbackOn.RUNTIME_EXCEPTIONS; + } diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java index f2128f3aca..3adbbf511e 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -55,13 +55,6 @@ public class ProxyTransactionManagementConfiguration extends AbstractTransaction return advisor; } - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public TransactionAttributeSource transactionAttributeSource() { - // Accept protected @Transactional methods on CGLIB proxies, as of 6.0. - return new AnnotationTransactionAttributeSource(false); - } - @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) { diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/RollbackOn.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/RollbackOn.java new file mode 100644 index 0000000000..46d57c9394 --- /dev/null +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/RollbackOn.java @@ -0,0 +1,49 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://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.annotation; + +/** + * An enum for global rollback-on behavior. + * + *

Note that the default behavior matches the traditional behavior in + * EJB CMT and JTA, with the latter having rollback rules similar to Spring. + * A global switch to trigger a rollback on any exception affects Spring's + * {@link Transactional} as well as {@link jakarta.transaction.Transactional} + * but leaves the non-rule-based {@link jakarta.ejb.TransactionAttribute} as-is. + * + * @author Juergen Hoeller + * @since 6.2 + * @see EnableTransactionManagement#rollbackOn() + * @see org.springframework.transaction.interceptor.RuleBasedTransactionAttribute + */ +public enum RollbackOn { + + /** + * The default rollback-on behavior: rollback on + * {@link RuntimeException RuntimeExceptions} as well as {@link Error Errors}. + * @see org.springframework.transaction.interceptor.RollbackRuleAttribute#ROLLBACK_ON_RUNTIME_EXCEPTIONS + */ + RUNTIME_EXCEPTIONS, + + /** + * The alternative mode: rollback on all exceptions, including any checked + * {@link Exception}. + * @see org.springframework.transaction.interceptor.RollbackRuleAttribute#ROLLBACK_ON_ALL_EXCEPTIONS + */ + ALL_EXCEPTIONS + +} diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionAnnotationParser.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionAnnotationParser.java index 0e9ee83a56..8aa5f2b280 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionAnnotationParser.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionAnnotationParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -60,6 +60,8 @@ public interface TransactionAnnotationParser { * based on an annotation type understood by this parser. *

This essentially parses a known transaction annotation into Spring's metadata * attribute class. Returns {@code null} if the method/class is not transactional. + *

The returned attribute will typically (but not necessarily) be of type + * {@link org.springframework.transaction.interceptor.RuleBasedTransactionAttribute}. * @param element the annotated method or class * @return the configured transaction attribute, or {@code null} if none found * @see AnnotationTransactionAttributeSource#determineTransactionAttribute diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java index 0762a9ae79..2f9e23296d 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.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. @@ -65,6 +65,14 @@ public class RollbackRuleAttribute implements Serializable{ public static final RollbackRuleAttribute ROLLBACK_ON_RUNTIME_EXCEPTIONS = new RollbackRuleAttribute(RuntimeException.class); + /** + * The {@linkplain RollbackRuleAttribute rollback rule} for all + * {@link Exception Exceptions}, including checked exceptions. + * @since 6.2 + */ + public static final RollbackRuleAttribute ROLLBACK_ON_ALL_EXCEPTIONS = + new RollbackRuleAttribute(Exception.class); + /** * Exception pattern: used when searching for matches in a thrown exception's 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 e58cfcf262..49ac8a30d3 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 @@ -46,6 +46,7 @@ import org.springframework.transaction.testfixture.CallCountingTransactionManage import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; +import static org.springframework.transaction.annotation.RollbackOn.ALL_EXCEPTIONS; /** * Tests demonstrating use of @EnableTransactionManagement @Configuration classes. @@ -226,8 +227,8 @@ class EnableTransactionManagementTests { // should throw CNFE when trying to load AnnotationTransactionAspect. // Do you actually have org.springframework.aspects on the classpath? assertThatException() - .isThrownBy(() -> new AnnotationConfigApplicationContext(EnableAspectjTxConfig.class, TxManagerConfig.class)) - .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); + .isThrownBy(() -> new AnnotationConfigApplicationContext(EnableAspectjTxConfig.class, TxManagerConfig.class)) + .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); } @Test @@ -288,8 +289,8 @@ class EnableTransactionManagementTests { } @Test - void gh24502AppliesTransactionOnlyOnAnnotatedInterface() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh24502ConfigA.class); + void gh24502AppliesTransactionFromAnnotatedInterface() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh24502Config.class); Object bean = ctx.getBean("testBean"); CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); @@ -302,6 +303,36 @@ class EnableTransactionManagementTests { ctx.close(); } + @Test + void gh23473AppliesToRuntimeExceptionOnly() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh23473ConfigA.class); + TestServiceWithRollback bean = ctx.getBean("testBean", TestServiceWithRollback.class); + CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); + + assertThatException().isThrownBy(bean::methodOne); + assertThatException().isThrownBy(bean::methodTwo); + assertThat(txManager.begun).isEqualTo(2); + assertThat(txManager.commits).isEqualTo(2); + assertThat(txManager.rollbacks).isEqualTo(0); + + ctx.close(); + } + + @Test + void gh23473AppliesRollbackOnAnyException() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh23473ConfigB.class); + TestServiceWithRollback bean = ctx.getBean("testBean", TestServiceWithRollback.class); + CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); + + assertThatException().isThrownBy(bean::methodOne); + assertThatException().isThrownBy(bean::methodTwo); + assertThat(txManager.begun).isEqualTo(2); + assertThat(txManager.commits).isEqualTo(0); + assertThat(txManager.rollbacks).isEqualTo(2); + + ctx.close(); + } + @Service public static class TransactionalTestBean { @@ -590,7 +621,7 @@ class EnableTransactionManagementTests { @Configuration @EnableTransactionManagement - static class Gh24502ConfigA { + static class Gh24502Config { @Bean public MixedTransactionalTestService testBean() { @@ -603,4 +634,50 @@ class EnableTransactionManagementTests { } } + + static class TestServiceWithRollback { + + @Transactional + public void methodOne() throws Exception { + throw new Exception(); + } + + @Transactional + public void methodTwo() throws Exception { + throw new Exception(); + } + } + + + @Configuration + @EnableTransactionManagement + static class Gh23473ConfigA { + + @Bean + public TestServiceWithRollback testBean() { + return new TestServiceWithRollback(); + } + + @Bean + public PlatformTransactionManager txManager() { + return new CallCountingTransactionManager(); + } + } + + + @Configuration + @EnableTransactionManagement(rollbackOn = ALL_EXCEPTIONS) + static class Gh23473ConfigB { + + @Bean + public TestServiceWithRollback testBean() { + return new TestServiceWithRollback(); + } + + @Bean + public PlatformTransactionManager txManager() { + return new CallCountingTransactionManager(); + } + } + }