Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<URL> 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 {
Expand Down Expand Up @@ -135,7 +135,7 @@ private static Class<?> getStartClass(List<URL> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}

Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -559,9 +559,7 @@ public void clearAttributes() {
* <strong>not</strong> 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() {
Expand All @@ -580,20 +578,17 @@ public Locale getLocale() {
* <strong>not</strong> 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<Locale> getLocales() {
return Collections.enumeration(this.locales);
}

/**
* 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
}
Expand All @@ -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);

Expand Down Expand Up @@ -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
Expand All @@ -217,7 +234,17 @@ private static class ProxyFilterChain implements FilterChain {
*/
ProxyFilterChain(DispatcherServlet servlet) {
List<Filter> filters = new ArrayList<>();
servlet.getServletContext().getFilterRegistrations().values().forEach(fr -> filters.add(((ServerlessFilterRegistration) fr).getFilter()));
for (Map.Entry<String, ? extends FilterRegistration> 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[] {}));
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> attributes = new HashMap<>();

Expand Down Expand Up @@ -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
Expand Down
Loading