Skip to content

Commit e1f45c5

Browse files
committed
Generate reflection hints for main methods
This commit makes sure to register the necessary hints to invoke the main method of any bean available in the context. This is necessary for tests that use the UseMainMethod feature. This generates more hints than strictly necessary as there isn't a way to contribute hints based on a ContextLoader, see spring-projects/spring-framework#34513 for more details. Closes gh-44461
1 parent c91c8e2 commit e1f45c5

File tree

3 files changed

+91
-8
lines changed

3 files changed

+91
-8
lines changed

spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java

+53-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,11 +19,19 @@
1919
import java.lang.reflect.Method;
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
22+
import java.util.Collection;
2223
import java.util.Collections;
2324
import java.util.List;
2425
import java.util.function.Consumer;
2526

27+
import org.springframework.aot.generate.GenerationContext;
28+
import org.springframework.aot.hint.ExecutableMode;
29+
import org.springframework.aot.hint.ReflectionHints;
2630
import org.springframework.beans.BeanUtils;
31+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
32+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
33+
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
34+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
2735
import org.springframework.boot.ApplicationContextFactory;
2836
import org.springframework.boot.Banner;
2937
import org.springframework.boot.ConfigurableBootstrapContext;
@@ -158,20 +166,23 @@ private Method getMainMethod(MergedContextConfiguration mergedConfig, UseMainMet
158166
.orElse(null);
159167
Assert.state(springBootConfiguration != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE,
160168
"Cannot use main method as no @SpringBootConfiguration-annotated class is available");
161-
Method mainMethod = (springBootConfiguration != null)
162-
? ReflectionUtils.findMethod(springBootConfiguration, "main", String[].class) : null;
169+
Method mainMethod = findMainMethod(springBootConfiguration);
170+
Assert.state(mainMethod != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE,
171+
() -> "Main method not found on '%s'".formatted(springBootConfiguration.getName()));
172+
return mainMethod;
173+
}
174+
175+
private static Method findMainMethod(Class<?> type) {
176+
Method mainMethod = (type != null) ? ReflectionUtils.findMethod(type, "main", String[].class) : null;
163177
if (mainMethod == null && KotlinDetector.isKotlinPresent()) {
164178
try {
165-
Class<?> kotlinClass = ClassUtils.forName(springBootConfiguration.getName() + "Kt",
166-
springBootConfiguration.getClassLoader());
179+
Class<?> kotlinClass = ClassUtils.forName(type.getName() + "Kt", type.getClassLoader());
167180
mainMethod = ReflectionUtils.findMethod(kotlinClass, "main", String[].class);
168181
}
169182
catch (ClassNotFoundException ex) {
170183
// Ignore
171184
}
172185
}
173-
Assert.state(mainMethod != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE,
174-
() -> "Main method not found on '%s'".formatted(springBootConfiguration.getName()));
175186
return mainMethod;
176187
}
177188

@@ -574,4 +585,39 @@ private ApplicationContext run(ThrowingSupplier<ConfigurableApplicationContext>
574585

575586
}
576587

588+
static class MainMethodBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
589+
590+
@Override
591+
public BeanFactoryInitializationAotContribution processAheadOfTime(
592+
ConfigurableListableBeanFactory beanFactory) {
593+
List<Method> mainMethods = new ArrayList<>();
594+
for (String beanName : beanFactory.getBeanDefinitionNames()) {
595+
Class<?> beanType = beanFactory.getType(beanName);
596+
Method mainMethod = findMainMethod(beanType);
597+
if (mainMethod != null) {
598+
mainMethods.add(mainMethod);
599+
}
600+
}
601+
return !mainMethods.isEmpty() ? new AotContribution(mainMethods) : null;
602+
}
603+
604+
static class AotContribution implements BeanFactoryInitializationAotContribution {
605+
606+
private final Collection<Method> mainMethods;
607+
608+
AotContribution(Collection<Method> mainMethods) {
609+
this.mainMethods = mainMethods;
610+
}
611+
612+
@Override
613+
public void applyTo(GenerationContext generationContext,
614+
BeanFactoryInitializationCode beanFactoryInitializationCode) {
615+
ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection();
616+
this.mainMethods.forEach((method) -> reflectionHints.registerMethod(method, ExecutableMode.INVOKE));
617+
}
618+
619+
}
620+
621+
}
622+
577623
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
2+
org.springframework.boot.test.context.SpringBootContextLoader.MainMethodBeanFactoryInitializationAotProcessor

spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,10 +25,16 @@
2525
import org.junit.jupiter.api.Disabled;
2626
import org.junit.jupiter.api.Test;
2727

28+
import org.springframework.aot.hint.RuntimeHints;
29+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
30+
import org.springframework.aot.test.generate.TestGenerationContext;
2831
import org.springframework.beans.factory.BeanCreationException;
32+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
33+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
2934
import org.springframework.boot.ApplicationContextFactory;
3035
import org.springframework.boot.SpringApplication;
3136
import org.springframework.boot.SpringBootConfiguration;
37+
import org.springframework.boot.test.context.SpringBootContextLoader.MainMethodBeanFactoryInitializationAotProcessor;
3238
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;
3339
import org.springframework.boot.test.util.TestPropertyValues;
3440
import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext;
@@ -248,6 +254,35 @@ void whenUseMainMethodWithContextHierarchyThrowsException() {
248254
.withMessage("UseMainMethod.ALWAYS cannot be used with @ContextHierarchy tests");
249255
}
250256

257+
@Test
258+
void whenMainMethodPresentRegisterReflectionHints() {
259+
TestContext testContext = new ExposedTestContextManager(UseMainMethodWhenAvailableAndNoMainMethod.class)
260+
.getExposedTestContext();
261+
ConfigurableListableBeanFactory beanFactory = (ConfigurableListableBeanFactory) testContext
262+
.getApplicationContext()
263+
.getAutowireCapableBeanFactory();
264+
BeanFactoryInitializationAotContribution aotContribution = new MainMethodBeanFactoryInitializationAotProcessor()
265+
.processAheadOfTime(beanFactory);
266+
assertThat(aotContribution).isNull();
267+
}
268+
269+
@Test
270+
void whenMainMethodNotAvailableReturnsNoAotContribution() {
271+
TestContext testContext = new ExposedTestContextManager(UseMainMethodWhenAvailableAndMainMethod.class)
272+
.getExposedTestContext();
273+
ConfigurableListableBeanFactory beanFactory = (ConfigurableListableBeanFactory) testContext
274+
.getApplicationContext()
275+
.getAutowireCapableBeanFactory();
276+
BeanFactoryInitializationAotContribution aotContribution = new MainMethodBeanFactoryInitializationAotProcessor()
277+
.processAheadOfTime(beanFactory);
278+
assertThat(aotContribution).isNotNull();
279+
TestGenerationContext generationContext = new TestGenerationContext();
280+
aotContribution.applyTo(generationContext, null);
281+
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
282+
assertThat(RuntimeHintsPredicates.reflection().onMethod(ConfigWithMain.class, "main").invoke())
283+
.accepts(runtimeHints);
284+
}
285+
251286
@Test
252287
void whenSubclassProvidesCustomApplicationContextFactory() {
253288
TestContext testContext = new ExposedTestContextManager(CustomApplicationContextTest.class)

0 commit comments

Comments
 (0)