Skip to content

Commit 3637228

Browse files
committed
Make sure RuntimeHintsRegistrar are invoked only once
Close gh-28594
1 parent 74d1be9 commit 3637228

File tree

4 files changed

+96
-28
lines changed

4 files changed

+96
-28
lines changed

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,43 @@
2525
import org.springframework.aot.hint.RuntimeHintsRegistrar;
2626

2727
/**
28-
* Indicates that one or more {@link RuntimeHintsRegistrar} implementations should be processed.
29-
* <p>Unlike declaring {@link RuntimeHintsRegistrar} as {@code spring/aot.factories},
30-
* {@code @ImportRuntimeHints} allows for more flexible use cases where registrations are only
31-
* processed if the annotated configuration class or bean method is considered by the
32-
* application context.
28+
* Indicates that one or more {@link RuntimeHintsRegistrar} implementations
29+
* should be processed.
30+
*
31+
* <p>Unlike declaring {@link RuntimeHintsRegistrar} using
32+
* {@code spring/aot.factories}, this annotation allows for more flexible
33+
* registration where it is only processed if the annotated component or bean
34+
* method is actually registered in the bean factory. To illustrate this
35+
* behavior, consider the following example:
36+
*
37+
* <pre class="code">
38+
* &#064;Configuration
39+
* public class MyConfiguration {
40+
*
41+
* &#064;Bean
42+
* &#064;ImportRuntimeHints(MyHints.class)
43+
* &#064;Conditional(MyCondition.class)
44+
* public MyService myService() {
45+
* return new MyService();
46+
* }
47+
*
48+
* }</pre>
49+
*
50+
* If the configuration class above is processed, {@code MyHints} will be
51+
* contributed only if {@code MyCondition} matches. If it does not, and
52+
* therefore {@code MyService} is not defined as a bean, the hints will
53+
* not be processed either.
54+
*
55+
* <p>If several components refer to the same {@link RuntimeHintsRegistrar}
56+
* implementation, it is invoked only once for a given bean factory
57+
* processing.
3358
*
3459
* @author Brian Clozel
60+
* @author Stephane Nicoll
3561
* @since 6.0
3662
* @see org.springframework.aot.hint.RuntimeHints
3763
*/
38-
@Target({ElementType.TYPE, ElementType.METHOD})
64+
@Target({ ElementType.TYPE, ElementType.METHOD })
3965
@Retention(RetentionPolicy.RUNTIME)
4066
@Documented
4167
public @interface ImportRuntimeHints {

spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616

1717
package org.springframework.context.aot;
1818

19-
import java.util.ArrayList;
20-
import java.util.List;
19+
import java.util.LinkedHashMap;
20+
import java.util.LinkedHashSet;
21+
import java.util.Map;
22+
import java.util.Set;
2123

2224
import org.apache.commons.logging.Log;
2325
import org.apache.commons.logging.LogFactory;
@@ -55,29 +57,37 @@ class RuntimeHintsBeanFactoryInitializationAotProcessor
5557
public BeanFactoryInitializationAotContribution processAheadOfTime(
5658
ConfigurableListableBeanFactory beanFactory) {
5759
AotFactoriesLoader loader = new AotFactoriesLoader(beanFactory);
58-
List<RuntimeHintsRegistrar> registrars = new ArrayList<>(
59-
loader.load(RuntimeHintsRegistrar.class));
60+
Map<Class<? extends RuntimeHintsRegistrar>, RuntimeHintsRegistrar> registrars = loader
61+
.load(RuntimeHintsRegistrar.class).stream()
62+
.collect(LinkedHashMap::new, (map, item) -> map.put(item.getClass(), item), Map::putAll);
63+
extractFromBeanFactory(beanFactory).forEach(registrarClass ->
64+
registrars.computeIfAbsent(registrarClass, BeanUtils::instantiateClass));
65+
return new RuntimeHintsRegistrarContribution(registrars.values(),
66+
beanFactory.getBeanClassLoader());
67+
}
68+
69+
private Set<Class<? extends RuntimeHintsRegistrar>> extractFromBeanFactory(ConfigurableListableBeanFactory beanFactory) {
70+
Set<Class<? extends RuntimeHintsRegistrar>> registrarClasses = new LinkedHashSet<>();
6071
for (String beanName : beanFactory
6172
.getBeanNamesForAnnotation(ImportRuntimeHints.class)) {
6273
ImportRuntimeHints annotation = beanFactory.findAnnotationOnBean(beanName,
6374
ImportRuntimeHints.class);
6475
if (annotation != null) {
65-
registrars.addAll(extracted(beanName, annotation));
76+
registrarClasses.addAll(extractFromBeanDefinition(beanName, annotation));
6677
}
6778
}
68-
return new RuntimeHintsRegistrarContribution(registrars,
69-
beanFactory.getBeanClassLoader());
79+
return registrarClasses;
7080
}
7181

72-
private List<RuntimeHintsRegistrar> extracted(String beanName,
82+
private Set<Class<? extends RuntimeHintsRegistrar>> extractFromBeanDefinition(String beanName,
7383
ImportRuntimeHints annotation) {
74-
Class<? extends RuntimeHintsRegistrar>[] registrarClasses = annotation.value();
75-
List<RuntimeHintsRegistrar> registrars = new ArrayList<>(registrarClasses.length);
76-
for (Class<? extends RuntimeHintsRegistrar> registrarClass : registrarClasses) {
84+
85+
Set<Class<? extends RuntimeHintsRegistrar>> registrars = new LinkedHashSet<>();
86+
for (Class<? extends RuntimeHintsRegistrar> registrarClass : annotation.value()) {
7787
logger.trace(
7888
LogMessage.format("Loaded [%s] registrar from annotated bean [%s]",
7989
registrarClass.getCanonicalName(), beanName));
80-
registrars.add(BeanUtils.instantiateClass(registrarClass));
90+
registrars.add(registrarClass);
8191
}
8292
return registrars;
8393
}
@@ -87,13 +97,13 @@ static class RuntimeHintsRegistrarContribution
8797
implements BeanFactoryInitializationAotContribution {
8898

8999

90-
private final List<RuntimeHintsRegistrar> registrars;
100+
private final Iterable<RuntimeHintsRegistrar> registrars;
91101

92102
@Nullable
93103
private final ClassLoader beanClassLoader;
94104

95105

96-
RuntimeHintsRegistrarContribution(List<RuntimeHintsRegistrar> registrars,
106+
RuntimeHintsRegistrarContribution(Iterable<RuntimeHintsRegistrar> registrars,
97107
@Nullable ClassLoader beanClassLoader) {
98108
this.registrars = registrars;
99109
this.beanClassLoader = beanClassLoader;

spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.net.URL;
2121
import java.util.Enumeration;
22+
import java.util.concurrent.atomic.AtomicInteger;
2223
import java.util.stream.Stream;
2324

2425
import org.junit.jupiter.api.BeforeEach;
@@ -38,6 +39,7 @@
3839
import org.springframework.context.annotation.ImportRuntimeHints;
3940
import org.springframework.context.support.GenericApplicationContext;
4041
import org.springframework.javapoet.ClassName;
42+
import org.springframework.lang.Nullable;
4143

4244
import static org.assertj.core.api.Assertions.assertThat;
4345
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -91,13 +93,31 @@ void shouldProcessRegistrarInSpringFactory() {
9193
assertThatSampleRegistrarContributed();
9294
}
9395

96+
@Test
97+
void shouldProcessDuplicatedRegistrarsOnlyOnce() {
98+
GenericApplicationContext applicationContext = createApplicationContext();
99+
applicationContext.registerBeanDefinition("incremental1",
100+
new RootBeanDefinition(ConfigurationWithIncrementalHints.class));
101+
applicationContext.registerBeanDefinition("incremental2",
102+
new RootBeanDefinition(ConfigurationWithIncrementalHints.class));
103+
applicationContext.setClassLoader(
104+
new TestSpringFactoriesClassLoader("test-duplicated-runtime-hints-aot.factories"));
105+
IncrementalRuntimeHintsRegistrar.counter.set(0);
106+
this.generator.generateApplicationContext(applicationContext,
107+
this.generationContext, MAIN_GENERATED_TYPE);
108+
RuntimeHints runtimeHints = this.generationContext.getRuntimeHints();
109+
assertThat(runtimeHints.resources().resourceBundles().map(ResourceBundleHint::getBaseName))
110+
.containsOnly("com.example.example0", "sample");
111+
assertThat(IncrementalRuntimeHintsRegistrar.counter.get()).isEqualTo(1);
112+
}
113+
94114
@Test
95115
void shouldRejectRuntimeHintsRegistrarWithoutDefaultConstructor() {
96116
GenericApplicationContext applicationContext = createApplicationContext(
97117
ConfigurationWithIllegalRegistrar.class);
98118
assertThatThrownBy(() -> this.generator.generateApplicationContext(
99119
applicationContext, this.generationContext, MAIN_GENERATED_TYPE))
100-
.isInstanceOf(BeanInstantiationException.class);
120+
.isInstanceOf(BeanInstantiationException.class);
101121
}
102122

103123
private void assertThatSampleRegistrarContributed() {
@@ -119,10 +139,9 @@ private GenericApplicationContext createApplicationContext(
119139
}
120140

121141

122-
@ImportRuntimeHints(SampleRuntimeHintsRegistrar.class)
123142
@Configuration(proxyBeanMethods = false)
143+
@ImportRuntimeHints(SampleRuntimeHintsRegistrar.class)
124144
static class ConfigurationWithHints {
125-
126145
}
127146

128147

@@ -137,7 +156,6 @@ SampleBean sampleBean() {
137156

138157
}
139158

140-
141159
public static class SampleRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
142160

143161
@Override
@@ -147,19 +165,31 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
147165

148166
}
149167

168+
@Configuration(proxyBeanMethods = false)
169+
@ImportRuntimeHints(IncrementalRuntimeHintsRegistrar.class)
170+
static class ConfigurationWithIncrementalHints {
171+
}
172+
173+
static class IncrementalRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
150174

151-
static class SampleBean {
175+
static final AtomicInteger counter = new AtomicInteger();
152176

177+
@Override
178+
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
179+
hints.resources().registerResourceBundle("com.example.example" + counter.getAndIncrement());
180+
}
153181
}
154182

183+
static class SampleBean {
184+
185+
}
155186

156-
@ImportRuntimeHints(IllegalRuntimeHintsRegistrar.class)
157187
@Configuration(proxyBeanMethods = false)
188+
@ImportRuntimeHints(IllegalRuntimeHintsRegistrar.class)
158189
static class ConfigurationWithIllegalRegistrar {
159190

160191
}
161192

162-
163193
public static class IllegalRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
164194

165195
public IllegalRuntimeHintsRegistrar(String arg) {
@@ -173,7 +203,6 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
173203

174204
}
175205

176-
177206
static class TestSpringFactoriesClassLoader extends ClassLoader {
178207

179208
private final String factoriesName;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
org.springframework.aot.hint.RuntimeHintsRegistrar= \
2+
org.springframework.context.aot.RuntimeHintsBeanFactoryInitializationAotProcessorTests.IncrementalRuntimeHintsRegistrar, \
3+
org.springframework.context.aot.RuntimeHintsBeanFactoryInitializationAotProcessorTests.SampleRuntimeHintsRegistrar

0 commit comments

Comments
 (0)