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
This commit is contained in:
Juergen Hoeller
2024-03-05 18:08:08 +01:00
parent eb01cc0d9d
commit 7d4c8a403e
12 changed files with 295 additions and 71 deletions

View File

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

View File

@@ -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<TransactionAnnotationParser> annotationParsers;
private boolean publicMethodsOnly = true;
@Nullable
private Set<RollbackRuleAttribute> 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.
* <p>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.
* <p>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<TransactionAnnotationParser> 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() {

View File

@@ -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}. <strong>Applicable only if {@link #mode()} is set to
* {@link AdviceMode#PROXY}</strong>.
* 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}. <strong>Applicable only if {@link #mode()}
* is set to {@link AdviceMode#PROXY}</strong>.
* <p>Note that setting this attribute to {@code true} will affect <em>all</em>
* 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).
* <p>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.
* <p>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;
}

View File

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

View File

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

View File

@@ -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.
* <p>This essentially parses a known transaction annotation into Spring's metadata
* attribute class. Returns {@code null} if the method/class is not transactional.
* <p>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

View File

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