From f2ea7c3656815135455cc5ab53d8efedd40f2f72 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Tue, 9 May 2023 09:49:06 +0200 Subject: [PATCH] Add delegation proxy to spring web proxy This PR allows for separation of responsibilty between AWS and Spring where AWS side remains responsible to create HttpServletRequest from JSON representation of the API Gateway's input stream. Once created it can now delegate to Spring provided module for further interaction with the DispatcherServlet and the rest of thet Spring stack Introduce a boolean flag to disable delegation to Spring --- .../AwsLambdaServletContainerHandler.java | 6 ++- .../proxy/model/AwsProxyRequest.java | 31 +++++++----- aws-serverless-java-container-spring/pom.xml | 12 ++--- .../spring/SpringLambdaContainerHandler.java | 29 ++++++++--- .../spring/SpringProxyHandlerBuilder.java | 49 +++++++++++++++++++ .../proxy/spring/SpringAwsProxyTest.java | 25 ++++++++-- .../spring/profile/SpringProfileTest.java | 2 + 7 files changed, 121 insertions(+), 33 deletions(-) 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..a0863637a 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,20 @@ 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 +196,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..dcf751c7e 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,34 @@ 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 +115,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