diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java index b35bba92c..f3976ab29 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java @@ -121,11 +121,13 @@ public ServletContext getServletContext() { * Sets the ServletContext in the handler and initialized a new FilterChainManager * @param context An initialized ServletContext */ - protected void setServletContext(final ServletContext context) { + public void setServletContext(final ServletContext context) { servletContext = context; // We assume custom implementations of the RequestWriter for HttpServletRequest will reuse // the existing AwsServletContext object since it has no dependencies other than the Lambda context - filterChainManager = new AwsFilterChainManager((AwsServletContext)servletContext); + if (context instanceof AwsServletContext) { + filterChainManager = new AwsFilterChainManager((AwsServletContext)servletContext); + } } protected FilterChain getFilterChain(HttpServletRequest req, Servlet servlet) { diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/AwsProxyRequest.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/AwsProxyRequest.java index 075db0ac3..4ee168491 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/AwsProxyRequest.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/AwsProxyRequest.java @@ -32,7 +32,7 @@ public class AwsProxyRequest { private String resource; private AwsProxyRequestContext requestContext; private MultiValuedTreeMap multiValueQueryStringParameters; - private Map queryStringParameters; + private Map queryStringParameters; private Headers multiValueHeaders; private SingleValueHeaders headers; private Map pathParameters; @@ -41,12 +41,13 @@ public class AwsProxyRequest { private String path; private boolean isBase64Encoded; - public AwsProxyRequest() { - multiValueHeaders = new Headers(); - multiValueQueryStringParameters = new MultiValuedTreeMap<>(); - pathParameters = new HashMap<>(); - stageVariables = new HashMap<>(); - } + public AwsProxyRequest() { + this.headers = new SingleValueHeaders(); + multiValueHeaders = new Headers(); + multiValueQueryStringParameters = new MultiValuedTreeMap<>(); + pathParameters = new HashMap<>(); + stageVariables = new HashMap<>(); + } //------------------------------------------------------------- @@ -131,17 +132,21 @@ public Headers getMultiValueHeaders() { return multiValueHeaders; } - public void setMultiValueHeaders(Headers multiValueHeaders) { - this.multiValueHeaders = multiValueHeaders; - } + public void setMultiValueHeaders(Headers multiValueHeaders) { + if (multiValueHeaders != null) { + this.multiValueHeaders = multiValueHeaders; + } + } public SingleValueHeaders getHeaders() { return headers; } - public void setHeaders(SingleValueHeaders headers) { - this.headers = headers; - } + public void setHeaders(SingleValueHeaders headers) { + if (headers != null) { + this.headers = headers; + } + } public Map getPathParameters() { diff --git a/aws-serverless-java-container-spring/pom.xml b/aws-serverless-java-container-spring/pom.xml index c734576a4..bae45b1d3 100644 --- a/aws-serverless-java-container-spring/pom.xml +++ b/aws-serverless-java-container-spring/pom.xml @@ -21,6 +21,11 @@ + + org.springframework.cloud + spring-cloud-function-serverless-web + 4.0.3 + com.amazonaws.serverless @@ -57,13 +62,6 @@ test - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - test - - jakarta.activation jakarta.activation-api diff --git a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringLambdaContainerHandler.java b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringLambdaContainerHandler.java index 5e56dba62..41dff16a3 100644 --- a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringLambdaContainerHandler.java +++ b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringLambdaContainerHandler.java @@ -20,6 +20,7 @@ import com.amazonaws.serverless.proxy.internal.servlet.*; import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.services.lambda.runtime.Context; + import org.springframework.web.context.ConfigurableWebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; @@ -49,13 +50,19 @@ public class SpringLambdaContainerHandler extends Aws * @return An initialized instance of the `SpringLambdaContainerHandler` * @throws ContainerInitializationException When the Spring framework fails to start. */ - public static SpringLambdaContainerHandler getAwsProxyHandler(Class... config) throws ContainerInitializationException { - return new SpringProxyHandlerBuilder() - .defaultProxy() - .initializationWrapper(new InitializationWrapper()) - .configurationClasses(config) - .buildAndInitialize(); - } + public static SpringLambdaContainerHandler getAwsProxyHandler(Class... config) + throws ContainerInitializationException { + // Temporary flag. Should be removed once spring-cloud-function delegation model becomes the only path. + boolean delegateToSpringCloudFunction = Boolean.parseBoolean(System.getenv().getOrDefault("spring.cloud.function.enable", + (String) System.getProperties().getOrDefault("spring.cloud.function.enable", "true"))); + if (delegateToSpringCloudFunction) { + return getSpringNativeHandler(config); + } else { + return new SpringProxyHandlerBuilder().defaultProxy() + .initializationWrapper(new InitializationWrapper()).configurationClasses(config) + .buildAndInitialize(); + } + } /** * Creates a default SpringLambdaContainerHandler initialized with the `AwsProxyRequest` and `AwsProxyResponse` objects and sets the given profiles as active @@ -188,4 +195,11 @@ protected void registerServlets() { reg.addMapping("/"); reg.setLoadOnStartup(1); } + + private static SpringLambdaContainerHandler getSpringNativeHandler(Class... config) throws ContainerInitializationException { + SpringLambdaContainerHandler handler = new SpringProxyHandlerBuilder() + .defaultProxy().configurationClasses(config).buildSpringProxy(); + + return handler; + } } diff --git a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringProxyHandlerBuilder.java b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringProxyHandlerBuilder.java index 5614b94df..4db08159a 100644 --- a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringProxyHandlerBuilder.java +++ b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringProxyHandlerBuilder.java @@ -13,12 +13,19 @@ package com.amazonaws.serverless.proxy.spring; import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; import com.amazonaws.serverless.proxy.internal.servlet.ServletLambdaContainerHandlerBuilder; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.services.lambda.runtime.Context; + +import org.springframework.cloud.function.serverless.web.ProxyMvc; import org.springframework.web.context.ConfigurableWebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.function.BiFunction; + public class SpringProxyHandlerBuilder extends ServletLambdaContainerHandlerBuilder< RequestType, @@ -73,6 +80,33 @@ public SpringLambdaContainerHandler build() throw return handler; } + /** + * Builds an instance of SpringLambdaContainerHandler with "delegate" to Spring provided ProxyMvc. The delegate + * is provided via BiFunction which takes HttpServletRequest and HttpSerbletResponse as input parameters. + * The AWS context is set as attribute of HttpServletRequest under `AWS_CONTEXT` key. + * + * @return instance of SpringLambdaContainerHandler + */ + SpringLambdaContainerHandler buildSpringProxy() { + ProxyMvc mvc = ProxyMvc.INSTANCE(this.configurationClasses); + BiFunction handlerDelegate = new BiFunction() { + @Override + public Void apply(HttpServletRequest request, HttpServletResponse response) { + try { + mvc.service(request, response); + response.flushBuffer(); + } + catch (Exception e) { + throw new IllegalStateException(e); + } + return null; + } + }; + SpringLambdaContainerHandler handler = createHandler(mvc.getApplicationContext(), handlerDelegate); + handler.setServletContext(mvc.getServletContext()); + return handler; + } + protected SpringLambdaContainerHandler createHandler(ConfigurableWebApplicationContext ctx) { return new SpringLambdaContainerHandler<>( requestTypeClass, responseTypeClass, requestReader, responseWriter, @@ -80,6 +114,20 @@ protected SpringLambdaContainerHandler createHand ); } + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected SpringLambdaContainerHandler createHandler(ConfigurableWebApplicationContext ctx, + BiFunction handler) { + return new SpringLambdaContainerHandler(requestTypeClass, responseTypeClass, requestReader, responseWriter, + securityContextWriter, exceptionHandler, ctx, initializationWrapper) { + @Override + protected void handleRequest(HttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, + Context lambdaContext) throws Exception { + containerRequest.setAttribute("AWS_CONTEXT", lambdaContext); + handler.apply(containerRequest, containerResponse); + } + }; + } + @Override public SpringLambdaContainerHandler buildAndInitialize() throws ContainerInitializationException { SpringLambdaContainerHandler handler = build(); diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java index 1101efc8c..19378a650 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java @@ -3,7 +3,6 @@ import com.amazonaws.serverless.exceptions.ContainerInitializationException; import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler; -import com.amazonaws.serverless.proxy.internal.servlet.AwsServletRegistration; import com.amazonaws.serverless.proxy.model.*; import com.amazonaws.serverless.proxy.internal.servlet.AwsServletContext; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; @@ -21,13 +20,19 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.web.servlet.DispatcherServlet; import jakarta.servlet.DispatcherType; import jakarta.servlet.FilterRegistration; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; + +import org.springframework.util.ReflectionUtils; +import org.springframework.web.servlet.DispatcherServlet; + import java.io.IOException; +import java.lang.reflect.Field; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; @@ -57,8 +62,8 @@ public class SpringAwsProxyTest { registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/echo/*"); // servlet name mappings are disabled and will throw an exception - //handler.getApplicationInitializer().getDispatcherServlet().setThrowExceptionIfNoHandlerFound(true); - ((DispatcherServlet)((AwsServletRegistration)c.getServletRegistration("dispatcherServlet")).getServlet()).setThrowExceptionIfNoHandlerFound(true); + DispatcherServlet dServlet = extractDispatcherServletFromContext(c); + dServlet.setThrowExceptionIfNoHandlerFound(true); }); private String type; @@ -503,5 +508,17 @@ private void validateSingleValueModel(AwsProxyResponse output, String value) { fail("Exception while parsing response body: " + e.getMessage()); } } + + private static DispatcherServlet extractDispatcherServletFromContext(ServletContext servletContext) { + ServletRegistration servletRegistration = servletContext.getServletRegistration("dispatcherServlet"); + Field field = ReflectionUtils.findField(servletRegistration.getClass(), "servlet"); + field.setAccessible(true); + try { + return (DispatcherServlet) field.get(servletRegistration); + } + catch (IllegalArgumentException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } } diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java index 7742db7f6..3d251d2f8 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java @@ -55,6 +55,7 @@ void profile_defaultProfile() throws Exception { @Test void profile_overrideProfile() throws Exception { + System.setProperty("spring.cloud.function.enable", "false"); AwsProxyRequest request = new AwsProxyRequestBuilder("/profile/spring-properties", "GET") .build(); SpringLambdaContainerHandler handler = SpringLambdaContainerHandler.getAwsProxyHandler(EchoSpringAppConfig.class); @@ -67,5 +68,6 @@ void profile_overrideProfile() throws Exception { assertEquals("override-profile", response.getValues().get("profileTest")); assertEquals("not-overridden", response.getValues().get("noOverride")); assertEquals("override-profile-from-bean", response.getValues().get("beanInjectedValue")); + System.setProperty("spring.cloud.function.enable", "true"); } } \ No newline at end of file diff --git a/samples/spring/pet-store/pom.xml b/samples/spring/pet-store/pom.xml index 9958e007c..91843c33f 100644 --- a/samples/spring/pet-store/pom.xml +++ b/samples/spring/pet-store/pom.xml @@ -32,6 +32,12 @@ + + io.github.crac + org-crac + 0.1.3 + + com.amazonaws.serverless aws-serverless-java-container-spring