Detect JDK25-style main method in SpringBootTest

This commit adapts SpringBootTest when it has to use a main method to
detect package private main method as well as main methods that do not
have any arguments.

Closes gh-48271
This commit is contained in:
Stéphane Nicoll
2025-11-24 16:45:57 +01:00
parent 709e19f708
commit ae3408f285
2 changed files with 94 additions and 14 deletions
@@ -17,6 +17,7 @@
package org.springframework.boot.test.context;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -136,7 +137,14 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao
}
ContextLoaderHook hook = new ContextLoaderHook(mode, initializer,
(application) -> configure(mergedConfig, application));
return hook.runMain(() -> ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { args }));
return hook.runMain(() -> {
if (mainMethod.getParameterCount() == 0) {
ReflectionUtils.invokeMethod(mainMethod, null);
}
else {
ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { args });
}
});
}
SpringApplication application = getSpringApplication();
configure(mergedConfig, application);
@@ -172,7 +180,7 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao
}
private static Method findMainMethod(Class<?> type) {
Method mainMethod = (type != null) ? ReflectionUtils.findMethod(type, "main", String[].class) : null;
Method mainMethod = (type != null) ? findMainJavaMethod(type) : null;
if (mainMethod == null && KotlinDetector.isKotlinPresent()) {
try {
Class<?> kotlinClass = ClassUtils.forName(type.getName() + "Kt", type.getClassLoader());
@@ -185,6 +193,30 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao
return mainMethod;
}
private static Method findMainJavaMethod(Class<?> type) {
try {
Method method = getMainMethod(type);
if (Modifier.isStatic(method.getModifiers())) {
method.setAccessible(true);
return method;
}
}
catch (Exception ex) {
// Ignore
}
return null;
}
private static Method getMainMethod(Class<?> type) throws NoSuchMethodException {
try {
return type.getDeclaredMethod("main", String[].class);
}
catch (NoSuchMethodException ex) {
return type.getDeclaredMethod("main");
}
}
private boolean isSpringBootConfiguration(Class<?> candidate) {
return MergedAnnotations.from(candidate, SearchStrategy.TYPE_HIERARCHY)
.isPresent(SpringBootConfiguration.class);
@@ -24,6 +24,8 @@ import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
@@ -199,10 +201,13 @@ class SpringBootContextLoaderTests {
assertThat(applicationContext.getEnvironment().getActiveProfiles()).isEmpty();
}
@Test
void whenUseMainMethodWhenAvailableAndMainMethod() {
TestContext testContext = new ExposedTestContextManager(UseMainMethodWhenAvailableAndMainMethod.class)
.getExposedTestContext();
@ParameterizedTest
@ValueSource(classes = { UsePublicMainMethodWhenAvailableAndMainMethod.class,
UsePublicParameterlessMainMethodWhenAvailableAndMainMethod.class,
UsePackagePrivateMainMethodWhenAvailableAndMainMethod.class,
UsePackagePrivateParameterlessMainMethodWhenAvailableAndMainMethod.class })
void whenUseMainMethodWhenAvailableAndMainMethod(Class<?> testClass) {
TestContext testContext = new ExposedTestContextManager(testClass).getExposedTestContext();
ApplicationContext applicationContext = testContext.getApplicationContext();
assertThat(applicationContext.getEnvironment().getActiveProfiles()).contains("frommain");
}
@@ -264,11 +269,11 @@ class SpringBootContextLoaderTests {
void whenMainMethodPresentRegisterReflectionHints() throws Exception {
SpringBootContextLoader contextLoader = new SpringBootContextLoader();
MergedContextConfiguration contextConfiguration = BootstrapUtils
.resolveTestContextBootstrapper(UseMainMethodWhenAvailableAndMainMethod.class)
.resolveTestContextBootstrapper(UsePublicMainMethodWhenAvailableAndMainMethod.class)
.buildMergedContextConfiguration();
RuntimeHints runtimeHints = new RuntimeHints();
contextLoader.loadContextForAotProcessing(contextConfiguration, runtimeHints);
assertThat(RuntimeHintsPredicates.reflection().onMethod(ConfigWithMain.class, "main").invoke())
assertThat(RuntimeHintsPredicates.reflection().onMethod(ConfigWithPublicMain.class, "main").invoke())
.accepts(runtimeHints);
}
@@ -370,12 +375,28 @@ class SpringBootContextLoaderTests {
}
@SpringBootTest(classes = ConfigWithMain.class, useMainMethod = UseMainMethod.WHEN_AVAILABLE)
static class UseMainMethodWhenAvailableAndMainMethod {
@SpringBootTest(classes = ConfigWithPublicMain.class, useMainMethod = UseMainMethod.WHEN_AVAILABLE)
static class UsePublicMainMethodWhenAvailableAndMainMethod {
}
@SpringBootTest(classes = ConfigWithMain.class, useMainMethod = UseMainMethod.NEVER)
@SpringBootTest(classes = ConfigWithPublicParameterlessMain.class, useMainMethod = UseMainMethod.WHEN_AVAILABLE)
static class UsePublicParameterlessMainMethodWhenAvailableAndMainMethod {
}
@SpringBootTest(classes = ConfigWithPackagePrivateMain.class, useMainMethod = UseMainMethod.WHEN_AVAILABLE)
static class UsePackagePrivateMainMethodWhenAvailableAndMainMethod {
}
@SpringBootTest(classes = ConfigWithPackagePrivateParameterlessMain.class,
useMainMethod = UseMainMethod.WHEN_AVAILABLE)
static class UsePackagePrivateParameterlessMainMethodWhenAvailableAndMainMethod {
}
@SpringBootTest(classes = ConfigWithPublicMain.class, useMainMethod = UseMainMethod.NEVER)
static class UseMainMethodNever {
}
@@ -391,7 +412,7 @@ class SpringBootContextLoaderTests {
}
@SpringBootTest(useMainMethod = UseMainMethod.ALWAYS)
@ContextHierarchy({ @ContextConfiguration(classes = ConfigWithMain.class),
@ContextHierarchy({ @ContextConfiguration(classes = ConfigWithPublicMain.class),
@ContextConfiguration(classes = AnotherConfigWithMain.class) })
static class UseMainMethodWithContextHierarchy {
@@ -422,10 +443,37 @@ class SpringBootContextLoaderTests {
}
@SpringBootConfiguration(proxyBeanMethods = false)
public static class ConfigWithMain {
public static class ConfigWithPublicMain {
public static void main(String[] args) {
new SpringApplication(ConfigWithMain.class).run("--spring.profiles.active=frommain");
new SpringApplication(ConfigWithPublicMain.class).run("--spring.profiles.active=frommain");
}
}
@SpringBootConfiguration(proxyBeanMethods = false)
public static class ConfigWithPublicParameterlessMain {
public static void main() {
new SpringApplication(ConfigWithPublicMain.class).run("--spring.profiles.active=frommain");
}
}
@SpringBootConfiguration(proxyBeanMethods = false)
public static class ConfigWithPackagePrivateMain {
static void main(String[] args) {
new SpringApplication(ConfigWithPublicMain.class).run("--spring.profiles.active=frommain");
}
}
@SpringBootConfiguration(proxyBeanMethods = false)
public static class ConfigWithPackagePrivateParameterlessMain {
static void main() {
new SpringApplication(ConfigWithPublicMain.class).run("--spring.profiles.active=frommain");
}
}