Skip to content

Commit 7ca2f07

Browse files
author
Dennis Kieselhorst
authored
feat: native Spring Web workloads (#335)
2 parents e1acefc + 4533c4b commit 7ca2f07

File tree

37 files changed

+2110
-103
lines changed

37 files changed

+2110
-103
lines changed

aws-serverless-java-container-springboot3/pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<dependency>
2626
<groupId>org.springframework.cloud</groupId>
2727
<artifactId>spring-cloud-function-serverless-web</artifactId>
28-
<version>4.0.4</version>
28+
<version>4.0.6</version>
2929
</dependency>
3030
<dependency>
3131
<groupId>com.amazonaws.serverless</groupId>
@@ -201,6 +201,11 @@
201201
<configuration>
202202
<destFile>${basedir}/target/coverage-reports/jacoco-unit.exec</destFile>
203203
<dataFile>${basedir}/target/coverage-reports/jacoco-unit.exec</dataFile>
204+
<excludes>
205+
<!-- Native AOT implementation is currently not covered (due to complexity of the test setup) -->
206+
<exclude>com/amazonaws/serverless/proxy/spring/AwsSpringWebCustomRuntimeEventLoop*</exclude>
207+
<exclude>com/amazonaws/serverless/proxy/spring/AwsSpringAotTypesProcessor*</exclude>
208+
</excludes>
204209
</configuration>
205210
<executions>
206211
<execution>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2024-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.amazonaws.serverless.proxy.spring;
18+
19+
import org.springframework.aot.generate.GenerationContext;
20+
import org.springframework.aot.hint.MemberCategory;
21+
import org.springframework.aot.hint.RuntimeHints;
22+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
23+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
24+
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
25+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
26+
27+
import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse;
28+
import com.amazonaws.serverless.proxy.model.ApiGatewayRequestIdentity;
29+
import com.amazonaws.serverless.proxy.model.AwsProxyRequest;
30+
import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext;
31+
import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
32+
import com.amazonaws.serverless.proxy.model.Headers;
33+
import com.amazonaws.serverless.proxy.model.MultiValuedTreeMap;
34+
import com.amazonaws.serverless.proxy.model.SingleValueHeaders;
35+
import com.fasterxml.jackson.core.JsonToken;
36+
37+
/**
38+
* AOT Initialization processor required to register reflective hints for GraalVM.
39+
* This is necessary to ensure proper JSON serialization/deserialization.
40+
* It is registered with META-INF/spring/aot.factories
41+
*
42+
* @author Oleg Zhurakousky
43+
*/
44+
public class AwsSpringAotTypesProcessor implements BeanFactoryInitializationAotProcessor {
45+
46+
@Override
47+
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
48+
return new ReflectiveProcessorBeanFactoryInitializationAotContribution();
49+
}
50+
51+
private static final class ReflectiveProcessorBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotContribution {
52+
@Override
53+
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
54+
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
55+
// known static types
56+
57+
runtimeHints.reflection().registerType(AwsProxyRequest.class,
58+
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES);
59+
runtimeHints.reflection().registerType(AwsProxyResponse.class,
60+
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES);
61+
runtimeHints.reflection().registerType(SingleValueHeaders.class,
62+
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES);
63+
runtimeHints.reflection().registerType(JsonToken.class,
64+
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES);
65+
runtimeHints.reflection().registerType(MultiValuedTreeMap.class,
66+
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES);
67+
runtimeHints.reflection().registerType(Headers.class,
68+
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES);
69+
runtimeHints.reflection().registerType(AwsProxyRequestContext.class,
70+
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES);
71+
runtimeHints.reflection().registerType(ApiGatewayRequestIdentity.class,
72+
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES);
73+
runtimeHints.reflection().registerType(AwsHttpServletResponse.class,
74+
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
75+
MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS);
76+
}
77+
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package com.amazonaws.serverless.proxy.spring;
2+
3+
import java.io.InputStream;
4+
import java.nio.charset.StandardCharsets;
5+
import java.util.Map;
6+
import java.util.concurrent.CountDownLatch;
7+
import java.util.concurrent.TimeUnit;
8+
9+
import org.apache.commons.logging.Log;
10+
import org.apache.commons.logging.LogFactory;
11+
import org.springframework.cloud.function.serverless.web.ServerlessHttpServletRequest;
12+
import org.springframework.cloud.function.serverless.web.ServerlessMVC;
13+
import org.springframework.util.FileCopyUtils;
14+
import org.springframework.util.MultiValueMapAdapter;
15+
import org.springframework.util.StringUtils;
16+
17+
import com.amazonaws.serverless.proxy.AsyncInitializationWrapper;
18+
import com.amazonaws.serverless.proxy.AwsHttpApiV2SecurityContextWriter;
19+
import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter;
20+
import com.amazonaws.serverless.proxy.RequestReader;
21+
import com.amazonaws.serverless.proxy.SecurityContextWriter;
22+
import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse;
23+
import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter;
24+
import com.amazonaws.serverless.proxy.model.AwsProxyRequest;
25+
import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
26+
import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest;
27+
import com.amazonaws.services.lambda.runtime.Context;
28+
import com.fasterxml.jackson.databind.ObjectMapper;
29+
30+
import jakarta.servlet.ServletContext;
31+
import jakarta.servlet.http.HttpServletRequest;
32+
33+
class AwsSpringHttpProcessingUtils {
34+
35+
private static Log logger = LogFactory.getLog(AwsSpringHttpProcessingUtils.class);
36+
private static final int LAMBDA_MAX_REQUEST_DURATION_MINUTES = 15;
37+
38+
private AwsSpringHttpProcessingUtils() {
39+
40+
}
41+
42+
public static AwsProxyResponse processRequest(HttpServletRequest request, ServerlessMVC mvc,
43+
AwsProxyHttpServletResponseWriter responseWriter) {
44+
CountDownLatch latch = new CountDownLatch(1);
45+
AwsHttpServletResponse response = new AwsHttpServletResponse(request, latch);
46+
try {
47+
mvc.service(request, response);
48+
boolean requestTimedOut = !latch.await(LAMBDA_MAX_REQUEST_DURATION_MINUTES, TimeUnit.MINUTES); // timeout is potentially lower as user configures it
49+
if (requestTimedOut) {
50+
logger.warn("request timed out after " + LAMBDA_MAX_REQUEST_DURATION_MINUTES + " minutes");
51+
}
52+
AwsProxyResponse awsResponse = responseWriter.writeResponse(response, null);
53+
return awsResponse;
54+
}
55+
catch (Exception e) {
56+
e.printStackTrace();
57+
throw new IllegalStateException(e);
58+
}
59+
}
60+
61+
public static String extractVersion() {
62+
try {
63+
String path = AwsSpringHttpProcessingUtils.class.getProtectionDomain().getCodeSource().getLocation().toString();
64+
int endIndex = path.lastIndexOf('.');
65+
if (endIndex < 0) {
66+
return "UNKNOWN-VERSION";
67+
}
68+
int startIndex = path.lastIndexOf("/") + 1;
69+
return path.substring(startIndex, endIndex).replace("spring-cloud-function-serverless-web-", "");
70+
}
71+
catch (Exception e) {
72+
if (logger.isDebugEnabled()) {
73+
logger.debug("Failed to detect version", e);
74+
}
75+
return "UNKNOWN-VERSION";
76+
}
77+
78+
}
79+
80+
public static HttpServletRequest generateHttpServletRequest(InputStream jsonRequest, Context lambdaContext,
81+
ServletContext servletContext, ObjectMapper mapper) {
82+
try {
83+
String text = new String(FileCopyUtils.copyToByteArray(jsonRequest), StandardCharsets.UTF_8);
84+
if (logger.isDebugEnabled()) {
85+
logger.debug("Creating HttpServletRequest from: " + text);
86+
}
87+
return generateHttpServletRequest(text, lambdaContext, servletContext, mapper);
88+
} catch (Exception e) {
89+
throw new IllegalStateException(e);
90+
}
91+
}
92+
93+
@SuppressWarnings({ "rawtypes", "unchecked" })
94+
public static HttpServletRequest generateHttpServletRequest(String jsonRequest, Context lambdaContext,
95+
ServletContext servletContext, ObjectMapper mapper) {
96+
Map<String, Object> _request = readValue(jsonRequest, Map.class, mapper);
97+
SecurityContextWriter securityWriter = "2.0".equals(_request.get("version"))
98+
? new AwsHttpApiV2SecurityContextWriter()
99+
: new AwsProxySecurityContextWriter();
100+
HttpServletRequest httpServletRequest = "2.0".equals(_request.get("version"))
101+
? AwsSpringHttpProcessingUtils.generateRequest2(jsonRequest, lambdaContext, securityWriter, mapper, servletContext)
102+
: AwsSpringHttpProcessingUtils.generateRequest1(jsonRequest, lambdaContext, securityWriter, mapper, servletContext);
103+
return httpServletRequest;
104+
}
105+
106+
@SuppressWarnings({ "unchecked", "rawtypes" })
107+
private static HttpServletRequest generateRequest1(String request, Context lambdaContext,
108+
SecurityContextWriter securityWriter, ObjectMapper mapper, ServletContext servletContext) {
109+
AwsProxyRequest v1Request = readValue(request, AwsProxyRequest.class, mapper);
110+
111+
ServerlessHttpServletRequest httpRequest = new ServerlessHttpServletRequest(servletContext, v1Request.getHttpMethod(), v1Request.getPath());
112+
if (v1Request.getMultiValueHeaders() != null) {
113+
MultiValueMapAdapter headers = new MultiValueMapAdapter(v1Request.getMultiValueHeaders());
114+
httpRequest.setHeaders(headers);
115+
}
116+
if (StringUtils.hasText(v1Request.getBody())) {
117+
httpRequest.setContentType("application/json");
118+
httpRequest.setContent(v1Request.getBody().getBytes(StandardCharsets.UTF_8));
119+
}
120+
if (v1Request.getRequestContext() != null) {
121+
httpRequest.setAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY, v1Request.getRequestContext());
122+
httpRequest.setAttribute(RequestReader.ALB_CONTEXT_PROPERTY, v1Request.getRequestContext().getElb());
123+
}
124+
httpRequest.setAttribute(RequestReader.API_GATEWAY_STAGE_VARS_PROPERTY, v1Request.getStageVariables());
125+
httpRequest.setAttribute(RequestReader.API_GATEWAY_EVENT_PROPERTY, v1Request);
126+
httpRequest.setAttribute(RequestReader.LAMBDA_CONTEXT_PROPERTY, lambdaContext);
127+
httpRequest.setAttribute(RequestReader.JAX_SECURITY_CONTEXT_PROPERTY,
128+
securityWriter.writeSecurityContext(v1Request, lambdaContext));
129+
return httpRequest;
130+
}
131+
132+
@SuppressWarnings({ "rawtypes", "unchecked" })
133+
private static HttpServletRequest generateRequest2(String request, Context lambdaContext,
134+
SecurityContextWriter securityWriter, ObjectMapper mapper, ServletContext servletContext) {
135+
HttpApiV2ProxyRequest v2Request = readValue(request, HttpApiV2ProxyRequest.class, mapper);
136+
ServerlessHttpServletRequest httpRequest = new ServerlessHttpServletRequest(servletContext,
137+
v2Request.getRequestContext().getHttp().getMethod(), v2Request.getRequestContext().getHttp().getPath());
138+
139+
v2Request.getHeaders().forEach(httpRequest::setHeader);
140+
141+
if (StringUtils.hasText(v2Request.getBody())) {
142+
httpRequest.setContentType("application/json");
143+
httpRequest.setContent(v2Request.getBody().getBytes(StandardCharsets.UTF_8));
144+
}
145+
httpRequest.setAttribute(RequestReader.HTTP_API_CONTEXT_PROPERTY, v2Request.getRequestContext());
146+
httpRequest.setAttribute(RequestReader.HTTP_API_STAGE_VARS_PROPERTY, v2Request.getStageVariables());
147+
httpRequest.setAttribute(RequestReader.HTTP_API_EVENT_PROPERTY, v2Request);
148+
httpRequest.setAttribute(RequestReader.LAMBDA_CONTEXT_PROPERTY, lambdaContext);
149+
httpRequest.setAttribute(RequestReader.JAX_SECURITY_CONTEXT_PROPERTY,
150+
securityWriter.writeSecurityContext(v2Request, lambdaContext));
151+
return httpRequest;
152+
}
153+
154+
private static <T> T readValue(String json, Class<T> clazz, ObjectMapper mapper) {
155+
try {
156+
return mapper.readValue(json, clazz);
157+
}
158+
catch (Exception e) {
159+
throw new IllegalStateException(e);
160+
}
161+
}
162+
163+
}

0 commit comments

Comments
 (0)