diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java index 3b291243060..764657883d6 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java @@ -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; @@ -140,7 +141,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); @@ -177,7 +185,7 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao } private static @Nullable Method findMainMethod(@Nullable 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 { Assert.state(type != null, "'type' must not be null"); @@ -191,6 +199,30 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao return mainMethod; } + private static @Nullable 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); diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java index 36d93200e1c..c15967226ea 100644 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java @@ -25,6 +25,8 @@ import org.jspecify.annotations.Nullable; 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().onMethodInvocation(ConfigWithMain.class, "main")) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(ConfigWithPublicMain.class, "main")) .accepts(runtimeHints); } @@ -371,12 +376,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 = ConfigWithPublicParameterlessMain.class, useMainMethod = UseMainMethod.WHEN_AVAILABLE) + static class UsePublicParameterlessMainMethodWhenAvailableAndMainMethod { } - @SpringBootTest(classes = ConfigWithMain.class, useMainMethod = UseMainMethod.NEVER) + @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 { } @@ -392,7 +413,7 @@ class SpringBootContextLoaderTests { } @SpringBootTest(useMainMethod = UseMainMethod.ALWAYS) - @ContextHierarchy({ @ContextConfiguration(classes = ConfigWithMain.class), + @ContextHierarchy({ @ContextConfiguration(classes = ConfigWithPublicMain.class), @ContextConfiguration(classes = AnotherConfigWithMain.class) }) static class UseMainMethodWithContextHierarchy { @@ -423,10 +444,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"); } }