diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java index f3627ecfa8c..14afec5c223 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java @@ -82,16 +82,6 @@ import org.springframework.util.StringUtils; */ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstrapper { - private static final String[] WEB_ENVIRONMENT_CLASSES = { "jakarta.servlet.Servlet", - "org.springframework.web.context.ConfigurableWebApplicationContext" }; - - private static final String REACTIVE_WEB_ENVIRONMENT_CLASS = "org.springframework." - + "web.reactive.DispatcherHandler"; - - private static final String MVC_WEB_ENVIRONMENT_CLASS = "org.springframework.web.servlet.DispatcherServlet"; - - private static final String JERSEY_WEB_ENVIRONMENT_CLASS = "org.glassfish.jersey.server.ResourceConfig"; - private static final String ACTIVATE_SERVLET_LISTENER = "org.springframework.test." + "context.web.ServletTestExecutionListener.activateListener"; @@ -112,7 +102,7 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr TestContext context = super.buildTestContext(); verifyConfiguration(context.getTestClass()); WebEnvironment webEnvironment = getWebEnvironment(context.getTestClass()); - if (webEnvironment == WebEnvironment.MOCK && deduceWebApplicationType() == WebApplicationType.SERVLET) { + if (webEnvironment == WebEnvironment.MOCK && WebApplicationType.deduce() == WebApplicationType.SERVLET) { context.setAttribute(ACTIVATE_SERVLET_LISTENER, true); } else if (webEnvironment != null && webEnvironment.isEmbedded()) { @@ -171,21 +161,7 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr TestPropertySourceUtils.convertInlinedPropertiesToMap(configuration.getPropertySourceProperties())); Binder binder = new Binder(source); return binder.bind("spring.main.web-application-type", Bindable.of(WebApplicationType.class)) - .orElseGet(this::deduceWebApplicationType); - } - - private WebApplicationType deduceWebApplicationType() { - if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null) - && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null) - && !ClassUtils.isPresent(JERSEY_WEB_ENVIRONMENT_CLASS, null)) { - return WebApplicationType.REACTIVE; - } - for (String className : WEB_ENVIRONMENT_CLASSES) { - if (!ClassUtils.isPresent(className, null)) { - return WebApplicationType.NONE; - } - } - return WebApplicationType.SERVLET; + .orElseGet(WebApplicationType::deduce); } /** diff --git a/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index b0e1bcf6493..c38474584f6 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -275,7 +275,7 @@ public class SpringApplication { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "'primarySources' must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); - this.properties.setWebApplicationType(WebApplicationType.deduceFromClasspath()); + this.properties.setWebApplicationType(WebApplicationType.deduce()); this.bootstrapRegistryInitializers = new ArrayList<>( getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/WebApplicationType.java b/core/spring-boot/src/main/java/org/springframework/boot/WebApplicationType.java index 22854e9c1a1..09f80862771 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/WebApplicationType.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/WebApplicationType.java @@ -21,6 +21,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; +import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.ClassUtils; /** @@ -28,6 +29,7 @@ import org.springframework.util.ClassUtils; * * @author Andy Wilkinson * @author Brian Clozel + * @author Phillip Webb * @since 2.0.0 */ public enum WebApplicationType { @@ -53,23 +55,28 @@ public enum WebApplicationType { private static final String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext" }; - private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet"; - - private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler"; - - private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer"; - - static WebApplicationType deduceFromClasspath() { - if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) - && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) { - return WebApplicationType.REACTIVE; + /** + * Deduce the {@link WebApplicationType} from the current classpath. + * @return the deduced web application + * @since 4.0.1 + */ + public static WebApplicationType deduce() { + for (Deducer deducer : SpringFactoriesLoader.forDefaultResourceLocation().load(Deducer.class)) { + WebApplicationType deduced = deducer.deduceWebApplicationType(); + if (deduced != null) { + return deduced; + } } - for (String className : SERVLET_INDICATOR_CLASSES) { - if (!ClassUtils.isPresent(className, null)) { - return WebApplicationType.NONE; + return isServletApplication() ? WebApplicationType.SERVLET : WebApplicationType.NONE; + } + + private static boolean isServletApplication() { + for (String servletIndicatorClass : SERVLET_INDICATOR_CLASSES) { + if (!ClassUtils.isPresent(servletIndicatorClass, null)) { + return false; } } - return WebApplicationType.SERVLET; + return true; } static class WebApplicationTypeRuntimeHints implements RuntimeHintsRegistrar { @@ -79,9 +86,6 @@ public enum WebApplicationType { for (String servletIndicatorClass : SERVLET_INDICATOR_CLASSES) { registerTypeIfPresent(servletIndicatorClass, classLoader, hints); } - registerTypeIfPresent(JERSEY_INDICATOR_CLASS, classLoader, hints); - registerTypeIfPresent(WEBFLUX_INDICATOR_CLASS, classLoader, hints); - registerTypeIfPresent(WEBMVC_INDICATOR_CLASS, classLoader, hints); } private void registerTypeIfPresent(String typeName, @Nullable ClassLoader classLoader, RuntimeHints hints) { @@ -92,4 +96,21 @@ public enum WebApplicationType { } + /** + * Strategy that may be implemented by a module that can deduce the + * {@link WebApplicationType}. + * + * @since 4.0.1 + */ + @FunctionalInterface + public interface Deducer { + + /** + * Deduce the web application type. + * @return the deduced web application type or {@code null} + */ + @Nullable WebApplicationType deduceWebApplicationType(); + + } + } diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/WebFluxWebApplicationTypeDeducer.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/WebFluxWebApplicationTypeDeducer.java new file mode 100644 index 00000000000..0088ac44438 --- /dev/null +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/WebFluxWebApplicationTypeDeducer.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.webflux; + +import org.jspecify.annotations.Nullable; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.boot.WebApplicationType; +import org.springframework.core.annotation.Order; +import org.springframework.util.ClassUtils; + +/** + * {@link WebApplicationType} deducer to ensure Spring WebFlux applications use + * {@link WebApplicationType#REACTIVE}. + * + * @author Phillip Webb + */ +@Order(20) // Ordered after MVC +class WebFluxWebApplicationTypeDeducer implements WebApplicationType.Deducer { + + private static final String[] INDICATOR_CLASSES = { "reactor.core.publisher.Mono", + "org.springframework.web.reactive.DispatcherHandler" }; + + @Override + public @Nullable WebApplicationType deduceWebApplicationType() { + // Guard in case the classic module is being used and dependencies are excluded + for (String indicatorClass : INDICATOR_CLASSES) { + if (!ClassUtils.isPresent(indicatorClass, null)) { + return null; + } + } + return WebApplicationType.REACTIVE; + } + + static class WebFluxWebApplicationTypeDeducerRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + for (String servletIndicatorClass : INDICATOR_CLASSES) { + registerTypeIfPresent(servletIndicatorClass, classLoader, hints); + } + } + + private void registerTypeIfPresent(String typeName, @Nullable ClassLoader classLoader, RuntimeHints hints) { + if (ClassUtils.isPresent(typeName, classLoader)) { + hints.reflection().registerType(TypeReference.of(typeName)); + } + } + + } + +} diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/package-info.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/package-info.java new file mode 100644 index 00000000000..62f9a67755e --- /dev/null +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Core integration between Spring Boot and Spring WebFlux. + */ +@NullMarked +package org.springframework.boot.webflux; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-webflux/src/main/resources/META-INF/spring.factories b/module/spring-boot-webflux/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..8bbdca873b1 --- /dev/null +++ b/module/spring-boot-webflux/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# WebApplicationType Deducers +org.springframework.boot.WebApplicationType$Deducer=\ +org.springframework.boot.webflux.WebFluxWebApplicationTypeDeducer diff --git a/module/spring-boot-webflux/src/main/resources/META-INF/spring/aot.factories b/module/spring-boot-webflux/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000000..c6cdd2c5879 --- /dev/null +++ b/module/spring-boot-webflux/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=\ +org.springframework.boot.webflux.WebFluxWebApplicationTypeDeducer$WebFluxWebApplicationTypeDeducerRuntimeHints diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/WebMvcWebApplicationTypeDeducer.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/WebMvcWebApplicationTypeDeducer.java new file mode 100644 index 00000000000..4ff6607896f --- /dev/null +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/WebMvcWebApplicationTypeDeducer.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-present 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.webmvc; + +import org.jspecify.annotations.Nullable; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.boot.WebApplicationType; +import org.springframework.core.annotation.Order; +import org.springframework.util.ClassUtils; + +/** + * {@link WebApplicationType} deducer to ensure Spring MVC applications use + * {@link WebApplicationType#SERVLET}. + * + * @author Phillip Webb + */ +@Order(10) // Ordered after WebFlux +class WebMvcWebApplicationTypeDeducer implements WebApplicationType.Deducer { + + private static final String[] INDICATOR_CLASSES = { "jakarta.servlet.Servlet", + "org.springframework.web.servlet.DispatcherServlet", + "org.springframework.web.context.ConfigurableWebApplicationContext" }; + + @Override + public @Nullable WebApplicationType deduceWebApplicationType() { + // Guard in case the classic module is being used and dependencies are excluded + for (String indicatorClass : INDICATOR_CLASSES) { + if (!ClassUtils.isPresent(indicatorClass, null)) { + return null; + } + } + return WebApplicationType.SERVLET; + } + + static class WebMvcWebApplicationTypeDeducerRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + for (String servletIndicatorClass : INDICATOR_CLASSES) { + registerTypeIfPresent(servletIndicatorClass, classLoader, hints); + } + } + + private void registerTypeIfPresent(String typeName, @Nullable ClassLoader classLoader, RuntimeHints hints) { + if (ClassUtils.isPresent(typeName, classLoader)) { + hints.reflection().registerType(TypeReference.of(typeName)); + } + } + + } + +} diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/package-info.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/package-info.java new file mode 100644 index 00000000000..8f49778ba3c --- /dev/null +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Core integration between Spring Boot and Spring Web MVC. + */ +@NullMarked +package org.springframework.boot.webmvc; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-webmvc/src/main/resources/META-INF/spring.factories b/module/spring-boot-webmvc/src/main/resources/META-INF/spring.factories index 128c571ee37..e5c371556c9 100644 --- a/module/spring-boot-webmvc/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-webmvc/src/main/resources/META-INF/spring.factories @@ -1,3 +1,7 @@ # Template Availability Providers org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\ org.springframework.boot.webmvc.autoconfigure.JspTemplateAvailabilityProvider + +# WebApplicationType Deducers +org.springframework.boot.WebApplicationType$Deducer=\ +org.springframework.boot.webmvc.WebMvcWebApplicationTypeDeducer diff --git a/module/spring-boot-webmvc/src/main/resources/META-INF/spring/aot.factories b/module/spring-boot-webmvc/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000000..22c95f013f4 --- /dev/null +++ b/module/spring-boot-webmvc/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=\ +org.springframework.boot.webmvc.WebMvcWebApplicationTypeDeducer$WebMvcWebApplicationTypeDeducerRuntimeHints diff --git a/smoke-test/spring-boot-smoke-test-web-application-type/build.gradle b/smoke-test/spring-boot-smoke-test-web-application-type/build.gradle index e0ae1808344..b06db8d3a22 100644 --- a/smoke-test/spring-boot-smoke-test-web-application-type/build.gradle +++ b/smoke-test/spring-boot-smoke-test-web-application-type/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation(project(":starter:spring-boot-starter-webflux")) testImplementation(project(":starter:spring-boot-starter-test")) + testImplementation(project(":test-support:spring-boot-test-support")) } tasks.named("compileTestJava") { diff --git a/smoke-test/spring-boot-smoke-test-web-application-type/src/test/java/smoketest/webapplicationtype/WebApplicationTypeIntegrationTests.java b/smoke-test/spring-boot-smoke-test-web-application-type/src/test/java/smoketest/webapplicationtype/WebApplicationTypeIntegrationTests.java new file mode 100644 index 00000000000..d4478fd470d --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-web-application-type/src/test/java/smoketest/webapplicationtype/WebApplicationTypeIntegrationTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-present 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.webapplicationtype; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link WebApplicationType} deducers. + * + * @author Phillip Webb + */ +class WebApplicationTypeIntegrationTests { + + @Test + @ClassPathExclusions("spring-boot-webflux*") + void deduceWhenNoWebFluxModuleAndWebMvcModule() { + assertThat(WebApplicationType.deduce()).isEqualTo(WebApplicationType.SERVLET); + } + + @Test + @ClassPathExclusions({ "spring-boot-webflux*", "spring-web-*" }) + void deduceWhenNoWebFluxModuleAndNoSpringWebAndWebMvcModule() { + assertThat(WebApplicationType.deduce()).isEqualTo(WebApplicationType.NONE); + } + + @Test + @ClassPathExclusions("spring-boot-webmvc*") + void deduceWhenNoWebMvcModuleAndWebFluxModule() { + assertThat(WebApplicationType.deduce()).isEqualTo(WebApplicationType.REACTIVE); + } + + @Test + @ClassPathExclusions({ "spring-boot-webmvc*", "spring-webflux-*" }) + void deduceWhenNoWebMvcModuleAndNoWebFluxAndWebFluxModule() { + assertThat(WebApplicationType.deduce()).isEqualTo(WebApplicationType.SERVLET); + } + + @Test + @ClassPathExclusions({ "spring-boot-webmvc*", "spring-webflux-*", "spring-web-*" }) + void deduceWhenNoWebMvcModuleAndNoWebFluxAndNoWebAndWebFluxModule() { + assertThat(WebApplicationType.deduce()).isEqualTo(WebApplicationType.NONE); + } + + @Test + void deduceWhenWebMvcAndWebFlux() { + assertThat(WebApplicationType.deduce()).isEqualTo(WebApplicationType.SERVLET); + } + + @Test + @ClassPathExclusions("spring-webmvc-*") + void deduceWhenNoWebMvc() { + assertThat(WebApplicationType.deduce()).isEqualTo(WebApplicationType.REACTIVE); + } + + @Test + @ClassPathExclusions({ "spring-webmvc-*", "spring-webflux-*" }) + void deduceWhenNoWebMvcAndNoWebFlux() { + assertThat(WebApplicationType.deduce()).isEqualTo(WebApplicationType.SERVLET); + } + + @Test + @ClassPathExclusions({ "spring-web-*" }) + void deduceWhenNoWeb() { + assertThat(WebApplicationType.deduce()).isEqualTo(WebApplicationType.NONE); + } + + @Test + @ClassPathExclusions(packages = "jakarta.servlet", files = "spring-webflux-*") + void deduceWhenNoServletOrWebFlux() { + assertThat(WebApplicationType.deduce()).isEqualTo(WebApplicationType.NONE); + } + +}