Introduce resolvable timeout attribute on @Transactional and <tx:method>

Placeholders get resolved in timeoutString, qualifier and labels now.

Closes gh-25052
This commit is contained in:
Juergen Hoeller
2020-05-12 21:55:22 +02:00
parent 273d952ddf
commit dd0d0d51f6
14 changed files with 244 additions and 63 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.transaction.interceptor.NoRollbackRuleAttribute;
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.StringUtils;
/**
* Strategy implementation for parsing Spring's {@link Transactional} annotation.
@@ -70,7 +72,13 @@ public class SpringTransactionAnnotationParser implements TransactionAnnotationP
rbta.setPropagationBehavior(propagation.value());
Isolation isolation = attributes.getEnum("isolation");
rbta.setIsolationLevel(isolation.value());
rbta.setTimeout(attributes.getNumber("timeout").intValue());
String timeoutString = attributes.getString("timeoutString");
Assert.isTrue(!StringUtils.hasText(timeoutString) || rbta.getTimeout() < 0,
"Specify 'timeout' or 'timeoutString', not both");
rbta.setTimeoutString(timeoutString);
rbta.setReadOnly(attributes.getBoolean("readOnly"));
rbta.setQualifier(attributes.getString("value"));
rbta.setLabels(Arrays.asList(attributes.getStringArray("label")));

View File

@@ -84,6 +84,18 @@ public @interface Transactional {
@AliasFor("value")
String transactionManager() default "";
/**
* Defines zero (0) or more transaction labels. Labels may be used to
* describe a transaction and they can be evaluated by individual transaction
* manager. Labels may serve a solely descriptive purpose or map to
* pre-defined transaction manager-specific options.
* <p>See the description of the actual transaction manager implementation
* how it evaluates transaction labels.
* @since 5.3
* @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#getLabels()
*/
String[] label() default {};
/**
* The transaction propagation type.
* <p>Defaults to {@link Propagation#REQUIRED}.
@@ -111,10 +123,23 @@ public @interface Transactional {
* <p>Exclusively designed for use with {@link Propagation#REQUIRED} or
* {@link Propagation#REQUIRES_NEW} since it only applies to newly started
* transactions.
* @return the timeout in seconds
* @see org.springframework.transaction.interceptor.TransactionAttribute#getTimeout()
*/
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
/**
* The timeout for this transaction (in seconds).
* <p>Defaults to the default timeout of the underlying transaction system.
* <p>Exclusively designed for use with {@link Propagation#REQUIRED} or
* {@link Propagation#REQUIRES_NEW} since it only applies to newly started
* transactions.
* @return the timeout in seconds as a String value, e.g. a placeholder
* @since 5.3
* @see org.springframework.transaction.interceptor.TransactionAttribute#getTimeout()
*/
String timeoutString() default "";
/**
* A boolean flag that can be set to {@code true} if the transaction is
* effectively read-only, allowing for corresponding optimizations at runtime.
@@ -190,17 +215,4 @@ public @interface Transactional {
*/
String[] noRollbackForClassName() default {};
/**
* Defines zero (0) or more transaction labels. Labels may be used to
* describe a transaction and they can be evaluated by individual transaction
* manager. Labels may serve a solely descriptive purpose or map to
* pre-defined transaction manager-specific options.
* <p>See the description of the actual transaction manager implementation
* how it evaluates transaction labels.
*
* @since 5.3
* @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#getLabels()
*/
String[] label() default {};
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2020 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.
@@ -116,12 +116,7 @@ class TxAdviceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
attribute.setIsolationLevelName(RuleBasedTransactionAttribute.PREFIX_ISOLATION + isolation);
}
if (StringUtils.hasText(timeout)) {
try {
attribute.setTimeout(Integer.parseInt(timeout));
}
catch (NumberFormatException ex) {
parserContext.getReaderContext().error("Timeout must be an integer value: [" + timeout + "]", methodEle);
}
attribute.setTimeoutString(timeout);
}
if (StringUtils.hasText(readOnly)) {
attribute.setReadOnly(Boolean.parseBoolean(methodEle.getAttribute(READ_ONLY_ATTRIBUTE)));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -25,9 +25,11 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringValueResolver;
/**
* Abstract implementation of {@link TransactionAttributeSource} that caches
@@ -49,7 +51,8 @@ import org.springframework.util.ClassUtils;
* @author Juergen Hoeller
* @since 1.1
*/
public abstract class AbstractFallbackTransactionAttributeSource implements TransactionAttributeSource {
public abstract class AbstractFallbackTransactionAttributeSource
implements TransactionAttributeSource, EmbeddedValueResolverAware {
/**
* Canonical value held in cache to indicate no transaction attribute was
@@ -71,6 +74,9 @@ public abstract class AbstractFallbackTransactionAttributeSource implements Tran
*/
protected final Log logger = LogFactory.getLog(getClass());
@Nullable
private transient StringValueResolver embeddedValueResolver;
/**
* Cache of TransactionAttributes, keyed by method on a specific target class.
* <p>As this base class is not marked Serializable, the cache will be recreated
@@ -79,6 +85,12 @@ public abstract class AbstractFallbackTransactionAttributeSource implements Tran
private final Map<Object, TransactionAttribute> attributeCache = new ConcurrentHashMap<>(1024);
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
/**
* Determine the transaction attribute for this method invocation.
* <p>Defaults to the class's transaction attribute if no method attribute is found.
@@ -117,7 +129,9 @@ public abstract class AbstractFallbackTransactionAttributeSource implements Tran
else {
String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass);
if (txAttr instanceof DefaultTransactionAttribute) {
((DefaultTransactionAttribute) txAttr).setDescriptor(methodIdentification);
DefaultTransactionAttribute dta = (DefaultTransactionAttribute) txAttr;
dta.setDescriptor(methodIdentification);
dta.resolveAttributeStrings(this.embeddedValueResolver);
}
if (logger.isTraceEnabled()) {
logger.trace("Adding transactional method '" + methodIdentification + "' with attribute: " + txAttr);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -18,10 +18,13 @@ package org.springframework.transaction.interceptor;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
/**
* Spring's common transaction attribute implementation.
@@ -36,10 +39,13 @@ import org.springframework.util.StringUtils;
public class DefaultTransactionAttribute extends DefaultTransactionDefinition implements TransactionAttribute {
@Nullable
private String qualifier;
private String descriptor;
@Nullable
private String descriptor;
private String timeoutString;
@Nullable
private String qualifier;
private Collection<String> labels = Collections.emptyList();
@@ -83,11 +89,54 @@ public class DefaultTransactionAttribute extends DefaultTransactionDefinition im
}
/**
* Set a descriptor for this transaction attribute,
* e.g. indicating where the attribute is applying.
* @since 4.3.4
*/
public void setDescriptor(@Nullable String descriptor) {
this.descriptor = descriptor;
}
/**
* Return a descriptor for this transaction attribute,
* or {@code null} if none.
* @since 4.3.4
*/
@Nullable
public String getDescriptor() {
return this.descriptor;
}
/**
* Set the timeout to apply, if any,
* as a String value that resolves to a number of seconds.
* @since 5.3
* @see #setTimeout
* @see #resolveAttributeStrings
*/
public void setTimeoutString(@Nullable String timeoutString) {
this.timeoutString = timeoutString;
}
/**
* Return the timeout to apply, if any,
* as a String value that resolves to a number of seconds.
* @since 5.3
* @see #getTimeout
* @see #resolveAttributeStrings
*/
@Nullable
public String getTimeoutString() {
return this.timeoutString;
}
/**
* Associate a qualifier value with this transaction attribute.
* <p>This may be used for choosing a corresponding transaction manager
* to process this specific transaction.
* @since 3.0
* @see #resolveAttributeStrings
*/
public void setQualifier(@Nullable String qualifier) {
this.qualifier = qualifier;
@@ -108,6 +157,7 @@ public class DefaultTransactionAttribute extends DefaultTransactionDefinition im
* <p>This may be used for applying specific transactional behavior
* or follow a purely descriptive nature.
* @since 5.3
* @see #resolveAttributeStrings
*/
public void setLabels(Collection<String> labels) {
this.labels = labels;
@@ -118,25 +168,6 @@ public class DefaultTransactionAttribute extends DefaultTransactionDefinition im
return this.labels;
}
/**
* Set a descriptor for this transaction attribute,
* e.g. indicating where the attribute is applying.
* @since 4.3.4
*/
public void setDescriptor(@Nullable String descriptor) {
this.descriptor = descriptor;
}
/**
* Return a descriptor for this transaction attribute,
* or {@code null} if none.
* @since 4.3.4
*/
@Nullable
public String getDescriptor() {
return this.descriptor;
}
/**
* The default behavior is as with EJB: rollback on unchecked exception
* ({@link RuntimeException}), assuming an unexpected outcome outside of any
@@ -157,6 +188,42 @@ public class DefaultTransactionAttribute extends DefaultTransactionDefinition im
}
/**
* Resolve attribute values that are defined as resolvable Strings:
* {@link #setTimeoutString}, {@link #setQualifier}, {@link #setLabels}.
* This is typically used for resolving "${...}" placeholders.
* @param resolver the embedded value resolver to apply, if any
* @since 5.3
*/
public void resolveAttributeStrings(@Nullable StringValueResolver resolver) {
String timeoutString = this.timeoutString;
if (StringUtils.hasText(timeoutString)) {
if (resolver != null) {
timeoutString = resolver.resolveStringValue(timeoutString);
}
if (StringUtils.hasLength(timeoutString)) {
try {
setTimeout(Integer.parseInt(timeoutString));
}
catch (RuntimeException ex) {
throw new IllegalArgumentException(
"Invalid timeoutString value \"" + timeoutString + "\" - cannot parse into int");
}
}
}
if (resolver != null) {
if (this.qualifier != null) {
this.qualifier = resolver.resolveStringValue(this.qualifier);
}
Set<String> resolvedLabels = new LinkedHashSet<>(this.labels.size());
for (String label : this.labels) {
resolvedLabels.add(resolver.resolveStringValue(label));
}
this.labels = resolvedLabels;
}
}
/**
* Return an identifying description for this transaction attribute.
* <p>Available to subclasses, for inclusion in their {@code toString()} result.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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,7 @@ import org.springframework.util.ObjectUtils;
* methods being handled by a transaction interceptor.
*
* @author Colin Sampaleanu
* @author Juergen Hoeller
* @since 15.10.2003
* @see org.springframework.transaction.interceptor.TransactionProxyFactoryBean
* @see org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator
@@ -48,6 +49,9 @@ public class MatchAlwaysTransactionAttributeSource implements TransactionAttribu
* @see org.springframework.transaction.interceptor.TransactionAttributeEditor
*/
public void setTransactionAttribute(TransactionAttribute transactionAttribute) {
if (transactionAttribute instanceof DefaultTransactionAttribute) {
((DefaultTransactionAttribute) transactionAttribute).resolveAttributeStrings(null);
}
this.transactionAttribute = transactionAttribute;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -27,11 +27,13 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringValueResolver;
/**
* Simple {@link TransactionAttributeSource} implementation that
@@ -44,7 +46,7 @@ import org.springframework.util.PatternMatchUtils;
* @see NameMatchTransactionAttributeSource
*/
public class MethodMapTransactionAttributeSource
implements TransactionAttributeSource, BeanClassLoaderAware, InitializingBean {
implements TransactionAttributeSource, EmbeddedValueResolverAware, BeanClassLoaderAware, InitializingBean {
/** Logger available to subclasses. */
protected final Log logger = LogFactory.getLog(getClass());
@@ -53,6 +55,9 @@ public class MethodMapTransactionAttributeSource
@Nullable
private Map<String, TransactionAttribute> methodMap;
@Nullable
private StringValueResolver embeddedValueResolver;
@Nullable
private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
@@ -83,6 +88,11 @@ public class MethodMapTransactionAttributeSource
this.methodMap = methodMap;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
@Override
public void setBeanClassLoader(ClassLoader beanClassLoader) {
this.beanClassLoader = beanClassLoader;
@@ -189,6 +199,9 @@ public class MethodMapTransactionAttributeSource
if (logger.isDebugEnabled()) {
logger.debug("Adding transactional method [" + method + "] with attribute [" + attr + "]");
}
if (this.embeddedValueResolver != null && attr instanceof DefaultTransactionAttribute) {
((DefaultTransactionAttribute) attr).resolveAttributeStrings(this.embeddedValueResolver);
}
this.transactionAttributeMap.put(method, attr);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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,10 +26,13 @@ import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringValueResolver;
/**
* Simple {@link TransactionAttributeSource} implementation that
@@ -41,7 +44,8 @@ import org.springframework.util.PatternMatchUtils;
* @see MethodMapTransactionAttributeSource
*/
@SuppressWarnings("serial")
public class NameMatchTransactionAttributeSource implements TransactionAttributeSource, Serializable {
public class NameMatchTransactionAttributeSource
implements TransactionAttributeSource, EmbeddedValueResolverAware, InitializingBean, Serializable {
/**
* Logger available to subclasses.
@@ -50,7 +54,10 @@ public class NameMatchTransactionAttributeSource implements TransactionAttribute
protected static final Log logger = LogFactory.getLog(NameMatchTransactionAttributeSource.class);
/** Keys are method names; values are TransactionAttributes. */
private Map<String, TransactionAttribute> nameMap = new HashMap<>();
private final Map<String, TransactionAttribute> nameMap = new HashMap<>();
@Nullable
private StringValueResolver embeddedValueResolver;
/**
@@ -94,9 +101,26 @@ public class NameMatchTransactionAttributeSource implements TransactionAttribute
if (logger.isDebugEnabled()) {
logger.debug("Adding transactional method [" + methodName + "] with attribute [" + attr + "]");
}
if (this.embeddedValueResolver != null && attr instanceof DefaultTransactionAttribute) {
((DefaultTransactionAttribute) attr).resolveAttributeStrings(this.embeddedValueResolver);
}
this.nameMap.put(methodName, attr);
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
@Override
public void afterPropertiesSet() {
for (TransactionAttribute attr : this.nameMap.values()) {
if (attr instanceof DefaultTransactionAttribute) {
((DefaultTransactionAttribute) attr).resolveAttributeStrings(this.embeddedValueResolver);
}
}
}
@Override
@Nullable

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -69,7 +69,7 @@ public class TransactionAttributeEditor extends PropertyEditorSupport {
}
else if (trimmedToken.startsWith(RuleBasedTransactionAttribute.PREFIX_TIMEOUT)) {
String value = trimmedToken.substring(DefaultTransactionAttribute.PREFIX_TIMEOUT.length());
attr.setTimeout(Integer.parseInt(value));
attr.setTimeoutString(value);
}
else if (trimmedToken.equals(RuleBasedTransactionAttribute.READ_ONLY_MARKER)) {
attr.setReadOnly(true);
@@ -84,6 +84,7 @@ public class TransactionAttributeEditor extends PropertyEditorSupport {
throw new IllegalArgumentException("Invalid transaction attribute token: [" + trimmedToken + "]");
}
}
attr.resolveAttributeStrings(null); // placeholders expected to be pre-resolved
setValue(attr);
}
else {