From d7e6b793361dc8759f8613d9da72b58613c8e076 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 25 Oct 2023 12:25:46 +0200 Subject: [PATCH] Log and skip resource hint registration for classpath location patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since we do not yet have support for registering resource hints for classpath location patterns, we have decided to explicitly skip such resources and log a warning to inform users that they need to manually supply resource hints for the exact resources needed by their application. This commit applies this change for @⁠PropertySource and @⁠TestPropertySource. See gh-31162 Closes gh-31429 --- .../ConfigurationClassPostProcessor.java | 23 +++- ...lassPostProcessorAotContributionTests.java | 48 +++++++++ ...ergedContextConfigurationRuntimeHints.java | 43 ++++++-- .../TestPropertySourceRuntimeHintsTests.java | 100 ++++++++++++++++++ 4 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/aot/TestPropertySourceRuntimeHintsTests.java diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index 87ad3b39aed..1e5be5523b2 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -91,6 +91,7 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PropertySourceDescriptor; import org.springframework.core.io.support.PropertySourceProcessor; +import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.metrics.ApplicationStartup; import org.springframework.core.metrics.StartupStep; import org.springframework.core.type.AnnotationMetadata; @@ -655,6 +656,8 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo private static final String RESOURCE_LOADER_VARIABLE = "resourceLoader"; + private final Log logger = LogFactory.getLog(getClass()); + private final List descriptors; private final Function resourceResolver; @@ -679,9 +682,23 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo hints.reflection().registerType(factoryClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); } for (String location : descriptor.locations()) { - Resource resource = this.resourceResolver.apply(location); - if (resource instanceof ClassPathResource classPathResource && classPathResource.exists()) { - hints.resources().registerPattern(classPathResource.getPath()); + if (location.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX) || + (location.startsWith(ResourcePatternResolver.CLASSPATH_URL_PREFIX) && + (location.contains("*") || location.contains("?")))) { + + if (logger.isWarnEnabled()) { + logger.warn(""" + Runtime hint registration is not supported for the 'classpath*:' \ + prefix or wildcards in @PropertySource locations. Please manually \ + register a resource hint for each property source location represented \ + by '%s'.""".formatted(location)); + } + } + else { + Resource resource = this.resourceResolver.apply(location); + if (resource instanceof ClassPathResource classPathResource && classPathResource.exists()) { + hints.resources().registerPattern(classPathResource.getPath()); + } } } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java index 13cb21a2f39..c47dfc9e81a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java @@ -65,6 +65,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.entry; /** @@ -72,6 +73,7 @@ import static org.assertj.core.api.Assertions.entry; * * @author Phillip Webb * @author Stephane Nicoll + * @author Sam Brannen */ class ConfigurationClassPostProcessorAotContributionTests { @@ -264,6 +266,42 @@ class ConfigurationClassPostProcessorAotContributionTests { }); } + @Test + void propertySourceWithClassPathStarLocationPattern() { + BeanFactoryInitializationAotContribution contribution = + getContribution(PropertySourceWithClassPathStarLocationPatternConfiguration.class); + + // We can effectively only assert that an exception is not thrown; however, + // a WARN-level log message similar to the following should be logged. + // + // Runtime hint registration is not supported for the 'classpath*:' prefix or wildcards + // in @PropertySource locations. Please manually register a resource hint for each property + // source location represented by 'classpath*:org/springframework/context/annotation/*.properties'. + assertThatNoException().isThrownBy(() -> contribution.applyTo(generationContext, beanFactoryInitializationCode)); + + // But we can also ensure that a resource hint was not registered. + assertThat(resource("org/springframework/context/annotation/p1.properties")) + .rejects(generationContext.getRuntimeHints()); + } + + @Test + void propertySourceWithWildcardLocationPattern() { + BeanFactoryInitializationAotContribution contribution = + getContribution(PropertySourceWithWildcardLocationPatternConfiguration.class); + + // We can effectively only assert that an exception is not thrown; however, + // a WARN-level log message similar to the following should be logged. + // + // Runtime hint registration is not supported for the 'classpath*:' prefix or wildcards + // in @PropertySource locations. Please manually register a resource hint for each property + // source location represented by 'classpath:org/springframework/context/annotation/p?.properties'. + assertThatNoException().isThrownBy(() -> contribution.applyTo(generationContext, beanFactoryInitializationCode)); + + // But we can also ensure that a resource hint was not registered. + assertThat(resource("org/springframework/context/annotation/p1.properties")) + .rejects(generationContext.getRuntimeHints()); + } + @Test void applyToWhenHasPropertySourcesInvokesPropertySourceProcessorInOrder() { BeanFactoryInitializationAotContribution contribution = getContribution( @@ -363,6 +401,16 @@ class ConfigurationClassPostProcessorAotContributionTests { } + @Configuration(proxyBeanMethods = false) + @PropertySource("classpath*:org/springframework/context/annotation/*.properties") + static class PropertySourceWithClassPathStarLocationPatternConfiguration { + } + + @Configuration(proxyBeanMethods = false) + @PropertySource("classpath:org/springframework/context/annotation/p?.properties") + static class PropertySourceWithWildcardLocationPatternConfiguration { + } + } @Nested diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java b/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java index 22fa9eea663..70cbd9cfdfa 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java @@ -18,7 +18,10 @@ package org.springframework.test.context.aot; import java.lang.reflect.Method; import java.util.Arrays; -import java.util.stream.Stream; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.aot.hint.ResourceHints; import org.springframework.aot.hint.RuntimeHints; @@ -31,7 +34,8 @@ import org.springframework.test.context.MergedContextConfiguration; import org.springframework.util.ClassUtils; import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTRUCTORS; -import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX; +import static org.springframework.core.io.ResourceLoader.CLASSPATH_URL_PREFIX; +import static org.springframework.core.io.support.ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX; /** * {@code MergedContextConfigurationRuntimeHints} registers run-time hints for @@ -57,6 +61,8 @@ class MergedContextConfigurationRuntimeHints { private static final Method getResourceBasePathMethod = loadGetResourceBasePathMethod(); + private final Log logger = LogFactory.getLog(getClass()); + @SuppressWarnings("deprecation") public void registerHints(RuntimeHints runtimeHints, MergedContextConfiguration mergedConfig, ClassLoader classLoader) { @@ -71,11 +77,11 @@ class MergedContextConfigurationRuntimeHints { .forEach(clazz -> registerDeclaredConstructors(clazz, runtimeHints)); // @ContextConfiguration(locations = ...) - registerClasspathResources(mergedConfig.getLocations(), runtimeHints, classLoader); + registerClasspathResources("@ContextConfiguration", mergedConfig.getLocations(), runtimeHints, classLoader); for (PropertySourceDescriptor descriptor : mergedConfig.getPropertySourceDescriptors()) { // @TestPropertySource(locations = ...) - registerClasspathResources(descriptor.locations().stream(), runtimeHints, classLoader); + registerClasspathResources("@TestPropertySource", descriptor.locations(), runtimeHints, classLoader); // @TestPropertySource(factory = ...) Class factoryClass = descriptor.propertySourceFactory(); @@ -102,17 +108,32 @@ class MergedContextConfigurationRuntimeHints { runtimeHints.reflection().registerType(type, INVOKE_DECLARED_CONSTRUCTORS); } - private void registerClasspathResources(String[] locations, RuntimeHints runtimeHints, ClassLoader classLoader) { - registerClasspathResources(Arrays.stream(locations), runtimeHints, classLoader); + private void registerClasspathResources(String annotation, String[] locations, RuntimeHints runtimeHints, ClassLoader classLoader) { + registerClasspathResources(annotation, Arrays.asList(locations), runtimeHints, classLoader); } - private void registerClasspathResources(Stream locations, RuntimeHints runtimeHints, ClassLoader classLoader) { + private void registerClasspathResources(String annotation, List locations, RuntimeHints runtimeHints, ClassLoader classLoader) { DefaultResourceLoader resourceLoader = new DefaultResourceLoader(classLoader); ResourceHints resourceHints = runtimeHints.resources(); - locations.map(resourceLoader::getResource) - .filter(ClassPathResource.class::isInstance) - .filter(Resource::exists) - .forEach(resourceHints::registerResource); + for (String location : locations) { + if (location.startsWith(CLASSPATH_ALL_URL_PREFIX) || + (location.startsWith(CLASSPATH_URL_PREFIX) && (location.contains("*") || location.contains("?")))) { + + if (logger.isWarnEnabled()) { + logger.warn(""" + Runtime hint registration is not supported for the 'classpath*:' \ + prefix or wildcards in %s locations. Please manually register a \ + resource hint for each location represented by '%s'.""" + .formatted(annotation, location)); + } + } + else { + Resource resource = resourceLoader.getResource(location); + if (resource instanceof ClassPathResource classPathResource && classPathResource.exists()) { + resourceHints.registerPattern(classPathResource.getPath()); + } + } + } } private void registerClasspathResourceDirectoryStructure(String directory, RuntimeHints runtimeHints) { diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestPropertySourceRuntimeHintsTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestPropertySourceRuntimeHintsTests.java new file mode 100644 index 00000000000..b3daf35441d --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestPropertySourceRuntimeHintsTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2023 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.test.context.aot; + +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.env.YamlTestProperties; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + +/** + * Tests for registering run-time hints for {@code @TestPropertySource}, tested + * via the {@link TestContextAotGenerator}. + * + * @author Sam Brannen + * @since 6.1 + */ +class TestPropertySourceRuntimeHintsTests extends AbstractAotTests { + + private final RuntimeHints runtimeHints = new RuntimeHints(); + + private final TestContextAotGenerator generator = + new TestContextAotGenerator(new InMemoryGeneratedFiles(), this.runtimeHints); + + + @Test + void testPropertySourceWithClassPathStarLocationPattern() { + Class testClass = ClassPathStarLocationPatternTestCase.class; + + // We can effectively only assert that an exception is not thrown; however, + // a WARN-level log message similar to the following should be logged. + // + // Runtime hint registration is not supported for the 'classpath*:' prefix or + // wildcards in @TestPropertySource locations. Please manually register a resource + // hint for each location represented by 'classpath*:**/aot/samples/basic/test?.yaml'. + assertThatNoException().isThrownBy(() -> this.generator.processAheadOfTime(Stream.of(testClass))); + + // But we can also ensure that a resource hint was not registered. + assertThat(resource("org/springframework/test/context/aot/samples/basic/test1.yaml")).rejects(runtimeHints); + } + + @Test + void testPropertySourceWithWildcardLocationPattern() { + Class testClass = WildcardLocationPatternTestCase.class; + + // We can effectively only assert that an exception is not thrown; however, + // a WARN-level log message similar to the following should be logged. + // + // Runtime hint registration is not supported for the 'classpath*:' prefix or + // wildcards in @TestPropertySource locations. Please manually register a resource + // hint for each location represented by 'classpath:org/springframework/test/context/aot/samples/basic/test?.yaml'. + assertThatNoException().isThrownBy(() -> this.generator.processAheadOfTime(Stream.of(testClass))); + + // But we can also ensure that a resource hint was not registered. + assertThat(resource("org/springframework/test/context/aot/samples/basic/test1.yaml")).rejects(runtimeHints); + } + + private static Predicate resource(String location) { + return RuntimeHintsPredicates.resource().forResource(location); + } + + + @SpringJUnitConfig(Config.class) + @YamlTestProperties("classpath*:**/aot/samples/basic/test?.yaml") + static class ClassPathStarLocationPatternTestCase { + } + + @SpringJUnitConfig(Config.class) + @YamlTestProperties("classpath:org/springframework/test/context/aot/samples/basic/test?.yaml") + static class WildcardLocationPatternTestCase { + } + + @Configuration + static class Config { + } + +}