diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/README.md b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/README.md index b5f51b0ec..354a03473 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/README.md +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/README.md @@ -8,6 +8,12 @@ A sample is provided in [sample](https://github.com/spring-cloud/spring-cloud-fu _NOTE: Although this module is AWS specific, this dependency is protocol only (not binary), therefore there is no AWS dependnecies._ +_NOTE: The serverless `ServletWebServerFactory` is declared with `@ConditionalOnMissingBean`. If your +application defines its own `ServletWebServerFactory` bean (for example Tomcat/Jetty/Undertow customization), +that custom bean will take precedence and can disable the serverless adapter path. For serverless-web usage, +do not provide a competing `ServletWebServerFactory` bean unless it delegates to +`ServerlessAutoConfiguration.ServerlessServletWebServerFactory`._ + The aformentioned proxy is identified as AWS Lambda [handler](https://github.com/spring-cloud/spring-cloud-function/blob/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml#L14) The main Spring Boot configuration file is identified as [MAIN_CLASS](https://github.com/spring-cloud/spring-cloud-function/blob/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml#L22) diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/FunctionClassUtils.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/FunctionClassUtils.java index c97d23cbf..0b3f6b533 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/FunctionClassUtils.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/FunctionClassUtils.java @@ -42,7 +42,7 @@ */ public final class FunctionClassUtils { - private static Log logger = LogFactory.getLog(FunctionClassUtils.class); + private static final Log LOGGER = LogFactory.getLog(FunctionClassUtils.class); private static Class MAIN_CLASS; @@ -91,20 +91,20 @@ else if (System.getProperty("MAIN_CLASS") != null) { + "entry in META-INF/MANIFEST.MF (in that order).", ex); } } - logger.info("Main class: " + mainClass); + LOGGER.info("Main class: " + mainClass); return mainClass; } private static Class getStartClass(List list, ClassLoader classLoader) { - if (logger.isTraceEnabled()) { - logger.trace("Searching manifests: " + list); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Searching manifests: " + list); } for (URL url : list) { try { InputStream inputStream = null; Manifest manifest = new Manifest(url.openStream()); - logger.info("Searching for start class in manifest: " + url); - if (logger.isDebugEnabled()) { + LOGGER.info("Searching for start class in manifest: " + url); + if (LOGGER.isDebugEnabled()) { manifest.write(System.out); } try { @@ -135,7 +135,7 @@ private static Class getStartClass(List list, ClassLoader classLoader) { } } catch (Exception ex) { - logger.debug("Failed to determine Start-Class in manifest file of " + url, ex); + LOGGER.debug("Failed to determine Start-Class in manifest file of " + url, ex); } } return null; diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAsyncContext.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAsyncContext.java index f50c4826b..1b69c4f3e 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAsyncContext.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAsyncContext.java @@ -30,9 +30,9 @@ import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.util.WebUtils; diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfiguration.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfiguration.java index 2183b28c2..7364bd32c 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfiguration.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; @@ -39,13 +40,20 @@ * @author Oleg Zhurakousky * @since 4.x */ +@AutoConfiguration(beforeName = { + "org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration", + "org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration", + "org.springframework.boot.jetty.autoconfigure.servlet.JettyServletWebServerAutoConfiguration", + "org.springframework.boot.undertow.autoconfigure.servlet.UndertowServletWebServerAutoConfiguration" +}) @Configuration(proxyBeanMethods = false) public class ServerlessAutoConfiguration { - private static Log logger = LogFactory.getLog(ServerlessAutoConfiguration.class); + private static final Log LOGGER = LogFactory.getLog(ServerlessAutoConfiguration.class); @Bean @ConditionalOnMissingBean public ServletWebServerFactory servletWebServerFactory() { + // A user-defined ServletWebServerFactory bean will override this and may bypass serverless initialization. return new ServerlessServletWebServerFactory(); } @@ -82,14 +90,14 @@ public void setApplicationContext(ApplicationContext applicationContext) throws @Override public void afterPropertiesSet() throws Exception { if (applicationContext instanceof ServletWebServerApplicationContext servletApplicationContext) { - logger.info("Configuring Serverless Web Container"); + LOGGER.info("Configuring Serverless Web Container"); ServerlessServletContext servletContext = new ServerlessServletContext(); servletApplicationContext.setServletContext(servletContext); DispatcherServlet dispatcher = applicationContext.getBean(DispatcherServlet.class); try { - logger.info("Initializing DispatcherServlet"); + LOGGER.info("Initializing DispatcherServlet"); dispatcher.init(new ProxyServletConfig(servletApplicationContext.getServletContext())); - logger.info("Initialized DispatcherServlet"); + LOGGER.info("Initialized DispatcherServlet"); } catch (Exception e) { throw new IllegalStateException("Failed to create Spring MVC DispatcherServlet proxy", e); @@ -99,7 +107,7 @@ public void afterPropertiesSet() throws Exception { } } else { - logger.debug("Skipping Serverless configuration for " + this.applicationContext); + LOGGER.debug("Skipping Serverless configuration for " + this.applicationContext); } } } diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletRequest.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletRequest.java index a2c5e56f2..b001e9e61 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletRequest.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletRequest.java @@ -58,9 +58,9 @@ import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpUpgradeHandler; import jakarta.servlet.http.Part; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; @@ -559,9 +559,7 @@ public void clearAttributes() { * not take into consideration any locales specified via the * {@code Accept-Language} header. * - * @see javax.servlet.ServletRequest#getLocale() - * @see #addPreferredLocale(Locale) - * @see #setPreferredLocales(List) + * @see jakarta.servlet.ServletRequest#getLocale() */ @Override public Locale getLocale() { @@ -580,9 +578,7 @@ public Locale getLocale() { * not take into consideration any locales specified via the * {@code Accept-Language} header. * - * @see javax.servlet.ServletRequest#getLocales() - * @see #addPreferredLocale(Locale) - * @see #setPreferredLocales(List) + * @see jakarta.servlet.ServletRequest#getLocales() */ @Override public Enumeration getLocales() { @@ -590,10 +586,9 @@ public Enumeration getLocales() { } /** - * Return {@code true} if the {@link #setSecure secure} flag has been set to - * {@code true} or if the {@link #getScheme scheme} is {@code https}. + * Return {@code true} if the {@link #getScheme scheme} is {@code https}. * - * @see javax.servlet.ServletRequest#isSecure() + * @see jakarta.servlet.ServletRequest#isSecure() */ @Override public boolean isSecure() { diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletResponse.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletResponse.java index cef5ec035..041922229 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletResponse.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletResponse.java @@ -38,9 +38,9 @@ import jakarta.servlet.WriteListener; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.util.WebUtils; @@ -118,8 +118,7 @@ public byte[] getContentAsByteArray() { * specified for the response by the application, either through * {@link HttpServletResponse} methods or through a charset parameter on the * {@code Content-Type}. If no charset has been explicitly defined, the - * {@linkplain #setDefaultCharacterEncoding(String) default character encoding} - * will be used. + * default character encoding will be used. * * @return the content as a {@code String} * @throws UnsupportedEncodingException if the character encoding is not diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessMVC.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessMVC.java index 0c4e57021..bcb3a7c52 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessMVC.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessMVC.java @@ -23,6 +23,7 @@ import java.util.Enumeration; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -33,6 +34,7 @@ import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; +import jakarta.servlet.FilterRegistration; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.Servlet; import jakarta.servlet.ServletConfig; @@ -44,12 +46,12 @@ import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.boot.SpringApplication; import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration; import org.springframework.http.HttpStatus; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -72,12 +74,15 @@ public final class ServerlessMVC { */ public static String INIT_TIMEOUT = "contextInitTimeout"; - private static Log LOG = LogFactory.getLog(ServerlessMVC.class); + private static final Log LOGGER = LogFactory.getLog(ServerlessMVC.class); private volatile DispatcherServlet dispatcher; private volatile ServletWebServerApplicationContext applicationContext; + @Nullable + private volatile Throwable startupFailure; + private final CountDownLatch contextStartupLatch = new CountDownLatch(1); private final long initializationTimeout; @@ -107,16 +112,18 @@ private ServerlessMVC() { private void initializeContextAsync(Class... componentClasses) { new Thread(() -> { try { - LOG.info("Starting application with the following configuration classes:"); - Stream.of(componentClasses).forEach(clazz -> LOG.info(clazz.getSimpleName())); + LOGGER.info("Starting application with the following configuration classes:"); + Stream.of(componentClasses).forEach(clazz -> LOGGER.info(clazz.getSimpleName())); initContext(componentClasses); } catch (Exception e) { - throw new IllegalStateException(e); + this.startupFailure = e; + LOGGER.error("Application failed to initialize.", e); } finally { contextStartupLatch.countDown(); - LOG.info("Application is started successfully."); + LOGGER.info((this.startupFailure == null) ? "Application is started successfully." + : "Application startup finished with errors."); } }).start(); } @@ -126,41 +133,39 @@ private void initContext(Class... componentClasses) { if (this.applicationContext.containsBean(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) { this.dispatcher = this.applicationContext.getBean(DispatcherServlet.class); } + Assert.state(this.dispatcher != null, "DispatcherServlet bean was not initialized. " + + "Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory."); } public ConfigurableWebApplicationContext getApplicationContext() { - this.waitForContext(); + this.assertContextReady(); return this.applicationContext; } public ServletContext getServletContext() { - this.waitForContext(); + this.assertContextReady(); return this.dispatcher.getServletContext(); } public void stop() { - this.waitForContext(); + this.assertContextReady(); this.applicationContext.stop(); } /** - * Perform a request and return a type that allows chaining further actions, - * such as asserting expectations, on the result. + * Process a serverless request through the configured servlet/filter chain. * - * @param requestBuilder used to prepare the request to execute; see static - * factory methods in - * {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders} - * @return an instance of {@link ResultActions} (never {@code null}) - * @see org.springframework.test.web.servlet.request.MockMvcRequestBuilders - * @see org.springframework.test.web.servlet.result.MockMvcResultMatchers + * @param request the incoming request + * @param response the outgoing response */ public void service(HttpServletRequest request, HttpServletResponse response) throws Exception { - Assert.state(this.waitForContext(), "Failed to initialize Application within the specified time of " + this.initializationTimeout + " milliseconds. " - + "If you need to increase it, please set " + INIT_TIMEOUT + " environment variable"); + this.assertContextReady(); this.service(request, response, (CountDownLatch) null); } public void service(HttpServletRequest request, HttpServletResponse response, CountDownLatch latch) throws Exception { + Assert.state(this.dispatcher != null, "DispatcherServlet is not initialized. " + + "Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory."); ProxyFilterChain filterChain = new ProxyFilterChain(this.dispatcher); filterChain.doFilter(request, response); @@ -195,6 +200,18 @@ public boolean waitForContext() { return false; } + private void assertContextReady() { + Assert.state(this.waitForContext(), "Failed to initialize Application within the specified time of " + this.initializationTimeout + " milliseconds. " + + "If you need to increase it, please set " + INIT_TIMEOUT + " environment variable"); + if (this.startupFailure != null) { + throw new IllegalStateException("Application context failed to initialize. " + + "Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory.", this.startupFailure); + } + Assert.state(this.dispatcher != null, "DispatcherServlet is not initialized. " + + "Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory."); + Assert.state(this.applicationContext != null, "ApplicationContext is not initialized."); + } + private static class ProxyFilterChain implements FilterChain { @Nullable @@ -217,7 +234,17 @@ private static class ProxyFilterChain implements FilterChain { */ ProxyFilterChain(DispatcherServlet servlet) { List filters = new ArrayList<>(); - servlet.getServletContext().getFilterRegistrations().values().forEach(fr -> filters.add(((ServerlessFilterRegistration) fr).getFilter())); + for (Map.Entry entry : servlet.getServletContext().getFilterRegistrations() + .entrySet()) { + FilterRegistration registration = entry.getValue(); + if (registration instanceof ServerlessFilterRegistration serverlessFilterRegistration) { + filters.add(serverlessFilterRegistration.getFilter()); + } + else { + LOGGER.debug("Skipping unsupported filter registration type '" + registration.getClass().getName() + + "' for filter '" + entry.getKey() + "'"); + } + } Assert.notNull(filters, "filters cannot be null"); Assert.noNullElements(filters, "filters cannot contain null values"); this.filters = initFilterList(servlet, filters.toArray(new Filter[] {})); @@ -309,7 +336,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha ((ServerlessHttpServletRequest) request).setRequestURI("/error"); } - LOG.error("Failed processing the request to: " + ((HttpServletRequest) request).getRequestURI(), e); + LOGGER.error("Failed processing the request to: " + ((HttpServletRequest) request).getRequestURI(), e); this.delegateServlet.service(request, response); } diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessServletContext.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessServletContext.java index 9f54ffecf..89b723047 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessServletContext.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessServletContext.java @@ -58,7 +58,7 @@ */ public class ServerlessServletContext implements ServletContext { - private Log logger = LogFactory.getLog(ServerlessServletContext.class); + private static final Log LOGGER = LogFactory.getLog(ServerlessServletContext.class); private HashMap attributes = new HashMap<>(); @@ -145,12 +145,12 @@ public RequestDispatcher getNamedDispatcher(String name) { @Override public void log(String msg) { - this.logger.info(msg); + this.LOGGER.info(msg); } @Override public void log(String message, Throwable throwable) { - this.logger.error(message, throwable); + this.LOGGER.error(message, throwable); } @Override diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessWebApplication.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessWebApplication.java index 5eea2e757..80494e3b9 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessWebApplication.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessWebApplication.java @@ -74,7 +74,7 @@ */ public class ServerlessWebApplication extends SpringApplication { - private static final Log logger = LogFactory.getLog(ServerlessWebApplication.class); + private static final Log LOGGER = LogFactory.getLog(ServerlessWebApplication.class); private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; @@ -151,7 +151,7 @@ private SpringApplicationRunListeners getRunListeners(String[] args) { ArgumentResolver argumentResolver = ArgumentResolver.of(SpringApplication.class, this); argumentResolver = argumentResolver.and(String[].class, args); List listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class, argumentResolver); - return new SpringApplicationRunListeners(logger, listeners, this.applicationStartup); + return new SpringApplicationRunListeners(LOGGER, listeners, this.applicationStartup); } private Banner printBanner(ConfigurableEnvironment environment) { diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java index 7217f5071..b598724c2 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java @@ -16,8 +16,12 @@ package org.springframework.cloud.function.serverless.web; +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; import java.util.List; +import java.util.Map; +import jakarta.servlet.FilterRegistration; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -195,4 +199,38 @@ public void validatePostAsyncWithBody() throws Exception { assertThat(pet.getName()).isNotEmpty(); } + @Test + public void validateNonServerlessFilterRegistrationIsSkipped() throws Exception { + ServerlessServletContext servletContext = (ServerlessServletContext) this.mvc.getServletContext(); + Field registrationsField = ServerlessServletContext.class.getDeclaredField("filterRegistrations"); + registrationsField.setAccessible(true); + @SuppressWarnings("unchecked") + Map registrations = + (Map) registrationsField.get(servletContext); + + FilterRegistration nonServerlessRegistration = (FilterRegistration) Proxy.newProxyInstance( + FilterRegistration.class.getClassLoader(), + new Class[]{FilterRegistration.class}, + (proxy, method, args) -> { + if ("getName".equals(method.getName())) { + return "nonServerless"; + } + if (method.getReturnType().isPrimitive()) { + if (method.getReturnType() == boolean.class) { + return false; + } + return 0; + } + return null; + }); + + registrations.put("nonServerless", nonServerlessRegistration); + + HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/pets"); + ServerlessHttpServletResponse response = new ServerlessHttpServletResponse(); + this.mvc.service(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + } + } diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfigurationTests.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfigurationTests.java new file mode 100644 index 000000000..76ce32129 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfigurationTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024-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.cloud.function.serverless.web; + +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.server.WebServerException; +import org.springframework.boot.web.server.servlet.ServletWebServerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ServerlessAutoConfigurationTests { + + @Test + void autoConfigurationOrderingCoversSupportedServletContainers() { + AutoConfiguration autoConfiguration = ServerlessAutoConfiguration.class.getAnnotation(AutoConfiguration.class); + assertThat(autoConfiguration).isNotNull(); + + assertThat(autoConfiguration.beforeName()).contains( + "org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration", + "org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration", + "org.springframework.boot.jetty.autoconfigure.servlet.JettyServletWebServerAutoConfiguration", + "org.springframework.boot.undertow.autoconfigure.servlet.UndertowServletWebServerAutoConfiguration"); + } + + @Test + void missingServerlessAutoConfigurationFailsWithUsefulError() { + System.setProperty(ServerlessMVC.INIT_TIMEOUT, "5000"); + ServerlessMVC mvc = ServerlessMVC.INSTANCE(ApplicationWithoutServerlessAutoConfiguration.class); + HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/hello"); + ServerlessHttpServletResponse response = new ServerlessHttpServletResponse(); + + assertThatThrownBy(() -> mvc.service(request, response)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Application context failed to initialize") + .hasMessageContaining("Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory."); + } + + @Test + void customServletWebServerFactoryFailsWithUsefulErrorInsteadOfNpe() { + System.setProperty(ServerlessMVC.INIT_TIMEOUT, "5000"); + ServerlessMVC mvc = ServerlessMVC.INSTANCE(ApplicationWithCustomServletWebServerFactory.class); + HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/hello"); + ServerlessHttpServletResponse response = new ServerlessHttpServletResponse(); + + assertThatThrownBy(() -> mvc.service(request, response)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Application context failed to initialize") + .hasMessageContaining("Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory."); + } + + @Test + void failedStartupGetServletContextThrowsUsefulErrorInsteadOfNpe() { + System.setProperty(ServerlessMVC.INIT_TIMEOUT, "5000"); + ServerlessMVC mvc = ServerlessMVC.INSTANCE(ApplicationWithCustomServletWebServerFactory.class); + + assertThatThrownBy(mvc::getServletContext) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Application context failed to initialize") + .hasMessageContaining("Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory."); + } + + @AfterEach + void clearInitTimeoutOverride() { + System.clearProperty(ServerlessMVC.INIT_TIMEOUT); + } + + @RestController + @SpringBootApplication(excludeName = "org.springframework.cloud.function.serverless.web.ServerlessAutoConfiguration") + static class ApplicationWithoutServerlessAutoConfiguration { + + @GetMapping("/hello") + String hello() { + return "hello"; + } + } + + @RestController + @SpringBootApplication + static class ApplicationWithCustomServletWebServerFactory { + + @GetMapping("/hello") + String hello() { + return "hello"; + } + + @org.springframework.context.annotation.Bean + ServletWebServerFactory customServletWebServerFactory() { + return (initializers) -> new WebServer() { + @Override + public void start() throws WebServerException { + } + + @Override + public void stop() throws WebServerException { + } + + @Override + public int getPort() { + return 0; + } + }; + } + } + +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/ServerlessWebServerFactoryTests.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/ServerlessWebServerFactoryTests.java index f9aee8d71..7883e9bb6 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/ServerlessWebServerFactoryTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/ServerlessWebServerFactoryTests.java @@ -20,6 +20,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Oleg Zhurakousky */ @@ -29,6 +31,7 @@ public class ServerlessWebServerFactoryTests { public void testServerFactoryExists() { ServerlessMVC mvc = ServerlessMVC.INSTANCE(TestApplication.class); mvc.getApplicationContext(); + assertThat(mvc.getServletContext()).isNotNull(); } @SpringBootApplication diff --git a/spring-cloud-function-samples/function-sample-azure-web/pom.xml b/spring-cloud-function-samples/function-sample-azure-web/pom.xml index 3274b7783..7bf272d26 100644 --- a/spring-cloud-function-samples/function-sample-azure-web/pom.xml +++ b/spring-cloud-function-samples/function-sample-azure-web/pom.xml @@ -20,7 +20,7 @@ 17 1.0.31.RELEASE - 4.1.0-SNAPSHOT + 5.0.2-SNAPSHOT com.example.azure.web.AzureWebDemoApplication diff --git a/spring-cloud-function-samples/function-sample-azure-web/src/main/java/com/example/azure/web/CountryController.java b/spring-cloud-function-samples/function-sample-azure-web/src/main/java/com/example/azure/web/CountryController.java index 899773d5c..cd20dd1f7 100644 --- a/spring-cloud-function-samples/function-sample-azure-web/src/main/java/com/example/azure/web/CountryController.java +++ b/spring-cloud-function-samples/function-sample-azure-web/src/main/java/com/example/azure/web/CountryController.java @@ -59,7 +59,7 @@ public Country addCountry(@RequestBody Country country) { } @GetMapping("/countries/{id}") - public Country countryById(@PathVariable Integer id) { + public Country countryById(@PathVariable Long id) { return this.countryRepository.findById(id).get(); } } diff --git a/spring-cloud-function-samples/function-sample-azure-web/src/main/java/com/example/azure/web/CountryRepository.java b/spring-cloud-function-samples/function-sample-azure-web/src/main/java/com/example/azure/web/CountryRepository.java index cd81e6df6..163002c87 100644 --- a/spring-cloud-function-samples/function-sample-azure-web/src/main/java/com/example/azure/web/CountryRepository.java +++ b/spring-cloud-function-samples/function-sample-azure-web/src/main/java/com/example/azure/web/CountryRepository.java @@ -22,5 +22,5 @@ * * @author Christian Tzolov */ -public interface CountryRepository extends JpaRepository { -} \ No newline at end of file +public interface CountryRepository extends JpaRepository { +} diff --git a/spring-cloud-function-samples/function-sample-azure-web/src/main/resources/host.json b/spring-cloud-function-samples/function-sample-azure-web/src/main/resources/host.json index 10d0c0748..7621c6ca5 100644 --- a/spring-cloud-function-samples/function-sample-azure-web/src/main/resources/host.json +++ b/spring-cloud-function-samples/function-sample-azure-web/src/main/resources/host.json @@ -2,6 +2,6 @@ "version": "2.0", "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[3.*, 4.0.0)" + "version": "[4.*, 5.0.0)" } -} \ No newline at end of file +}