From 3d7ef3ebfc5be645730e2e1d601ccf5799bdf7cd Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 4 Mar 2024 12:55:33 +0100 Subject: [PATCH] Avoid storage of null marker per method for proxy decision purposes Includes missing isCandidateClass support on JCacheOperationSource. Closes gh-20072 --- ...AbstractFallbackJCacheOperationSource.java | 42 ++++--- .../AnnotationJCacheOperationSource.java | 14 ++- ...anFactoryJCacheOperationSourceAdvisor.java | 42 +------ .../interceptor/JCacheOperationSource.java | 36 +++++- .../JCacheOperationSourcePointcut.java | 110 ++++++++++++++++++ .../AbstractFallbackCacheOperationSource.java | 81 +++++++------ .../interceptor/CacheOperationSource.java | 18 ++- .../CacheOperationSourcePointcut.java | 17 ++- ...actFallbackTransactionAttributeSource.java | 50 ++++---- .../TransactionAttributeSource.java | 17 ++- .../TransactionAttributeSourcePointcut.java | 12 +- 11 files changed, 303 insertions(+), 136 deletions(-) create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java index 8b20e4b1482..0ff896eb5e3 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.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. @@ -27,14 +27,13 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.support.AopUtils; import org.springframework.core.MethodClassKey; import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; /** - * Abstract implementation of {@link JCacheOperationSource} that caches attributes + * Abstract implementation of {@link JCacheOperationSource} that caches operations * for methods and implements a fallback policy: 1. specific target method; * 2. declaring method. * - *

This implementation caches attributes by method after they are first used. - * * @author Stephane Nicoll * @author Juergen Hoeller * @since 4.1 @@ -43,24 +42,39 @@ import org.springframework.lang.Nullable; public abstract class AbstractFallbackJCacheOperationSource implements JCacheOperationSource { /** - * Canonical value held in cache to indicate no caching attribute was - * found for this method and we don't need to look again. + * Canonical value held in cache to indicate no cache operation was + * found for this method, and we don't need to look again. */ - private static final Object NULL_CACHING_ATTRIBUTE = new Object(); + private static final Object NULL_CACHING_MARKER = new Object(); protected final Log logger = LogFactory.getLog(getClass()); - private final Map cache = new ConcurrentHashMap<>(1024); + private final Map operationCache = new ConcurrentHashMap<>(1024); @Override + public boolean hasCacheOperation(Method method, @Nullable Class targetClass) { + return (getCacheOperation(method, targetClass, false) != null); + } + + @Override + @Nullable public JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass) { + return getCacheOperation(method, targetClass, true); + } + + @Nullable + private JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass, boolean cacheNull) { + if (ReflectionUtils.isObjectMethod(method)) { + return null; + } + MethodClassKey cacheKey = new MethodClassKey(method, targetClass); - Object cached = this.cache.get(cacheKey); + Object cached = this.operationCache.get(cacheKey); if (cached != null) { - return (cached != NULL_CACHING_ATTRIBUTE ? (JCacheOperation) cached : null); + return (cached != NULL_CACHING_MARKER ? (JCacheOperation) cached : null); } else { JCacheOperation operation = computeCacheOperation(method, targetClass); @@ -68,10 +82,10 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOpe if (logger.isDebugEnabled()) { logger.debug("Adding cacheable method '" + method.getName() + "' with operation: " + operation); } - this.cache.put(cacheKey, operation); + this.operationCache.put(cacheKey, operation); } - else { - this.cache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + else if (cacheNull) { + this.operationCache.put(cacheKey, NULL_CACHING_MARKER); } return operation; } @@ -84,7 +98,7 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOpe return null; } - // The method may be on an interface, but we need attributes from the target class. + // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java index b289b14715f..d250e220b2b 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -20,6 +20,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.Set; import javax.cache.annotation.CacheDefaults; import javax.cache.annotation.CacheKeyGenerator; @@ -32,6 +33,7 @@ import javax.cache.annotation.CacheResult; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; @@ -41,10 +43,20 @@ import org.springframework.util.StringUtils; * {@link CacheRemoveAll} annotations. * * @author Stephane Nicoll + * @author Juergen Hoeller * @since 4.1 */ public abstract class AnnotationJCacheOperationSource extends AbstractFallbackJCacheOperationSource { + private static final Set> JCACHE_OPERATION_ANNOTATIONS = + Set.of(CacheResult.class, CachePut.class, CacheRemove.class, CacheRemoveAll.class); + + + @Override + public boolean isCandidateClass(Class targetClass) { + return AnnotationUtils.isCandidateClass(targetClass, JCACHE_OPERATION_ANNOTATIONS); + } + @Override protected JCacheOperation findCacheOperation(Method method, @Nullable Class targetType) { CacheResult cacheResult = method.getAnnotation(CacheResult.class); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java index 51fda366b04..f6c1739340a 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.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. @@ -16,15 +16,9 @@ package org.springframework.cache.jcache.interceptor; -import java.io.Serializable; -import java.lang.reflect.Method; - import org.springframework.aop.ClassFilter; import org.springframework.aop.Pointcut; import org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor; -import org.springframework.aop.support.StaticMethodMatcherPointcut; -import org.springframework.lang.Nullable; -import org.springframework.util.ObjectUtils; /** * Advisor driven by a {@link JCacheOperationSource}, used to include a @@ -46,6 +40,7 @@ public class BeanFactoryJCacheOperationSourceAdvisor extends AbstractBeanFactory * Set the cache operation attribute source which is used to find cache * attributes. This should usually be identical to the source reference * set on the cache interceptor itself. + * @see JCacheInterceptor#setCacheOperationSource */ public void setCacheOperationSource(JCacheOperationSource cacheOperationSource) { this.pointcut.setCacheOperationSource(cacheOperationSource); @@ -64,37 +59,4 @@ public class BeanFactoryJCacheOperationSourceAdvisor extends AbstractBeanFactory return this.pointcut; } - - private static class JCacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { - - @Nullable - private JCacheOperationSource cacheOperationSource; - - public void setCacheOperationSource(@Nullable JCacheOperationSource cacheOperationSource) { - this.cacheOperationSource = cacheOperationSource; - } - - @Override - public boolean matches(Method method, Class targetClass) { - return (this.cacheOperationSource == null || - this.cacheOperationSource.getCacheOperation(method, targetClass) != null); - } - - @Override - public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof JCacheOperationSourcePointcut that && - ObjectUtils.nullSafeEquals(this.cacheOperationSource, that.cacheOperationSource))); - } - - @Override - public int hashCode() { - return JCacheOperationSourcePointcut.class.hashCode(); - } - - @Override - public String toString() { - return getClass().getName() + ": " + this.cacheOperationSource; - } - } - } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java index 445a7ef8282..686066c6028 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -25,16 +25,48 @@ import org.springframework.lang.Nullable; * cache operation attributes from standard JSR-107 annotations. * * @author Stephane Nicoll + * @author Juergen Hoeller * @since 4.1 * @see org.springframework.cache.interceptor.CacheOperationSource */ public interface JCacheOperationSource { + /** + * Determine whether the given class is a candidate for cache operations + * in the metadata format of this {@code JCacheOperationSource}. + *

If this method returns {@code false}, the methods on the given class + * will not get traversed for {@link #getCacheOperation} introspection. + * Returning {@code false} is therefore an optimization for non-affected + * classes, whereas {@code true} simply means that the class needs to get + * fully introspected for each method on the given class individually. + * @param targetClass the class to introspect + * @return {@code false} if the class is known to have no cache operation + * metadata at class or method level; {@code true} otherwise. The default + * implementation returns {@code true}, leading to regular introspection. + * @since 6.2 + * @see #hasCacheOperation + */ + default boolean isCandidateClass(Class targetClass) { + return true; + } + + /** + * Determine whether there is a JSR-107 cache operation for the given method. + * @param method the method to introspect + * @param targetClass the target class (can be {@code null}, in which case + * the declaring class of the method must be used) + * @since 6.2 + * @see #getCacheOperation + */ + default boolean hasCacheOperation(Method method, @Nullable Class targetClass) { + return (getCacheOperation(method, targetClass) != null); + } + /** * Return the cache operations for this method, or {@code null} * if the method contains no JSR-107 related metadata. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, in which case + * @param targetClass the target class (can be {@code null}, in which case * the declaring class of the method must be used) * @return the cache operation for this method, or {@code null} if none found */ diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java new file mode 100644 index 00000000000..bfd4bda19d9 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java @@ -0,0 +1,110 @@ +/* + * 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.cache.jcache.interceptor; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * A {@code Pointcut} that matches if the underlying {@link JCacheOperationSource} + * has an operation for a given method. + * + * @author Juergen Hoeller + * @since 6.2 + */ +@SuppressWarnings("serial") +final class JCacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { + + @Nullable + private JCacheOperationSource cacheOperationSource; + + + public JCacheOperationSourcePointcut() { + setClassFilter(new JCacheOperationSourceClassFilter()); + } + + + public void setCacheOperationSource(@Nullable JCacheOperationSource cacheOperationSource) { + this.cacheOperationSource = cacheOperationSource; + } + + @Override + public boolean matches(Method method, Class targetClass) { + return (this.cacheOperationSource == null || + this.cacheOperationSource.hasCacheOperation(method, targetClass)); + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof JCacheOperationSourcePointcut that && + ObjectUtils.nullSafeEquals(this.cacheOperationSource, that.cacheOperationSource))); + } + + @Override + public int hashCode() { + return JCacheOperationSourcePointcut.class.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.cacheOperationSource; + } + + + /** + * {@link ClassFilter} that delegates to {@link JCacheOperationSource#isCandidateClass} + * for filtering classes whose methods are not worth searching to begin with. + */ + private final class JCacheOperationSourceClassFilter implements ClassFilter { + + @Override + public boolean matches(Class clazz) { + if (CacheManager.class.isAssignableFrom(clazz)) { + return false; + } + return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz)); + } + + @Nullable + private JCacheOperationSource getCacheOperationSource() { + return cacheOperationSource; + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof JCacheOperationSourceClassFilter that && + ObjectUtils.nullSafeEquals(getCacheOperationSource(), that.getCacheOperationSource()))); + } + + @Override + public int hashCode() { + return JCacheOperationSourceClassFilter.class.hashCode(); + } + + @Override + public String toString() { + return JCacheOperationSourceClassFilter.class.getName() + ": " + getCacheOperationSource(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java index d20993ae27a..2657232886c 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.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. @@ -30,22 +30,20 @@ import org.springframework.aop.support.AopUtils; import org.springframework.core.MethodClassKey; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; /** - * Abstract implementation of {@link CacheOperation} that caches attributes + * Abstract implementation of {@link CacheOperationSource} that caches operations * for methods and implements a fallback policy: 1. specific target method; * 2. target class; 3. declaring method; 4. declaring class/interface. * - *

Defaults to using the target class's caching attribute if none is - * associated with the target method. Any caching attribute associated with - * the target method completely overrides a class caching attribute. + *

Defaults to using the target class's declared cache operations if none are + * associated with the target method. Any cache operations associated with + * the target method completely override any class-level declarations. * If none found on the target class, the interface that the invoked method * has been called through (in case of a JDK proxy) will be checked. * - *

This implementation caches attributes by method after they are first - * used. If it is ever desirable to allow dynamic changing of cacheable - * attributes (which is very unlikely), caching could be made configurable. - * * @author Costin Leau * @author Juergen Hoeller * @since 3.1 @@ -53,10 +51,10 @@ import org.springframework.util.ClassUtils; public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource { /** - * Canonical value held in cache to indicate no caching attribute was - * found for this method and we don't need to look again. + * Canonical value held in cache to indicate no cache operation was + * found for this method, and we don't need to look again. */ - private static final Collection NULL_CACHING_ATTRIBUTE = Collections.emptyList(); + private static final Collection NULL_CACHING_MARKER = Collections.emptyList(); /** @@ -71,40 +69,53 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera *

As this base class is not marked Serializable, the cache will be recreated * after serialization - provided that the concrete subclass is Serializable. */ - private final Map> attributeCache = new ConcurrentHashMap<>(1024); + private final Map> operationCache = new ConcurrentHashMap<>(1024); - /** - * Determine the caching attribute for this method invocation. - *

Defaults to the class's caching attribute if no method attribute is found. - * @param method the method for the current invocation (never {@code null}) - * @param targetClass the target class for this invocation (may be {@code null}) - * @return {@link CacheOperation} for this method, or {@code null} if the method - * is not cacheable - */ + @Override + public boolean hasCacheOperations(Method method, @Nullable Class targetClass) { + return !CollectionUtils.isEmpty(getCacheOperations(method, targetClass, false)); + } + @Override @Nullable public Collection getCacheOperations(Method method, @Nullable Class targetClass) { - if (method.getDeclaringClass() == Object.class) { + return getCacheOperations(method, targetClass, true); + } + + /** + * Determine the cache operations for this method invocation. + *

Defaults to class-declared metadata if no method-level metadata is found. + * @param method the method for the current invocation (never {@code null}) + * @param targetClass the target class for this invocation (can be {@code null}) + * @param cacheNull whether {@code null} results should be cached as well + * @return {@link CacheOperation} for this method, or {@code null} if the method + * is not cacheable + */ + @Nullable + private Collection getCacheOperations( + Method method, @Nullable Class targetClass, boolean cacheNull) { + + if (ReflectionUtils.isObjectMethod(method)) { return null; } Object cacheKey = getCacheKey(method, targetClass); - Collection cached = this.attributeCache.get(cacheKey); + Collection cached = this.operationCache.get(cacheKey); if (cached != null) { - return (cached != NULL_CACHING_ATTRIBUTE ? cached : null); + return (cached != NULL_CACHING_MARKER ? cached : null); } else { Collection cacheOps = computeCacheOperations(method, targetClass); if (cacheOps != null) { if (logger.isTraceEnabled()) { - logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps); + logger.trace("Adding cacheable method '" + method.getName() + "' with operations: " + cacheOps); } - this.attributeCache.put(cacheKey, cacheOps); + this.operationCache.put(cacheKey, cacheOps); } - else { - this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + else if (cacheNull) { + this.operationCache.put(cacheKey, NULL_CACHING_MARKER); } return cacheOps; } @@ -129,7 +140,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera return null; } - // The method may be on an interface, but we need attributes from the target class. + // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); @@ -163,19 +174,19 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera /** - * Subclasses need to implement this to return the caching attribute for the + * Subclasses need to implement this to return the cache operations for the * given class, if any. - * @param clazz the class to retrieve the attribute for - * @return all caching attribute associated with this class, or {@code null} if none + * @param clazz the class to retrieve the cache operations for + * @return all cache operations associated with this class, or {@code null} if none */ @Nullable protected abstract Collection findCacheOperations(Class clazz); /** - * Subclasses need to implement this to return the caching attribute for the + * Subclasses need to implement this to return the cache operations for the * given method, if any. - * @param method the method to retrieve the attribute for - * @return all caching attribute associated with this method, or {@code null} if none + * @param method the method to retrieve the cache operations for + * @return all cache operations associated with this method, or {@code null} if none */ @Nullable protected abstract Collection findCacheOperations(Method method); diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java index 02a9b4f4164..601fd4dbb4c 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.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. @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import java.util.Collection; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; /** * Interface used by {@link CacheInterceptor}. Implementations know how to source @@ -45,16 +46,29 @@ public interface CacheOperationSource { * metadata at class or method level; {@code true} otherwise. The default * implementation returns {@code true}, leading to regular introspection. * @since 5.2 + * @see #hasCacheOperations */ default boolean isCandidateClass(Class targetClass) { return true; } + /** + * Determine whether there are cache operations for the given method. + * @param method the method to introspect + * @param targetClass the target class (can be {@code null}, + * in which case the declaring class of the method must be used) + * @since 6.2 + * @see #getCacheOperations + */ + default boolean hasCacheOperations(Method method, @Nullable Class targetClass) { + return !CollectionUtils.isEmpty(getCacheOperations(method, targetClass)); + } + /** * Return the collection of cache operations for this method, * or {@code null} if the method contains no cacheable annotations. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, in which case + * @param targetClass the target class (can be {@code null}, in which case * the declaring class of the method must be used) * @return all cache operations for this method, or {@code null} if none found */ diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java index e70275aeaed..4b9054b10ed 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.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. @@ -23,12 +23,11 @@ import org.springframework.aop.ClassFilter; import org.springframework.aop.support.StaticMethodMatcherPointcut; import org.springframework.cache.CacheManager; import org.springframework.lang.Nullable; -import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; /** * A {@code Pointcut} that matches if the underlying {@link CacheOperationSource} - * has an attribute for a given method. + * has an operation for a given method. * * @author Costin Leau * @author Juergen Hoeller @@ -36,7 +35,7 @@ import org.springframework.util.ObjectUtils; * @since 3.1 */ @SuppressWarnings("serial") -class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { +final class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { @Nullable private CacheOperationSource cacheOperationSource; @@ -54,7 +53,7 @@ class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implement @Override public boolean matches(Method method, Class targetClass) { return (this.cacheOperationSource == null || - !CollectionUtils.isEmpty(this.cacheOperationSource.getCacheOperations(method, targetClass))); + this.cacheOperationSource.hasCacheOperations(method, targetClass)); } @Override @@ -78,7 +77,7 @@ class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implement * {@link ClassFilter} that delegates to {@link CacheOperationSource#isCandidateClass} * for filtering classes whose methods are not worth searching to begin with. */ - private class CacheOperationSourceClassFilter implements ClassFilter { + private final class CacheOperationSourceClassFilter implements ClassFilter { @Override public boolean matches(Class clazz) { @@ -88,6 +87,7 @@ class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implement return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz)); } + @Nullable private CacheOperationSource getCacheOperationSource() { return cacheOperationSource; } @@ -95,7 +95,7 @@ class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implement @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof CacheOperationSourceClassFilter that && - ObjectUtils.nullSafeEquals(cacheOperationSource, that.getCacheOperationSource()))); + ObjectUtils.nullSafeEquals(getCacheOperationSource(), that.getCacheOperationSource()))); } @Override @@ -105,9 +105,8 @@ class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implement @Override public String toString() { - return CacheOperationSourceClassFilter.class.getName() + ": " + cacheOperationSource; + return CacheOperationSourceClassFilter.class.getName() + ": " + getCacheOperationSource(); } - } } diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java index e669818c362..829680eae2f 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.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. @@ -29,6 +29,7 @@ import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.core.MethodClassKey; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringValueResolver; /** @@ -42,11 +43,6 @@ import org.springframework.util.StringValueResolver; * If none found on the target class, the interface that the invoked method * has been called through (in case of a JDK proxy) will be checked. * - *

This implementation caches attributes by method after they are first used. - * If it is ever desirable to allow dynamic changing of transaction attributes - * (which is very unlikely), caching could be made configurable. Caching is - * desirable because of the cost of evaluating rollback rules. - * * @author Rod Johnson * @author Juergen Hoeller * @since 1.1 @@ -91,42 +87,43 @@ public abstract class AbstractFallbackTransactionAttributeSource } + @Override + public boolean hasTransactionAttribute(Method method, @Nullable Class targetClass) { + return (getTransactionAttribute(method, targetClass, false) != null); + } + + @Override + @Nullable + public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class targetClass) { + return getTransactionAttribute(method, targetClass, true); + } + /** * Determine the transaction attribute for this method invocation. *

Defaults to the class's transaction attribute if no method attribute is found. * @param method the method for the current invocation (never {@code null}) - * @param targetClass the target class for this invocation (may be {@code null}) + * @param targetClass the target class for this invocation (can be {@code null}) + * @param cacheNull whether {@code null} results should be cached as well * @return a TransactionAttribute for this method, or {@code null} if the method * is not transactional */ - @Override @Nullable - public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class targetClass) { - if (method.getDeclaringClass() == Object.class) { + private TransactionAttribute getTransactionAttribute( + Method method, @Nullable Class targetClass, boolean cacheNull) { + + if (ReflectionUtils.isObjectMethod(method)) { return null; } - // First, see if we have a cached value. Object cacheKey = getCacheKey(method, targetClass); TransactionAttribute cached = this.attributeCache.get(cacheKey); + if (cached != null) { - // Value will either be canonical value indicating there is no transaction attribute, - // or an actual transaction attribute. - if (cached == NULL_TRANSACTION_ATTRIBUTE) { - return null; - } - else { - return cached; - } + return (cached != NULL_TRANSACTION_ATTRIBUTE ? cached : null); } else { - // We need to work it out. TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass); - // Put it in the cache. - if (txAttr == null) { - this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); - } - else { + if (txAttr != null) { String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass); if (txAttr instanceof DefaultTransactionAttribute dta) { dta.setDescriptor(methodIdentification); @@ -137,6 +134,9 @@ public abstract class AbstractFallbackTransactionAttributeSource } this.attributeCache.put(cacheKey, txAttr); } + else if (cacheNull) { + this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); + } return txAttr; } } diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.java index 329f5342052..8514092f4f0 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.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. @@ -48,16 +48,29 @@ public interface TransactionAttributeSource { * attributes at class or method level; {@code true} otherwise. The default * implementation returns {@code true}, leading to regular introspection. * @since 5.2 + * @see #hasTransactionAttribute */ default boolean isCandidateClass(Class targetClass) { return true; } + /** + * Determine whether there is a transaction attribute for the given method. + * @param method the method to introspect + * @param targetClass the target class (can be {@code null}, + * in which case the declaring class of the method must be used) + * @since 6.2 + * @see #getTransactionAttribute + */ + default boolean hasTransactionAttribute(Method method, @Nullable Class targetClass) { + return (getTransactionAttribute(method, targetClass) != null); + } + /** * Return the transaction attribute for the given method, * or {@code null} if the method is non-transactional. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, + * @param targetClass the target class (can be {@code null}, * in which case the declaring class of the method must be used) * @return the matching transaction attribute, or {@code null} if none found */ diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java index 10ac08147ae..319715b4562 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.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. @@ -53,7 +53,7 @@ final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointc @Override public boolean matches(Method method, Class targetClass) { return (this.transactionAttributeSource == null || - this.transactionAttributeSource.getTransactionAttribute(method, targetClass) != null); + this.transactionAttributeSource.hasTransactionAttribute(method, targetClass)); } @Override @@ -77,7 +77,7 @@ final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointc * {@link ClassFilter} that delegates to {@link TransactionAttributeSource#isCandidateClass} * for filtering classes whose methods are not worth searching to begin with. */ - private class TransactionAttributeSourceClassFilter implements ClassFilter { + private final class TransactionAttributeSourceClassFilter implements ClassFilter { @Override public boolean matches(Class clazz) { @@ -89,6 +89,7 @@ final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointc return (transactionAttributeSource == null || transactionAttributeSource.isCandidateClass(clazz)); } + @Nullable private TransactionAttributeSource getTransactionAttributeSource() { return transactionAttributeSource; } @@ -96,7 +97,7 @@ final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointc @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof TransactionAttributeSourceClassFilter that && - ObjectUtils.nullSafeEquals(transactionAttributeSource, that.getTransactionAttributeSource()))); + ObjectUtils.nullSafeEquals(getTransactionAttributeSource(), that.getTransactionAttributeSource()))); } @Override @@ -106,9 +107,8 @@ final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointc @Override public String toString() { - return TransactionAttributeSourceClassFilter.class.getName() + ": " + transactionAttributeSource; + return TransactionAttributeSourceClassFilter.class.getName() + ": " + getTransactionAttributeSource(); } - } }