From e34d7ba9da9db101160a616d807f1d4015a08af6 Mon Sep 17 00:00:00 2001 From: Stefano Buliani Date: Mon, 13 Apr 2020 08:25:03 -0700 Subject: [PATCH 1/5] test: Minor changes and fixes for springboot tests --- .../amazonaws/serverless/proxy/spring/SpringBootAppTest.java | 5 +---- .../amazonaws/serverless/proxy/spring/WebFluxAppTest.java | 1 + .../proxy/spring/webfluxapp/MessageController.java | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringBootAppTest.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringBootAppTest.java index 2a345b6ff..82d09ff15 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringBootAppTest.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringBootAppTest.java @@ -4,7 +4,6 @@ import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; -import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.model.Headers; import com.amazonaws.serverless.proxy.model.MultiValuedTreeMap; @@ -25,8 +24,7 @@ import java.util.Collection; import java.util.Objects; -import static com.amazonaws.serverless.proxy.spring.springbootapp.TestController.CUSTOM_HEADER_NAME; -import static com.amazonaws.serverless.proxy.spring.springbootapp.TestController.CUSTOM_QS_NAME; +import static com.amazonaws.serverless.proxy.spring.springbootapp.TestController.*; import static org.junit.Assert.*; import static org.junit.Assume.assumeFalse; @@ -187,7 +185,6 @@ public void utf8_returnUtf8String_expectCorrectHeaderMediaAndCharsetNoDefault() assertTrue(output.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).contains("charset=UTF-8")); } - private void validateSingleValueModel(AwsProxyResponse output, String value) { try { SingleValueModel response = mapper.readValue(output.getBody(), SingleValueModel.class); diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java index 811a1ab55..1d8a15e08 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java @@ -14,6 +14,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import javax.ws.rs.core.HttpHeaders; import java.util.Arrays; import java.util.Collection; diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java index a04604618..d0ee6967e 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java @@ -4,7 +4,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; import reactor.core.publisher.Flux; @RestController From 13530655e0cd15e0d957b6f2fa10826156385aed Mon Sep 17 00:00:00 2001 From: Stefano Buliani Date: Mon, 13 Apr 2020 08:27:17 -0700 Subject: [PATCH 2/5] feat: First experimental example of SpringBoot 2 GraalVM native compilation --- Dockerfile | 39 ++++ .../serverless/runtime/InvocationContext.java | 101 ++++++++++ .../serverless/runtime/InvocationRequest.java | 26 +++ .../amazonaws/serverless/runtime/Runtime.java | 101 ++++++++++ .../serverless/runtime/RuntimeClient.java | 188 ++++++++++++++++++ .../runtime/RuntimeClientException.java | 22 ++ .../springboot2/native-pet-store/README.md | 38 ++++ samples/springboot2/native-pet-store/pom.xml | 110 ++++++++++ .../native-pet-store/scripts/bootstrap | 4 + .../native-pet-store/scripts/graalvm-build.sh | 17 ++ .../native-pet-store/src/assembly/bin.xml | 27 +++ .../sample/springboot2/Application.java | 55 +++++ .../controller/PetsController.java | 79 ++++++++ .../filter/CognitoIdentityFilter.java | 69 +++++++ .../sample/springboot2/model/Error.java | 29 +++ .../sample/springboot2/model/Pet.java | 59 ++++++ .../sample/springboot2/model/PetData.java | 117 +++++++++++ .../src/main/resources/logback.xml | 6 + .../springboot2/native-pet-store/template.yml | 32 +++ 19 files changed, 1119 insertions(+) create mode 100644 Dockerfile create mode 100644 aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/InvocationContext.java create mode 100644 aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/InvocationRequest.java create mode 100644 aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/Runtime.java create mode 100644 aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClient.java create mode 100644 aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClientException.java create mode 100644 samples/springboot2/native-pet-store/README.md create mode 100644 samples/springboot2/native-pet-store/pom.xml create mode 100644 samples/springboot2/native-pet-store/scripts/bootstrap create mode 100644 samples/springboot2/native-pet-store/scripts/graalvm-build.sh create mode 100644 samples/springboot2/native-pet-store/src/assembly/bin.xml create mode 100644 samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/Application.java create mode 100644 samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/controller/PetsController.java create mode 100644 samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/filter/CognitoIdentityFilter.java create mode 100644 samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Error.java create mode 100644 samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Pet.java create mode 100644 samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/PetData.java create mode 100644 samples/springboot2/native-pet-store/src/main/resources/logback.xml create mode 100644 samples/springboot2/native-pet-store/template.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..c150aabe4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM amazonlinux:2017.03.1.20170812 as graalvm +# install graal to amazon linux. +ENV LANG=en_US.UTF-8 + +RUN yum install -y gcc gcc-c++ libc6-dev zlib1g-dev curl bash zlib zlib-devel zip \ + && rm -rf /var/cache/yum + +# Install JDK8 with backported JVMCI +ENV JDK_FILE openjdk-8u242-jvmci-20.1-b01-linux-amd64 +ENV JDK_DIR openjdk1.8.0_242-jvmci-20.1-b01 +RUN curl -4 -L https://github.com/graalvm/openjdk8-jvmci-builder/releases/download/jvmci-20.1-b01/${JDK_FILE}.tar.gz -o /tmp/jdk.tar.gz +RUN tar -zxvf /tmp/jdk.tar.gz -C /tmp \ + && mv /tmp/${JDK_DIR} /usr/lib/jdk +ENV JAVA_HOME /usr/lib/jdk + +# Download and install GraalVM 20.0.0 + native image +ENV GRAAL_VERSION 20.0.0 +ENV GRAAL_FILENAME graalvm-ce-java8-linux-amd64-${GRAAL_VERSION}.tar.gz +RUN curl -4 -L https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-${GRAAL_VERSION}/${GRAAL_FILENAME} -o /tmp/${GRAAL_FILENAME} +RUN tar -zxvf /tmp/${GRAAL_FILENAME} -C /tmp \ + && mv /tmp/graalvm-ce-java8-${GRAAL_VERSION} /usr/lib/graalvm +RUN /usr/lib/graalvm/bin/gu install native-image + +# Download and install maven +ENV MAVEN_VERSION 3.6.3 +RUN curl -4 -L http://apache.forsale.plus/maven/maven-3/3.6.3/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz -o /tmp/maven.tar.gz +RUN tar -zxvf /tmp/maven.tar.gz -C /tmp \ + && mv /tmp/apache-maven-${MAVEN_VERSION} /usr/lib/maven + +RUN rm -rf /tmp/* + +ENV PATH /usr/lib/graalvm/bin:/usr/lib/maven/bin:${PATH} + +COPY . /usr/lib/sjc +RUN cd /usr/lib/sjc && mvn install -DskipTests -Djacoco.minCoverage=0.1 + +VOLUME ["/func"] +WORKDIR /func +ENTRYPOINT ["/func/scripts/graalvm-build.sh"] \ No newline at end of file diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/InvocationContext.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/InvocationContext.java new file mode 100644 index 000000000..83718ef26 --- /dev/null +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/InvocationContext.java @@ -0,0 +1,101 @@ +package com.amazonaws.serverless.runtime; + +import com.amazonaws.services.lambda.runtime.ClientContext; +import com.amazonaws.services.lambda.runtime.CognitoIdentity; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; + +import java.time.Instant; + +public class InvocationContext implements Context { + private static final String FUNCTION_NAME_ENV_VAR = "AWS_LAMBDA_FUNCTION_NAME"; + private static final String FUNCTION_LOG_STREAM_ENV_VAR = "AWS_LAMBDA_LOG_STREAM_NAME"; + private static final String FUNCTION_LOG_GROUP_ENV_VAR = "AWS_LAMBDA_LOG_GROUP_NAME"; + private static final String FUNCTION_VERSION_ENV_VAR = "AWS_LAMBDA_FUNCTION_VERSION"; + private static final String FUNCTION_MEMORY_ENV_VAR = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE"; + + private static String functionName; + private static String logGroupName; + private static String logStreamName; + private static String functionVersion; + private static int memoryLimitMb; + + public static void prepareContext() { + functionName = System.getenv(FUNCTION_NAME_ENV_VAR); + logGroupName = System.getenv(FUNCTION_LOG_GROUP_ENV_VAR); + logStreamName = System.getenv(FUNCTION_LOG_STREAM_ENV_VAR); + functionVersion = System.getenv(FUNCTION_VERSION_ENV_VAR); + memoryLimitMb = Integer.parseInt(System.getenv(FUNCTION_MEMORY_ENV_VAR)); + } + + private String awsRequestId; + private long deadlineMs; + private String functionArn; + private String traceId; + + public InvocationContext(String reqId, long deadline, String arn, String trace) { + awsRequestId = reqId; + deadlineMs = deadline; + functionArn = arn; + traceId = trace; + } + + @Override + public String getAwsRequestId() { + return awsRequestId; + } + + @Override + public String getLogGroupName() { + return logGroupName; + } + + @Override + public String getLogStreamName() { + return logStreamName; + } + + @Override + public String getFunctionName() { + return functionName; + } + + @Override + public String getFunctionVersion() { + return functionVersion; + } + + @Override + public String getInvokedFunctionArn() { + return functionArn; + } + + @Override + public CognitoIdentity getIdentity() { + return null; + } + + @Override + public ClientContext getClientContext() { + return null; + } + + @Override + public int getRemainingTimeInMillis() { + return Math.toIntExact(deadlineMs - Instant.now().toEpochMilli()); + } + + @Override + public int getMemoryLimitInMB() { + return memoryLimitMb; + } + + @Override + public LambdaLogger getLogger() { + return null; + } + + public String getTraceId() { + return traceId; + } +} diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/InvocationRequest.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/InvocationRequest.java new file mode 100644 index 000000000..56257001a --- /dev/null +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/InvocationRequest.java @@ -0,0 +1,26 @@ +package com.amazonaws.serverless.runtime; + +import com.amazonaws.services.lambda.runtime.Context; + +import java.io.InputStream; + +public class InvocationRequest { + private InputStream event; + private Context context; + + public InputStream getEvent() { + return event; + } + + public void setEvent(InputStream event) { + this.event = event; + } + + public Context getContext() { + return context; + } + + public void setContext(Context context) { + this.context = context; + } +} diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/Runtime.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/Runtime.java new file mode 100644 index 000000000..37acb9a1b --- /dev/null +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/Runtime.java @@ -0,0 +1,101 @@ +package com.amazonaws.serverless.runtime; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class Runtime implements RequestStreamHandler { + private static final int MAX_RETRIES = 3; + + private int maxRetries; + private SpringBootLambdaContainerHandler handler; + + public Runtime(Class initializer) throws ContainerInitializationException { + this(initializer, MAX_RETRIES); + } + + public Runtime(Class initializer, int retries) throws ContainerInitializationException { + maxRetries = retries; + handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(initializer); + } + + public void start() { + RuntimeClient client = new RuntimeClient(); + start(client); + } + + public void start(RuntimeClient client) throws RuntimeException { + while (true) { + InvocationRequest req = getNextRequest(client); + if (req == null) { + throw new RuntimeException("Could not fetch next event after " + maxRetries + " attempts"); + } + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + handleRequest(req.getEvent(), os, req.getContext()); + } catch (IOException e) { + e.printStackTrace(); + postErrorResponse(client, req.getContext().getAwsRequestId(), e); + } + + postResponse(client, req.getContext().getAwsRequestId(), os); + } + } + + private InvocationRequest getNextRequest(RuntimeClient client) { + InvocationRequest req = null; + for (int i = 0; i < maxRetries; i++) { + try { + req = client.getNextEvent(); + return req; + } catch (RuntimeClientException e) { + if (!e.isRetriable()) { + throw new RuntimeException("Failed to fetch next event", e); + } + } + } + return null; + } + + private void postResponse(RuntimeClient client, String reqId, OutputStream os) { + for (int i = 0; i < maxRetries; i++) { + try { + client.postInvocationResponse(reqId, os); + return; + } catch (RuntimeClientException ex) { + if (!ex.isRetriable()) { + throw new RuntimeException("Failed to post invocation response", ex); + } + } + } + throw new RuntimeException("Failed to post invocation response " + maxRetries + " times"); + } + + private void postErrorResponse(RuntimeClient client, String reqId, Throwable e) { + for (int i = 0; i < maxRetries; i++) { + try { + client.postInvocationError(reqId, e); + return; + } catch (RuntimeClientException ex) { + if (!ex.isRetriable()) { + throw new RuntimeException("Failed to post invocation error", ex); + } + } + } + throw new RuntimeException("Failed to post invocation error response " + maxRetries + " times"); + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + handler.proxyStream(inputStream, outputStream, context); + } +} diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClient.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClient.java new file mode 100644 index 000000000..54e0f14cd --- /dev/null +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClient.java @@ -0,0 +1,188 @@ +package com.amazonaws.serverless.runtime; + +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.amazonaws.serverless.proxy.internal.SecurityUtils; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.HttpHeaders; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +public class RuntimeClient { + public static final String API_VERSION = "2018-06-01"; + public static final String API_PROTOCOL = "http"; + public static final String API_HOST_VAR_NAME = "AWS_LAMBDA_RUNTIME_API"; + + private static final String REQUEST_ID_HEADER = "Lambda-Runtime-Aws-Request-Id"; + private static final String DEADLINE_HEADER = "Lambda-Runtime-Deadline-Ms"; + private static final String FUNCTION_ARN_HEADER = "Lambda-Runtime-Invoked-Function-Arn"; + private static final String TRACE_ID_HEADER = "Lambda-Runtime-Trace-Id"; + + private final Logger log = LoggerFactory.getLogger(RuntimeClient.class); + + private final String apiHost; + private final URL nextEventUrl; + + public RuntimeClient() { + try { + apiHost = System.getenv(API_HOST_VAR_NAME); + nextEventUrl = new URL(API_PROTOCOL + "://" + apiHost + "/" + API_VERSION + "/runtime/invocation/next"); + InvocationContext.prepareContext(); + } catch (SecurityException e) { + log.error("Security Exception while reading runtime API host environment variable", e); + e.printStackTrace(); + throw new RuntimeException("Failed to initialize runtime client", e); + } catch (MalformedURLException e) { + log.error("Could not construct URL for runtime API endpoint", e); + e.printStackTrace(); + throw new RuntimeException("Failed to initialize runtime client", e); + } catch (NumberFormatException e) { + log.error("Could not parse memory limit from environment variable", e); + e.printStackTrace(); + throw new RuntimeException("Failed to initialize runtime client", e); + } + } + + public InvocationRequest getNextEvent() throws RuntimeClientException { + try { + HttpURLConnection conn = (HttpURLConnection)nextEventUrl.openConnection(); + conn.setRequestMethod("GET"); + conn.setReadTimeout(0); + conn.setConnectTimeout(0); + conn.setRequestProperty(HttpHeaders.ACCEPT, "application/json"); + + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + String errMsg = "Could not retrieve next event. Status code: " + conn.getResponseCode(); + if (conn.getErrorStream() != null) { + errMsg += "\n" + String.join("\n", IOUtils.readLines(conn.getErrorStream())); + } + throw new RuntimeClientException( + errMsg, + null, + true); + } + + InvocationRequest req = new InvocationRequest(); + req.setEvent(conn.getInputStream()); + + String reqId = conn.getHeaderField(REQUEST_ID_HEADER); + long deadline = Long.parseLong(conn.getHeaderField(DEADLINE_HEADER)); + String arn = conn.getHeaderField(FUNCTION_ARN_HEADER); + String trace = conn.getHeaderField(TRACE_ID_HEADER); + if (reqId == null || arn == null || trace == null) { + throw new RuntimeClientException("Could not read event header fields", null, true); + } + req.setContext(new InvocationContext(reqId, deadline, arn, trace)); + + return req; + } catch (IOException e) { + log.error("Error while requesting next event", e); + throw new RuntimeClientException("Error while requesting next event", e, true); + } catch (NumberFormatException e) { + log.error("Could not parse deadline ms value", e); + throw new RuntimeClientException("Error while requesting next event", e, true); + } + } + + @SuppressFBWarnings("CRLF_INJECTION_LOGS") + public void postInvocationResponse(String reqId, OutputStream out) throws RuntimeClientException { + try { + URL responseUrl = new URL(API_PROTOCOL + "://" + apiHost + "/" + API_VERSION + "/runtime/invocation/" + reqId + "/response"); + HttpURLConnection conn = (HttpURLConnection)responseUrl.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + + if (out instanceof ByteArrayOutputStream) { + conn.getOutputStream().write(((ByteArrayOutputStream)out).toByteArray()); + } else { + throw new RuntimeClientException("Only byte array output streams are supported " + out.getClass().getName(), null, false); + } + conn.getOutputStream().close(); + if (conn.getResponseCode() != HttpURLConnection.HTTP_ACCEPTED) { + throw new RuntimeClientException("Could not send invocation response (" + conn.getResponseCode() + ")", null, true); + } + } catch (MalformedURLException e) { + log.error("Could not construct invocation response url for " + SecurityUtils.crlf(reqId), e); + throw new RuntimeClientException("Error while posting invocation response", e, false); + } catch (IOException e) { + log.error("Could not write to runtime API connection for " + SecurityUtils.crlf(reqId), e); + throw new RuntimeClientException("Error while posting invocation response", e, true); + } + } + + @SuppressFBWarnings("CRLF_INJECTION_LOGS") + public void postInvocationError(String reqId, Throwable error) throws RuntimeClientException { + try { + URL errorUrl = new URL(API_PROTOCOL + "://" + apiHost + "/" + API_VERSION + "/runtime/invocation/" + reqId + "/error"); + HttpURLConnection conn = (HttpURLConnection)errorUrl.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + + InvocationError err = new InvocationError(error.getMessage(), error.getClass().getSimpleName()); + conn.getOutputStream().write(LambdaContainerHandler.getObjectMapper().writeValueAsBytes(err)); + conn.getOutputStream().close(); + if (conn.getResponseCode() != HttpURLConnection.HTTP_ACCEPTED) { + throw new RuntimeClientException("Could not send invocation response (" + conn.getResponseCode() + ")", null, true); + } + } catch (MalformedURLException e) { + log.error("Could not construct invocation error url for " + SecurityUtils.crlf(reqId), e); + throw new RuntimeClientException("Error while posting invocation error", e, false); + } catch (IOException e) { + log.error("Could not write to runtime API connection for " + SecurityUtils.crlf(reqId), e); + throw new RuntimeClientException("Error while posting invocation error", e, true); + } + } + + public void reportInitError(Throwable error) throws RuntimeClientException { + try { + URL errorUrl = new URL(API_PROTOCOL + "://" + apiHost + "/" + API_VERSION + "/runtime/init/error"); + HttpURLConnection conn = (HttpURLConnection)errorUrl.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + + InvocationError err = new InvocationError(error.getMessage(), error.getClass().getSimpleName()); + conn.getOutputStream().write(LambdaContainerHandler.getObjectMapper().writeValueAsBytes(err)); + conn.getOutputStream().close(); + if (conn.getResponseCode() != HttpURLConnection.HTTP_ACCEPTED) { + throw new RuntimeClientException("Could not send invocation response (" + conn.getResponseCode() + ")", null, true); + } + } catch (MalformedURLException e) { + log.error("Could not construct init error url", e); + throw new RuntimeClientException("Error while posting init error", e, false); + } catch (IOException e) { + log.error("Could not write init error to runtime API", e); + throw new RuntimeClientException("Error while posting init error", e, true); + } + } + + static class InvocationError { + private String errorMessage; + private String errorType; + + public InvocationError(String errorMessage, String errorType) { + this.errorMessage = errorMessage; + this.errorType = errorType; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getErrorType() { + return errorType; + } + + public void setErrorType(String errorType) { + this.errorType = errorType; + } + } +} diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClientException.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClientException.java new file mode 100644 index 000000000..830979084 --- /dev/null +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClientException.java @@ -0,0 +1,22 @@ +package com.amazonaws.serverless.runtime; + +public class RuntimeClientException extends Exception { + private boolean retriable; + + public RuntimeClientException(String msg, Throwable e) { + this(msg, e, false); + } + + public RuntimeClientException(String msg, Throwable e, boolean retriable) { + super(msg, e); + this.retriable = retriable; + } + + public boolean isRetriable() { + return retriable; + } + + public void setRetriable(boolean retriable) { + this.retriable = retriable; + } +} diff --git a/samples/springboot2/native-pet-store/README.md b/samples/springboot2/native-pet-store/README.md new file mode 100644 index 000000000..b7e03a754 --- /dev/null +++ b/samples/springboot2/native-pet-store/README.md @@ -0,0 +1,38 @@ +# Serverless Spring Boot 2 example for GraalVM Native Image +This experimental Pet Store example uses [Spring Boot's](https://projects.spring.io/spring-boot/) support for native compilation with [GraalVM](https://www.graalvm.org/) and executes as a custom runtime in AWS Lambda. + +The application uses Docker to compile a native executable compatible with AWS Lambda's runtime environment and can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. + +## Pre-requisites +* [Docker](https://www.docker.com/) +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) + +## Deployment +To make it easy to build our samples with GraalVM, We have published a Docker image that includes GraalVM version 20.0.0, the JDK 8 with backported compiler interfaces (JVMCI), Maven 3.6.3, and the latest snapshot version of Serverless Java Container. + +The container image expects the Maven project - the directory containing the `pom.xml` file - to be mounted to the `/func` volume and will execute `/func/scripts/graalvm-build.sh` as it entrypoint. + +From the `native-pet-store` sample folder, use a shell to run the build process with Docker +```bash +$ docker run --rm -v $PWD:/func sapessi/aws-serverless-java-container-graalvm-build +``` + +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen + +``` +$ sam deploy --guided +``` + +Once the deployment completes, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL + +``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- + +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` \ No newline at end of file diff --git a/samples/springboot2/native-pet-store/pom.xml b/samples/springboot2/native-pet-store/pom.xml new file mode 100644 index 000000000..6aaeddc43 --- /dev/null +++ b/samples/springboot2/native-pet-store/pom.xml @@ -0,0 +1,110 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + native-serverless-springboot2-example + 1.0-SNAPSHOT + + + + + spring-milestone + Spring milestone + https://repo.spring.io/milestone + + + + + + + spring-milestone + Spring milestone + https://repo.spring.io/milestone + + + + + org.springframework.boot + spring-boot-starter-parent + 2.3.0.M4 + + + + 1.8 + 1.8 + com.amazonaws.serverless.sample.springboot2.Application + 5 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + org.springframework.boot + spring-boot-starter-logging + + + + + + org.springframework + spring-context-indexer + + + + org.springframework.experimental + spring-graal-native + 0.6.0.RELEASE + + + + + org.apache.logging.log4j + log4j-core + 2.13.1 + + + + com.amazonaws.serverless + aws-serverless-java-container-springboot2 + 1.6-SNAPSHOT + + + + + + + + org.graalvm.nativeimage + native-image-maven-plugin + 20.0.0 + + + -J-Xmx${docker.memory.gb}g -Dspring.graal.remove-unused-autoconfig=true --no-fallback --allow-incomplete-classpath --report-unsupported-elements-at-runtime -H:+ReportExceptionStackTraces --no-server --initialize-at-run-time=org.apache.logging.log4j.core.async.AsyncLoggerContext --initialize-at-run-time=org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor --initialize-at-run-time=org.apache.logging.log4j.core.pattern.JAnsiTextRenderer + + + + + native-image + + package + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/samples/springboot2/native-pet-store/scripts/bootstrap b/samples/springboot2/native-pet-store/scripts/bootstrap new file mode 100644 index 000000000..14791d4d8 --- /dev/null +++ b/samples/springboot2/native-pet-store/scripts/bootstrap @@ -0,0 +1,4 @@ +#!/bin/sh +set -euo pipefail + +./$_HANDLER \ No newline at end of file diff --git a/samples/springboot2/native-pet-store/scripts/graalvm-build.sh b/samples/springboot2/native-pet-store/scripts/graalvm-build.sh new file mode 100644 index 000000000..69fdb7a15 --- /dev/null +++ b/samples/springboot2/native-pet-store/scripts/graalvm-build.sh @@ -0,0 +1,17 @@ +#!/bin/sh +echo "Starting GraalVM build" +cd /func && mvn clean package + +if [[ $? -ne 0 ]]; then + echo "Maven build failed" + exit 1 +fi + +mkdir /func/target/lambda + +# $(find ./target -maxdepth 1 -type f -not -name "*.jar*") +GRAAL_EXECUTABLE=com.amazonaws.serverless.sample.springboot2.application +mv /func/target/$GRAAL_EXECUTABLE /func/target/lambda/$GRAAL_EXECUTABLE +chmod +x /func/target/lambda/$GRAAL_EXECUTABLE + +cp /func/scripts/bootstrap /func/target/lambda \ No newline at end of file diff --git a/samples/springboot2/native-pet-store/src/assembly/bin.xml b/samples/springboot2/native-pet-store/src/assembly/bin.xml new file mode 100644 index 000000000..1e085057d --- /dev/null +++ b/samples/springboot2/native-pet-store/src/assembly/bin.xml @@ -0,0 +1,27 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + tomcat-embed* + + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + \ No newline at end of file diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/Application.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/Application.java new file mode 100644 index 000000000..e6c77e601 --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/Application.java @@ -0,0 +1,55 @@ +package com.amazonaws.serverless.sample.springboot2; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.runtime.Runtime; +import com.amazonaws.serverless.sample.springboot2.controller.PetsController; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + + +@SpringBootApplication( + proxyBeanMethods = false, + exclude = { + DataSourceAutoConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class, + HibernateJpaAutoConfiguration.class, + }) +@Import({ PetsController.class }) +public class Application { + + // silence console logging + @Value("${logging.level.root:OFF}") + String message = ""; + + /* + * Create required HandlerMapping, to avoid several default HandlerMapping instances being created + */ + @Bean + public HandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + /* + * Create required HandlerAdapter, to avoid several default HandlerAdapter instances being created + */ + @Bean + public HandlerAdapter handlerAdapter() { + return new RequestMappingHandlerAdapter(); + } + + public static void main(String[] args) throws ContainerInitializationException { + //SpringApplication.run(Application.class, args); + Runtime lambdaRuntime = new Runtime(Application.class); + lambdaRuntime.start(); + } +} \ No newline at end of file diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/controller/PetsController.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/controller/PetsController.java new file mode 100644 index 000000000..ff3dda64e --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/controller/PetsController.java @@ -0,0 +1,79 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot2.controller; + + + +import com.amazonaws.serverless.sample.springboot2.model.Pet; +import com.amazonaws.serverless.sample.springboot2.model.PetData; + +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + + +@RestController +@EnableWebMvc +public class PetsController { + @RequestMapping(path = "/pets", method = RequestMethod.POST) + public Pet createPet(@RequestBody Pet newPet) { + if (newPet.getName() == null || newPet.getBreed() == null) { + return null; + } + + Pet dbPet = newPet; + dbPet.setId(UUID.randomUUID().toString()); + return dbPet; + } + + @RequestMapping(path = "/pets", method = RequestMethod.GET) + public List listPets(@RequestParam("limit") Optional limit, Principal principal) { + int queryLimit = 10; + if (limit.isPresent()) { + queryLimit = limit.get(); + } + + List outputPets = new ArrayList<>(queryLimit); + + for (int i = 0; i < queryLimit; i++) { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setName(PetData.getRandomName()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + outputPets.add(newPet); + } + + return outputPets; + } + + @RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET) + public Pet listPets() { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + newPet.setName(PetData.getRandomName()); + return newPet; + } + +} diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/filter/CognitoIdentityFilter.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/filter/CognitoIdentityFilter.java new file mode 100644 index 000000000..5c3482e40 --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/filter/CognitoIdentityFilter.java @@ -0,0 +1,69 @@ +package com.amazonaws.serverless.sample.springboot2.filter; + + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import java.io.IOException; + + +/** + * Simple Filter implementation that looks for a Cognito identity id in the API Gateway request context + * and stores the value in a request attribute. The filter is registered with aws-serverless-java-container + * in the onStartup method from the {@link com.amazonaws.serverless.sample.springboot2.StreamLambdaHandler} class. + */ +public class CognitoIdentityFilter implements Filter { + public static final String COGNITO_IDENTITY_ATTRIBUTE = "com.amazonaws.serverless.cognitoId"; + + private static Logger log = LoggerFactory.getLogger(CognitoIdentityFilter.class); + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // nothing to do in init + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + Object apiGwContext = servletRequest.getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + if (apiGwContext == null) { + log.warn("API Gateway context is null"); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (!AwsProxyRequestContext.class.isAssignableFrom(apiGwContext.getClass())) { + log.warn("API Gateway context object is not of valid type"); + filterChain.doFilter(servletRequest, servletResponse); + } + + AwsProxyRequestContext ctx = (AwsProxyRequestContext)apiGwContext; + if (ctx.getIdentity() == null) { + log.warn("Identity context is null"); + filterChain.doFilter(servletRequest, servletResponse); + } + String cognitoIdentityId = ctx.getIdentity().getCognitoIdentityId(); + if (cognitoIdentityId == null || "".equals(cognitoIdentityId.trim())) { + log.warn("Cognito identity id in request is null"); + } + servletRequest.setAttribute(COGNITO_IDENTITY_ATTRIBUTE, cognitoIdentityId); + filterChain.doFilter(servletRequest, servletResponse); + } + + + @Override + public void destroy() { + // nothing to do in destroy + } +} diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Error.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Error.java new file mode 100644 index 000000000..a2278ed02 --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Error.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot2.model; + +public class Error { + private String message; + + public Error(String errorMessage) { + message = errorMessage; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Pet.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Pet.java new file mode 100644 index 000000000..5d61e6bc9 --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Pet.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot2.model; + +import java.util.Date; + + +public class Pet { + private String id; + private String breed; + private String name; + private Date dateOfBirth; + + public Pet() { + + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(Date dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } +} diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/PetData.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/PetData.java new file mode 100644 index 000000000..34a324211 --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/PetData.java @@ -0,0 +1,117 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot2.model; + + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + + +public class PetData { + private static List breeds = new ArrayList<>(); + static { + breeds.add("Afghan Hound"); + breeds.add("Beagle"); + breeds.add("Bernese Mountain Dog"); + breeds.add("Bloodhound"); + breeds.add("Dalmatian"); + breeds.add("Jack Russell Terrier"); + breeds.add("Norwegian Elkhound"); + } + + private static List names = new ArrayList<>(); + static { + names.add("Bailey"); + names.add("Bella"); + names.add("Max"); + names.add("Lucy"); + names.add("Charlie"); + names.add("Molly"); + names.add("Buddy"); + names.add("Daisy"); + names.add("Rocky"); + names.add("Maggie"); + names.add("Jake"); + names.add("Sophie"); + names.add("Jack"); + names.add("Sadie"); + names.add("Toby"); + names.add("Chloe"); + names.add("Cody"); + names.add("Bailey"); + names.add("Buster"); + names.add("Lola"); + names.add("Duke"); + names.add("Zoe"); + names.add("Cooper"); + names.add("Abby"); + names.add("Riley"); + names.add("Ginger"); + names.add("Harley"); + names.add("Roxy"); + names.add("Bear"); + names.add("Gracie"); + names.add("Tucker"); + names.add("Coco"); + names.add("Murphy"); + names.add("Sasha"); + names.add("Lucky"); + names.add("Lily"); + names.add("Oliver"); + names.add("Angel"); + names.add("Sam"); + names.add("Princess"); + names.add("Oscar"); + names.add("Emma"); + names.add("Teddy"); + names.add("Annie"); + names.add("Winston"); + names.add("Rosie"); + } + + public static List getBreeds() { + return breeds; + } + + public static List getNames() { + return names; + } + + public static String getRandomBreed() { + return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1)); + } + + public static String getRandomName() { + return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1)); + } + + public static Date getRandomDoB() { + GregorianCalendar gc = new GregorianCalendar(); + + int year = ThreadLocalRandom.current().nextInt( + Calendar.getInstance().get(Calendar.YEAR) - 15, + Calendar.getInstance().get(Calendar.YEAR) + ); + + gc.set(Calendar.YEAR, year); + + int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR)); + + gc.set(Calendar.DAY_OF_YEAR, dayOfYear); + return gc.getTime(); + } +} diff --git a/samples/springboot2/native-pet-store/src/main/resources/logback.xml b/samples/springboot2/native-pet-store/src/main/resources/logback.xml new file mode 100644 index 000000000..ade52065d --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/resources/logback.xml @@ -0,0 +1,6 @@ + + + + + false + \ No newline at end of file diff --git a/samples/springboot2/native-pet-store/template.yml b/samples/springboot2/native-pet-store/template.yml new file mode 100644 index 000000000..4d26c3fb3 --- /dev/null +++ b/samples/springboot2/native-pet-store/template.yml @@ -0,0 +1,32 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written with SpringBoot with the aws-serverless-java-container library + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL + +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.serverless.sample.springboot2.application + Runtime: provided + CodeUri: target/lambda + MemorySize: 256 + Policies: AWSLambdaBasicExecutionRole + Timeout: 60 + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 3000 + PayloadFormatVersion: '1.0' + +Outputs: + SpringBootPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' + Export: + Name: SpringBootPetStoreApi From e5c1ca047ef1f746138c749e4e380babdfb81721 Mon Sep 17 00:00:00 2001 From: Stefano Buliani Date: Mon, 13 Apr 2020 21:15:31 -0700 Subject: [PATCH 3/5] fix: Temporarily disabled afterburner module for Jackson as it is not compatible with GraalVM's native image --- .../proxy/internal/LambdaContainerHandler.java | 14 ++++++++++---- .../src/main/resources/logback.xml | 6 ------ 2 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 samples/springboot2/native-pet-store/src/main/resources/logback.xml diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java index d31ae257b..1d896a31a 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java @@ -24,7 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +//import com.fasterxml.jackson.module.afterburner.AfterburnerModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -80,9 +80,15 @@ public abstract class LambdaContainerHandler - - - - false - \ No newline at end of file From 976f4e28e08f55b287cf47e2de380e38eee7c7e7 Mon Sep 17 00:00:00 2001 From: Stefano Buliani Date: Mon, 13 Apr 2020 21:17:23 -0700 Subject: [PATCH 4/5] fix: Updated runtime to receive the Handler object directly --- .../SpringBootLambdaContainerHandler.java | 11 ++--------- .../amazonaws/serverless/runtime/Runtime.java | 17 +++++++---------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java index 0eaf58f97..62ef2a31f 100644 --- a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java @@ -14,7 +14,6 @@ import com.amazonaws.serverless.exceptions.ContainerInitializationException; import com.amazonaws.serverless.proxy.*; -import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.servlet.*; import com.amazonaws.serverless.proxy.internal.testutils.Timer; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; @@ -25,18 +24,12 @@ import com.amazonaws.services.lambda.runtime.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.web.context.ConfigurableWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; import javax.servlet.Servlet; -import javax.servlet.ServletRegistration; import javax.servlet.http.HttpServletRequest; import java.util.concurrent.CountDownLatch; @@ -194,7 +187,7 @@ public void initialize() } applicationContext = builder.run(); if (springWebApplicationType == WebApplicationType.SERVLET) { - ((AnnotationConfigServletWebServerApplicationContext)applicationContext).setServletContext(getServletContext()); + ((ServletWebServerApplicationContext)applicationContext).setServletContext(getServletContext()); AwsServletRegistration reg = (AwsServletRegistration)getServletContext().getServletRegistration(DISPATCHER_SERVLET_REGISTRATION_NAME); if (reg != null) { reg.setLoadOnStartup(1); diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/Runtime.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/Runtime.java index 37acb9a1b..c84cd28ab 100644 --- a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/Runtime.java +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/Runtime.java @@ -1,8 +1,5 @@ package com.amazonaws.serverless.runtime; -import com.amazonaws.serverless.exceptions.ContainerInitializationException; -import com.amazonaws.serverless.proxy.model.AwsProxyRequest; -import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; @@ -15,16 +12,16 @@ public class Runtime implements RequestStreamHandler { private static final int MAX_RETRIES = 3; - private int maxRetries; - private SpringBootLambdaContainerHandler handler; + private final int maxRetries; + private final SpringBootLambdaContainerHandler handler; - public Runtime(Class initializer) throws ContainerInitializationException { - this(initializer, MAX_RETRIES); + public Runtime(SpringBootLambdaContainerHandler handler) { + this(handler, MAX_RETRIES); } - public Runtime(Class initializer, int retries) throws ContainerInitializationException { + public Runtime(SpringBootLambdaContainerHandler handler, int retries) { maxRetries = retries; - handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(initializer); + this.handler = handler; } public void start() { @@ -52,7 +49,7 @@ public void start(RuntimeClient client) throws RuntimeException { } private InvocationRequest getNextRequest(RuntimeClient client) { - InvocationRequest req = null; + InvocationRequest req; for (int i = 0; i < maxRetries; i++) { try { req = client.getNextEvent(); From 3e55220f53182017a39cfe4905cb82cd149d45e1 Mon Sep 17 00:00:00 2001 From: Stefano Buliani Date: Mon, 13 Apr 2020 21:23:45 -0700 Subject: [PATCH 5/5] fix: Updated build process to use the java agent to generate reflection metadata files for native image --- .../springboot2/native-pet-store/.gitignore | 1 + .../springboot2/native-pet-store/README.md | 2 + samples/springboot2/native-pet-store/pom.xml | 80 +++++++++++-------- .../native-pet-store/scripts/graalvm-build.sh | 17 +++- .../sample/springboot2/Application.java | 61 +++++--------- 5 files changed, 89 insertions(+), 72 deletions(-) create mode 100644 samples/springboot2/native-pet-store/.gitignore diff --git a/samples/springboot2/native-pet-store/.gitignore b/samples/springboot2/native-pet-store/.gitignore new file mode 100644 index 000000000..3160a3360 --- /dev/null +++ b/samples/springboot2/native-pet-store/.gitignore @@ -0,0 +1 @@ +src/main/resources/META-INF/native-image/* diff --git a/samples/springboot2/native-pet-store/README.md b/samples/springboot2/native-pet-store/README.md index b7e03a754..d6012a6f0 100644 --- a/samples/springboot2/native-pet-store/README.md +++ b/samples/springboot2/native-pet-store/README.md @@ -1,4 +1,6 @@ # Serverless Spring Boot 2 example for GraalVM Native Image +**Note that this is an experimental sample and the APIs it relies on should not be used in production. There will likely be breaking changes as the features stabilize!** + This experimental Pet Store example uses [Spring Boot's](https://projects.spring.io/spring-boot/) support for native compilation with [GraalVM](https://www.graalvm.org/) and executes as a custom runtime in AWS Lambda. The application uses Docker to compile a native executable compatible with AWS Lambda's runtime environment and can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. diff --git a/samples/springboot2/native-pet-store/pom.xml b/samples/springboot2/native-pet-store/pom.xml index 6aaeddc43..6b69e4d95 100644 --- a/samples/springboot2/native-pet-store/pom.xml +++ b/samples/springboot2/native-pet-store/pom.xml @@ -25,19 +25,20 @@ - - org.springframework.boot - spring-boot-starter-parent - 2.3.0.M4 - - 1.8 1.8 com.amazonaws.serverless.sample.springboot2.Application 5 + 2.3.0.M4 + + org.springframework.boot + spring-boot-starter-parent + 2.3.0.M4 + + org.springframework.boot @@ -47,10 +48,6 @@ org.springframework.boot spring-boot-starter-tomcat - - org.springframework.boot - spring-boot-starter-logging - @@ -62,43 +59,62 @@ org.springframework.experimental spring-graal-native - 0.6.0.RELEASE + 0.6.1.RELEASE - - org.apache.logging.log4j - log4j-core - 2.13.1 + javax.validation + validation-api + 2.0.1.Final com.amazonaws.serverless aws-serverless-java-container-springboot2 1.6-SNAPSHOT + + + com.fasterxml.jackson.module + jackson-module-afterburner + + + + + graalvm + + + + org.graalvm.nativeimage + native-image-maven-plugin + 20.0.0 + + + + -J-Xmx${docker.memory.gb}g -Dspring.graal.remove-unused-autoconfig=true -Dspring.graal.remove-yaml-support=true --no-fallback --allow-incomplete-classpath --report-unsupported-elements-at-runtime -H:+ReportExceptionStackTraces -H:EnableURLProtocols=http --no-server + + + + + native-image + + package + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + - - org.graalvm.nativeimage - native-image-maven-plugin - 20.0.0 - - - -J-Xmx${docker.memory.gb}g -Dspring.graal.remove-unused-autoconfig=true --no-fallback --allow-incomplete-classpath --report-unsupported-elements-at-runtime -H:+ReportExceptionStackTraces --no-server --initialize-at-run-time=org.apache.logging.log4j.core.async.AsyncLoggerContext --initialize-at-run-time=org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor --initialize-at-run-time=org.apache.logging.log4j.core.pattern.JAnsiTextRenderer - - - - - native-image - - package - - - org.springframework.boot spring-boot-maven-plugin diff --git a/samples/springboot2/native-pet-store/scripts/graalvm-build.sh b/samples/springboot2/native-pet-store/scripts/graalvm-build.sh index 69fdb7a15..f8d5c0af7 100644 --- a/samples/springboot2/native-pet-store/scripts/graalvm-build.sh +++ b/samples/springboot2/native-pet-store/scripts/graalvm-build.sh @@ -1,6 +1,21 @@ #!/bin/sh echo "Starting GraalVM build" -cd /func && mvn clean package + +METADATA_FOLDER=src/main/resources/META-INF/native-image + +mvn clean package + +if [[ $? -ne 0 ]]; then + echo "Maven build failed" + exit 1 +fi + +mkdir -p $METADATA_FOLDER + +java -agentlib:native-image-agent=config-output-dir=$METADATA_FOLDER \ + -Dagentrun=true -jar target/native-serverless-springboot2-example-1.0-SNAPSHOT.jar + +mvn -Pgraalvm clean package if [[ $? -ne 0 ]]; then echo "Maven build failed" diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/Application.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/Application.java index e6c77e601..2f2918054 100644 --- a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/Application.java +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/Application.java @@ -1,55 +1,38 @@ package com.amazonaws.serverless.sample.springboot2; import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; import com.amazonaws.serverless.runtime.Runtime; import com.amazonaws.serverless.sample.springboot2.controller.PetsController; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; -import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; -import org.springframework.context.annotation.Bean; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Import; -import org.springframework.web.servlet.HandlerAdapter; -import org.springframework.web.servlet.HandlerMapping; -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; -@SpringBootApplication( - proxyBeanMethods = false, - exclude = { - DataSourceAutoConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class, - HibernateJpaAutoConfiguration.class, - }) +@SpringBootApplication(proxyBeanMethods = false) @Import({ PetsController.class }) public class Application { - - // silence console logging - @Value("${logging.level.root:OFF}") - String message = ""; - - /* - * Create required HandlerMapping, to avoid several default HandlerMapping instances being created - */ - @Bean - public HandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); - } - - /* - * Create required HandlerAdapter, to avoid several default HandlerAdapter instances being created - */ - @Bean - public HandlerAdapter handlerAdapter() { - return new RequestMappingHandlerAdapter(); - } + private static ConfigurableApplicationContext ctx; public static void main(String[] args) throws ContainerInitializationException { - //SpringApplication.run(Application.class, args); - Runtime lambdaRuntime = new Runtime(Application.class); - lambdaRuntime.start(); + SpringBootLambdaContainerHandler handler = + new SpringBootProxyHandlerBuilder() + .defaultProxy() + .servletApplication() + .springBootApplication(Application.class) + .buildAndInitialize(); + if (System.getProperty("agentrun") != null) { + AwsProxyRequest testReq = new AwsProxyRequestBuilder("/pets", "GET").build(); + handler.proxy(testReq, new MockLambdaContext()); + } else { + Runtime lambdaRuntime = new Runtime(handler); + lambdaRuntime.start(); + } } } \ No newline at end of file