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
https://github.com/spring-projects/spring-framework/issues/34513 for
more details.

Closes gh-44461
This commit is contained in:
Stéphane Nicoll 2025-02-28 10:41:55 +01:00
parent c91c8e202a
commit e1f45c50ee
3 changed files with 91 additions and 8 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-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.
@ -19,11 +19,19 @@ package org.springframework.boot.test.context;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.ApplicationContextFactory;
import org.springframework.boot.Banner;
import org.springframework.boot.ConfigurableBootstrapContext;
@ -158,20 +166,23 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao
.orElse(null);
Assert.state(springBootConfiguration != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE,
"Cannot use main method as no @SpringBootConfiguration-annotated class is available");
Method mainMethod = (springBootConfiguration != null)
? ReflectionUtils.findMethod(springBootConfiguration, "main", String[].class) : null;
Method mainMethod = findMainMethod(springBootConfiguration);
Assert.state(mainMethod != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE,
() -> "Main method not found on '%s'".formatted(springBootConfiguration.getName()));
return mainMethod;
}
private static Method findMainMethod(Class<?> type) {
Method mainMethod = (type != null) ? ReflectionUtils.findMethod(type, "main", String[].class) : null;
if (mainMethod == null && KotlinDetector.isKotlinPresent()) {
try {
Class<?> kotlinClass = ClassUtils.forName(springBootConfiguration.getName() + "Kt",
springBootConfiguration.getClassLoader());
Class<?> kotlinClass = ClassUtils.forName(type.getName() + "Kt", type.getClassLoader());
mainMethod = ReflectionUtils.findMethod(kotlinClass, "main", String[].class);
}
catch (ClassNotFoundException ex) {
// Ignore
}
}
Assert.state(mainMethod != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE,
() -> "Main method not found on '%s'".formatted(springBootConfiguration.getName()));
return mainMethod;
}
@ -574,4 +585,39 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao
}
static class MainMethodBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
@Override
public BeanFactoryInitializationAotContribution processAheadOfTime(
ConfigurableListableBeanFactory beanFactory) {
List<Method> mainMethods = new ArrayList<>();
for (String beanName : beanFactory.getBeanDefinitionNames()) {
Class<?> beanType = beanFactory.getType(beanName);
Method mainMethod = findMainMethod(beanType);
if (mainMethod != null) {
mainMethods.add(mainMethod);
}
}
return !mainMethods.isEmpty() ? new AotContribution(mainMethods) : null;
}
static class AotContribution implements BeanFactoryInitializationAotContribution {
private final Collection<Method> mainMethods;
AotContribution(Collection<Method> mainMethods) {
this.mainMethods = mainMethods;
}
@Override
public void applyTo(GenerationContext generationContext,
BeanFactoryInitializationCode beanFactoryInitializationCode) {
ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection();
this.mainMethods.forEach((method) -> reflectionHints.registerMethod(method, ExecutableMode.INVOKE));
}
}
}
}

View File

@ -0,0 +1,2 @@
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
org.springframework.boot.test.context.SpringBootContextLoader.MainMethodBeanFactoryInitializationAotProcessor

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-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.
@ -25,10 +25,16 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.ApplicationContextFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootContextLoader.MainMethodBeanFactoryInitializationAotProcessor;
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext;
@ -248,6 +254,35 @@ class SpringBootContextLoaderTests {
.withMessage("UseMainMethod.ALWAYS cannot be used with @ContextHierarchy tests");
}
@Test
void whenMainMethodPresentRegisterReflectionHints() {
TestContext testContext = new ExposedTestContextManager(UseMainMethodWhenAvailableAndNoMainMethod.class)
.getExposedTestContext();
ConfigurableListableBeanFactory beanFactory = (ConfigurableListableBeanFactory) testContext
.getApplicationContext()
.getAutowireCapableBeanFactory();
BeanFactoryInitializationAotContribution aotContribution = new MainMethodBeanFactoryInitializationAotProcessor()
.processAheadOfTime(beanFactory);
assertThat(aotContribution).isNull();
}
@Test
void whenMainMethodNotAvailableReturnsNoAotContribution() {
TestContext testContext = new ExposedTestContextManager(UseMainMethodWhenAvailableAndMainMethod.class)
.getExposedTestContext();
ConfigurableListableBeanFactory beanFactory = (ConfigurableListableBeanFactory) testContext
.getApplicationContext()
.getAutowireCapableBeanFactory();
BeanFactoryInitializationAotContribution aotContribution = new MainMethodBeanFactoryInitializationAotProcessor()
.processAheadOfTime(beanFactory);
assertThat(aotContribution).isNotNull();
TestGenerationContext generationContext = new TestGenerationContext();
aotContribution.applyTo(generationContext, null);
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
assertThat(RuntimeHintsPredicates.reflection().onMethod(ConfigWithMain.class, "main").invoke())
.accepts(runtimeHints);
}
@Test
void whenSubclassProvidesCustomApplicationContextFactory() {
TestContext testContext = new ExposedTestContextManager(CustomApplicationContextTest.class)