diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index f409f90ee7a..b1b196243bf 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -109,11 +109,11 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut private @Nullable BeanFactory beanFactory; - private transient @Nullable ClassLoader pointcutClassLoader; + private transient volatile @Nullable ClassLoader pointcutClassLoader; - private transient @Nullable PointcutExpression pointcutExpression; + private transient volatile @Nullable PointcutExpression pointcutExpression; - private transient boolean pointcutParsingFailed = false; + private transient volatile boolean pointcutParsingFailed; /** @@ -193,11 +193,14 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut * Lazily build the underlying AspectJ pointcut expression. */ private PointcutExpression obtainPointcutExpression() { - if (this.pointcutExpression == null) { - this.pointcutClassLoader = determinePointcutClassLoader(); - this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader); + PointcutExpression pointcutExpression = this.pointcutExpression; + if (pointcutExpression == null) { + ClassLoader pointcutClassLoader = determinePointcutClassLoader(); + pointcutExpression = buildPointcutExpression(pointcutClassLoader); + this.pointcutClassLoader = pointcutClassLoader; + this.pointcutExpression = pointcutExpression; } - return this.pointcutExpression; + return pointcutExpression; } /** @@ -460,40 +463,24 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut } private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) { - ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(this, targetMethod); + ShadowMatchKey key = new ShadowMatchKey(this, targetMethod); + ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(key); if (shadowMatch == null) { - PointcutExpression fallbackExpression = null; - Method methodToMatch = targetMethod; - try { + PointcutExpression pointcutExpression = obtainPointcutExpression(); + synchronized (pointcutExpression) { + shadowMatch = ShadowMatchUtils.getShadowMatch(key); + if (shadowMatch != null) { + return shadowMatch; + } + PointcutExpression fallbackExpression = null; + Method methodToMatch = targetMethod; try { - shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); - } - catch (ReflectionWorldException ex) { - // Failed to introspect target method, probably because it has been loaded - // in a special ClassLoader. Let's try the declaring ClassLoader instead... try { - fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); - if (fallbackExpression != null) { - shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); - } - } - catch (ReflectionWorldException ex2) { - fallbackExpression = null; - } - } - if (targetMethod != originalMethod && (shadowMatch == null || - (Proxy.isProxyClass(targetMethod.getDeclaringClass()) && - (shadowMatch.neverMatches() || containsAnnotationPointcut())))) { - // Fall back to the plain original method in case of no resolvable match or a - // negative match on a proxy class (which doesn't carry any annotations on its - // redeclared methods), as well as for annotation pointcuts. - methodToMatch = originalMethod; - try { - shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); + shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch); } catch (ReflectionWorldException ex) { - // Could neither introspect the target class nor the proxy class -> - // let's try the original method's declaring class before we give up... + // Failed to introspect target method, probably because it has been loaded + // in a special ClassLoader. Let's try the declaring ClassLoader instead... try { fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); if (fallbackExpression != null) { @@ -504,21 +491,45 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut fallbackExpression = null; } } + if (targetMethod != originalMethod && (shadowMatch == null || + (Proxy.isProxyClass(targetMethod.getDeclaringClass()) && + (shadowMatch.neverMatches() || containsAnnotationPointcut())))) { + // Fall back to the plain original method in case of no resolvable match or a + // negative match on a proxy class (which doesn't carry any annotations on its + // redeclared methods), as well as for annotation pointcuts. + methodToMatch = originalMethod; + try { + shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch); + } + catch (ReflectionWorldException ex) { + // Could neither introspect the target class nor the proxy class -> + // let's try the original method's declaring class before we give up... + try { + fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); + if (fallbackExpression != null) { + shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); + } + } + catch (ReflectionWorldException ex2) { + fallbackExpression = null; + } + } + } } + catch (Throwable ex) { + // Possibly AspectJ 1.8.10 encountering an invalid signature + logger.debug("PointcutExpression matching rejected target method", ex); + fallbackExpression = null; + } + if (shadowMatch == null) { + shadowMatch = new ShadowMatchImpl(org.aspectj.util.FuzzyBoolean.NO, null, null, null); + } + else if (shadowMatch.maybeMatches() && fallbackExpression != null) { + shadowMatch = new DefensiveShadowMatch(shadowMatch, + fallbackExpression.matchesMethodExecution(methodToMatch)); + } + shadowMatch = ShadowMatchUtils.setShadowMatch(key, shadowMatch); } - catch (Throwable ex) { - // Possibly AspectJ 1.8.10 encountering an invalid signature - logger.debug("PointcutExpression matching rejected target method", ex); - fallbackExpression = null; - } - if (shadowMatch == null) { - shadowMatch = new ShadowMatchImpl(org.aspectj.util.FuzzyBoolean.NO, null, null, null); - } - else if (shadowMatch.maybeMatches() && fallbackExpression != null) { - shadowMatch = new DefensiveShadowMatch(shadowMatch, - fallbackExpression.matchesMethodExecution(methodToMatch)); - } - shadowMatch = ShadowMatchUtils.setShadowMatch(this, targetMethod, shadowMatch); } return shadowMatch; } @@ -713,4 +724,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut } } + + private record ShadowMatchKey(AspectJExpressionPointcut expression, Method method) { + } + } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java index 3c26b4abc52..5e45c6d7d93 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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,24 +16,46 @@ package org.springframework.aop.aspectj; -import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.aspectj.weaver.tools.ShadowMatch; import org.jspecify.annotations.Nullable; -import org.springframework.aop.support.ExpressionPointcut; - /** * Internal {@link ShadowMatch} utilities. * * @author Stephane Nicoll + * @author Juergen Hoeller * @since 6.2 */ public abstract class ShadowMatchUtils { - private static final Map shadowMatchCache = new ConcurrentHashMap<>(256); + private static final Map shadowMatchCache = new ConcurrentHashMap<>(256); + + + /** + * Find a {@link ShadowMatch} for the specified key. + * @param key the key to use + * @return the {@code ShadowMatch} to use for the specified key, + * or {@code null} if none found + */ + static @Nullable ShadowMatch getShadowMatch(Object key) { + return shadowMatchCache.get(key); + } + + /** + * Associate the {@link ShadowMatch} with the specified key. + * If an entry already exists, the given {@code shadowMatch} is ignored. + * @param key the key to use + * @param shadowMatch the shadow match to use for this key + * if none already exists + * @return the shadow match to use for the specified key + */ + static ShadowMatch setShadowMatch(Object key, ShadowMatch shadowMatch) { + ShadowMatch existing = shadowMatchCache.putIfAbsent(key, shadowMatch); + return (existing != null ? existing : shadowMatch); + } /** * Clear the cache of computed {@link ShadowMatch} instances. @@ -42,33 +64,4 @@ public abstract class ShadowMatchUtils { shadowMatchCache.clear(); } - /** - * Return the {@link ShadowMatch} for the specified {@link ExpressionPointcut} - * and {@link Method} or {@code null} if none is found. - * @param expression the expression - * @param method the method - * @return the {@code ShadowMatch} to use for the specified expression and method - */ - static @Nullable ShadowMatch getShadowMatch(ExpressionPointcut expression, Method method) { - return shadowMatchCache.get(new Key(expression, method)); - } - - /** - * Associate the {@link ShadowMatch} to the specified {@link ExpressionPointcut} - * and method. If an entry already exists, the given {@code shadowMatch} is - * ignored. - * @param expression the expression - * @param method the method - * @param shadowMatch the shadow match to use for this expression and method - * if none already exists - * @return the shadow match to use for the specified expression and method - */ - static ShadowMatch setShadowMatch(ExpressionPointcut expression, Method method, ShadowMatch shadowMatch) { - ShadowMatch existing = shadowMatchCache.putIfAbsent(new Key(expression, method), shadowMatch); - return (existing != null ? existing : shadowMatch); - } - - - private record Key(ExpressionPointcut expression, Method method) {} - } diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java index 13d4f2cddb2..b65ff3faacf 100644 --- a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java @@ -45,7 +45,6 @@ import org.springframework.util.ResourceUtils; */ public abstract class AbstractFileResolvingResource extends AbstractResource { - @SuppressWarnings("try") @Override public boolean exists() { try { @@ -90,9 +89,15 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { // existence of the entry (or the jar root in case of no entryName). // getJarFile() called for enforced presence check of the jar file, // throwing a NoSuchFileException otherwise (turned to false below). - try (JarFile jarFile = jarCon.getJarFile()) { + JarFile jarFile = jarCon.getJarFile(); + try { return (jarCon.getEntryName() == null || jarCon.getJarEntry() != null); } + finally { + if (!jarCon.getUseCaches()) { + jarFile.close(); + } + } } else if (con.getContentLengthLong() > 0) { return true; diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java index 5ce4c7764e7..bde76e830cd 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java @@ -44,6 +44,8 @@ import java.util.stream.Collectors; import java.util.zip.ZipEntry; import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -298,8 +300,8 @@ class PathMatchingResourcePatternResolverTests { @Test void rootPatternRetrievalInJarFiles() throws IOException { assertThat(resolver.getResources("classpath*:aspectj*.dtd")).extracting(Resource::getFilename) - .as("Could not find aspectj_1_5_0.dtd in the root of the aspectjweaver jar") - .containsExactly("aspectj_1_5_0.dtd"); + .as("Could not find aspectj_1_5_0.dtd in the root of the aspectjweaver jar") + .containsExactly("aspectj_1_5_0.dtd"); } } @@ -310,6 +312,16 @@ class PathMatchingResourcePatternResolverTests { @TempDir Path temp; + @BeforeAll + static void suppressJarCaches() { + URLConnection.setDefaultUseCaches("jar", false); + } + + @AfterAll + static void restoreJarCaches() { + URLConnection.setDefaultUseCaches("jar", true); + } + @Test void javaDashJarFindsClassPathManifestEntries() throws Exception { Path lib = this.temp.resolve("lib"); @@ -333,6 +345,7 @@ class PathMatchingResourcePatternResolverTests { StreamUtils.copy("test", StandardCharsets.UTF_8, jar); jar.closeEntry(); } + assertThat(new FileSystemResource(path).exists()).isTrue(); assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue(); assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt").exists()).isTrue(); @@ -340,6 +353,14 @@ class PathMatchingResourcePatternResolverTests { assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isFalse(); assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt").exists()).isFalse(); assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/none.txt").exists()).isFalse(); + + Resource resource = new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt"); + try (InputStream is = resource.getInputStream()) { + assertThat(resource.exists()).isTrue(); + assertThat(resource.createRelative("file.txt").exists()).isTrue(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue(); + is.readAllBytes(); + } } private void writeApplicationJar(Path path) throws Exception {