Skip to content

Commit d7e6b79

Browse files
committed
Log and skip resource hint registration for classpath location patterns
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
1 parent 9a7f141 commit d7e6b79

File tree

4 files changed

+200
-14
lines changed

4 files changed

+200
-14
lines changed

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
import org.springframework.core.io.ResourceLoader;
9292
import org.springframework.core.io.support.PropertySourceDescriptor;
9393
import org.springframework.core.io.support.PropertySourceProcessor;
94+
import org.springframework.core.io.support.ResourcePatternResolver;
9495
import org.springframework.core.metrics.ApplicationStartup;
9596
import org.springframework.core.metrics.StartupStep;
9697
import org.springframework.core.type.AnnotationMetadata;
@@ -655,6 +656,8 @@ private static class PropertySourcesAotContribution implements BeanFactoryInitia
655656

656657
private static final String RESOURCE_LOADER_VARIABLE = "resourceLoader";
657658

659+
private final Log logger = LogFactory.getLog(getClass());
660+
658661
private final List<PropertySourceDescriptor> descriptors;
659662

660663
private final Function<String, Resource> resourceResolver;
@@ -679,9 +682,23 @@ private void registerRuntimeHints(RuntimeHints hints) {
679682
hints.reflection().registerType(factoryClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
680683
}
681684
for (String location : descriptor.locations()) {
682-
Resource resource = this.resourceResolver.apply(location);
683-
if (resource instanceof ClassPathResource classPathResource && classPathResource.exists()) {
684-
hints.resources().registerPattern(classPathResource.getPath());
685+
if (location.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX) ||
686+
(location.startsWith(ResourcePatternResolver.CLASSPATH_URL_PREFIX) &&
687+
(location.contains("*") || location.contains("?")))) {
688+
689+
if (logger.isWarnEnabled()) {
690+
logger.warn("""
691+
Runtime hint registration is not supported for the 'classpath*:' \
692+
prefix or wildcards in @PropertySource locations. Please manually \
693+
register a resource hint for each property source location represented \
694+
by '%s'.""".formatted(location));
695+
}
696+
}
697+
else {
698+
Resource resource = this.resourceResolver.apply(location);
699+
if (resource instanceof ClassPathResource classPathResource && classPathResource.exists()) {
700+
hints.resources().registerPattern(classPathResource.getPath());
701+
}
685702
}
686703
}
687704
}

spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,15 @@
6565
import org.springframework.util.Assert;
6666

6767
import static org.assertj.core.api.Assertions.assertThat;
68+
import static org.assertj.core.api.Assertions.assertThatNoException;
6869
import static org.assertj.core.api.Assertions.entry;
6970

7071
/**
7172
* Tests for {@link ConfigurationClassPostProcessor} AOT contributions.
7273
*
7374
* @author Phillip Webb
7475
* @author Stephane Nicoll
76+
* @author Sam Brannen
7577
*/
7678
class ConfigurationClassPostProcessorAotContributionTests {
7779

@@ -264,6 +266,42 @@ void applyToWhenHasPropertySourceInvokePropertySourceProcessor() {
264266
});
265267
}
266268

269+
@Test
270+
void propertySourceWithClassPathStarLocationPattern() {
271+
BeanFactoryInitializationAotContribution contribution =
272+
getContribution(PropertySourceWithClassPathStarLocationPatternConfiguration.class);
273+
274+
// We can effectively only assert that an exception is not thrown; however,
275+
// a WARN-level log message similar to the following should be logged.
276+
//
277+
// Runtime hint registration is not supported for the 'classpath*:' prefix or wildcards
278+
// in @PropertySource locations. Please manually register a resource hint for each property
279+
// source location represented by 'classpath*:org/springframework/context/annotation/*.properties'.
280+
assertThatNoException().isThrownBy(() -> contribution.applyTo(generationContext, beanFactoryInitializationCode));
281+
282+
// But we can also ensure that a resource hint was not registered.
283+
assertThat(resource("org/springframework/context/annotation/p1.properties"))
284+
.rejects(generationContext.getRuntimeHints());
285+
}
286+
287+
@Test
288+
void propertySourceWithWildcardLocationPattern() {
289+
BeanFactoryInitializationAotContribution contribution =
290+
getContribution(PropertySourceWithWildcardLocationPatternConfiguration.class);
291+
292+
// We can effectively only assert that an exception is not thrown; however,
293+
// a WARN-level log message similar to the following should be logged.
294+
//
295+
// Runtime hint registration is not supported for the 'classpath*:' prefix or wildcards
296+
// in @PropertySource locations. Please manually register a resource hint for each property
297+
// source location represented by 'classpath:org/springframework/context/annotation/p?.properties'.
298+
assertThatNoException().isThrownBy(() -> contribution.applyTo(generationContext, beanFactoryInitializationCode));
299+
300+
// But we can also ensure that a resource hint was not registered.
301+
assertThat(resource("org/springframework/context/annotation/p1.properties"))
302+
.rejects(generationContext.getRuntimeHints());
303+
}
304+
267305
@Test
268306
void applyToWhenHasPropertySourcesInvokesPropertySourceProcessorInOrder() {
269307
BeanFactoryInitializationAotContribution contribution = getContribution(
@@ -363,6 +401,16 @@ static class PropertySourceWithCustomFactoryConfiguration {
363401

364402
}
365403

404+
@Configuration(proxyBeanMethods = false)
405+
@PropertySource("classpath*:org/springframework/context/annotation/*.properties")
406+
static class PropertySourceWithClassPathStarLocationPatternConfiguration {
407+
}
408+
409+
@Configuration(proxyBeanMethods = false)
410+
@PropertySource("classpath:org/springframework/context/annotation/p?.properties")
411+
static class PropertySourceWithWildcardLocationPatternConfiguration {
412+
}
413+
366414
}
367415

368416
@Nested

spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818

1919
import java.lang.reflect.Method;
2020
import java.util.Arrays;
21-
import java.util.stream.Stream;
21+
import java.util.List;
22+
23+
import org.apache.commons.logging.Log;
24+
import org.apache.commons.logging.LogFactory;
2225

2326
import org.springframework.aot.hint.ResourceHints;
2427
import org.springframework.aot.hint.RuntimeHints;
@@ -31,7 +34,8 @@
3134
import org.springframework.util.ClassUtils;
3235

3336
import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTRUCTORS;
34-
import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX;
37+
import static org.springframework.core.io.ResourceLoader.CLASSPATH_URL_PREFIX;
38+
import static org.springframework.core.io.support.ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
3539

3640
/**
3741
* {@code MergedContextConfigurationRuntimeHints} registers run-time hints for
@@ -57,6 +61,8 @@ class MergedContextConfigurationRuntimeHints {
5761

5862
private static final Method getResourceBasePathMethod = loadGetResourceBasePathMethod();
5963

64+
private final Log logger = LogFactory.getLog(getClass());
65+
6066

6167
@SuppressWarnings("deprecation")
6268
public void registerHints(RuntimeHints runtimeHints, MergedContextConfiguration mergedConfig, ClassLoader classLoader) {
@@ -71,11 +77,11 @@ public void registerHints(RuntimeHints runtimeHints, MergedContextConfiguration
7177
.forEach(clazz -> registerDeclaredConstructors(clazz, runtimeHints));
7278

7379
// @ContextConfiguration(locations = ...)
74-
registerClasspathResources(mergedConfig.getLocations(), runtimeHints, classLoader);
80+
registerClasspathResources("@ContextConfiguration", mergedConfig.getLocations(), runtimeHints, classLoader);
7581

7682
for (PropertySourceDescriptor descriptor : mergedConfig.getPropertySourceDescriptors()) {
7783
// @TestPropertySource(locations = ...)
78-
registerClasspathResources(descriptor.locations().stream(), runtimeHints, classLoader);
84+
registerClasspathResources("@TestPropertySource", descriptor.locations(), runtimeHints, classLoader);
7985

8086
// @TestPropertySource(factory = ...)
8187
Class<?> factoryClass = descriptor.propertySourceFactory();
@@ -102,17 +108,32 @@ private void registerDeclaredConstructors(Class<?> type, RuntimeHints runtimeHin
102108
runtimeHints.reflection().registerType(type, INVOKE_DECLARED_CONSTRUCTORS);
103109
}
104110

105-
private void registerClasspathResources(String[] locations, RuntimeHints runtimeHints, ClassLoader classLoader) {
106-
registerClasspathResources(Arrays.stream(locations), runtimeHints, classLoader);
111+
private void registerClasspathResources(String annotation, String[] locations, RuntimeHints runtimeHints, ClassLoader classLoader) {
112+
registerClasspathResources(annotation, Arrays.asList(locations), runtimeHints, classLoader);
107113
}
108114

109-
private void registerClasspathResources(Stream<String> locations, RuntimeHints runtimeHints, ClassLoader classLoader) {
115+
private void registerClasspathResources(String annotation, List<String> locations, RuntimeHints runtimeHints, ClassLoader classLoader) {
110116
DefaultResourceLoader resourceLoader = new DefaultResourceLoader(classLoader);
111117
ResourceHints resourceHints = runtimeHints.resources();
112-
locations.map(resourceLoader::getResource)
113-
.filter(ClassPathResource.class::isInstance)
114-
.filter(Resource::exists)
115-
.forEach(resourceHints::registerResource);
118+
for (String location : locations) {
119+
if (location.startsWith(CLASSPATH_ALL_URL_PREFIX) ||
120+
(location.startsWith(CLASSPATH_URL_PREFIX) && (location.contains("*") || location.contains("?")))) {
121+
122+
if (logger.isWarnEnabled()) {
123+
logger.warn("""
124+
Runtime hint registration is not supported for the 'classpath*:' \
125+
prefix or wildcards in %s locations. Please manually register a \
126+
resource hint for each location represented by '%s'."""
127+
.formatted(annotation, location));
128+
}
129+
}
130+
else {
131+
Resource resource = resourceLoader.getResource(location);
132+
if (resource instanceof ClassPathResource classPathResource && classPathResource.exists()) {
133+
resourceHints.registerPattern(classPathResource.getPath());
134+
}
135+
}
136+
}
116137
}
117138

118139
private void registerClasspathResourceDirectoryStructure(String directory, RuntimeHints runtimeHints) {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.aot;
18+
19+
import java.util.function.Predicate;
20+
import java.util.stream.Stream;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import org.springframework.aot.generate.InMemoryGeneratedFiles;
25+
import org.springframework.aot.hint.RuntimeHints;
26+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.test.context.env.YamlTestProperties;
29+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.assertj.core.api.Assertions.assertThatNoException;
33+
34+
/**
35+
* Tests for registering run-time hints for {@code @TestPropertySource}, tested
36+
* via the {@link TestContextAotGenerator}.
37+
*
38+
* @author Sam Brannen
39+
* @since 6.1
40+
*/
41+
class TestPropertySourceRuntimeHintsTests extends AbstractAotTests {
42+
43+
private final RuntimeHints runtimeHints = new RuntimeHints();
44+
45+
private final TestContextAotGenerator generator =
46+
new TestContextAotGenerator(new InMemoryGeneratedFiles(), this.runtimeHints);
47+
48+
49+
@Test
50+
void testPropertySourceWithClassPathStarLocationPattern() {
51+
Class<?> testClass = ClassPathStarLocationPatternTestCase.class;
52+
53+
// We can effectively only assert that an exception is not thrown; however,
54+
// a WARN-level log message similar to the following should be logged.
55+
//
56+
// Runtime hint registration is not supported for the 'classpath*:' prefix or
57+
// wildcards in @TestPropertySource locations. Please manually register a resource
58+
// hint for each location represented by 'classpath*:**/aot/samples/basic/test?.yaml'.
59+
assertThatNoException().isThrownBy(() -> this.generator.processAheadOfTime(Stream.of(testClass)));
60+
61+
// But we can also ensure that a resource hint was not registered.
62+
assertThat(resource("org/springframework/test/context/aot/samples/basic/test1.yaml")).rejects(runtimeHints);
63+
}
64+
65+
@Test
66+
void testPropertySourceWithWildcardLocationPattern() {
67+
Class<?> testClass = WildcardLocationPatternTestCase.class;
68+
69+
// We can effectively only assert that an exception is not thrown; however,
70+
// a WARN-level log message similar to the following should be logged.
71+
//
72+
// Runtime hint registration is not supported for the 'classpath*:' prefix or
73+
// wildcards in @TestPropertySource locations. Please manually register a resource
74+
// hint for each location represented by 'classpath:org/springframework/test/context/aot/samples/basic/test?.yaml'.
75+
assertThatNoException().isThrownBy(() -> this.generator.processAheadOfTime(Stream.of(testClass)));
76+
77+
// But we can also ensure that a resource hint was not registered.
78+
assertThat(resource("org/springframework/test/context/aot/samples/basic/test1.yaml")).rejects(runtimeHints);
79+
}
80+
81+
private static Predicate<RuntimeHints> resource(String location) {
82+
return RuntimeHintsPredicates.resource().forResource(location);
83+
}
84+
85+
86+
@SpringJUnitConfig(Config.class)
87+
@YamlTestProperties("classpath*:**/aot/samples/basic/test?.yaml")
88+
static class ClassPathStarLocationPatternTestCase {
89+
}
90+
91+
@SpringJUnitConfig(Config.class)
92+
@YamlTestProperties("classpath:org/springframework/test/context/aot/samples/basic/test?.yaml")
93+
static class WildcardLocationPatternTestCase {
94+
}
95+
96+
@Configuration
97+
static class Config {
98+
}
99+
100+
}

0 commit comments

Comments
 (0)