From 2fbe46bc1925c14b53697a9d4a7fdc955014993c Mon Sep 17 00:00:00 2001 From: cesarevalenti90 <50798547+cesarevalenti90@users.noreply.github.com> Date: Thu, 23 Mar 2023 21:43:34 +0100 Subject: [PATCH] BAEL-6255 - Run a Spring Boot application in AWS Lambda (#13629) * BAEL-6255 - Run a Spring Boot application in AWS Lambda * BAEL-6255 - Run a Spring Boot application in AWS Lambda * fix on template.yaml * fix on template.yaml * removed log from test * resolved issues reported on PR --------- Co-authored-by: Cesare --- spring-boot-modules/spring-boot-aws/README.md | 1 + spring-boot-modules/spring-boot-aws/pom.xml | 77 +++++++++++++++++++ .../java/com/baeldung/aws/Application.java | 12 +++ .../aws/controller/ProfileController.java | 21 +++++ .../main/java/com/baeldung/aws/data/User.java | 38 +++++++++ .../handler/AsynchronousLambdaHandler.java | 25 ++++++ .../aws/handler/HttpV2LambdaHandler.java | 26 +++++++ .../baeldung/aws/handler/LambdaHandler.java | 26 +++++++ .../src/main/resources/application.properties | 3 + .../com/baeldung/ProfileIntegrationTest.java | 36 +++++++++ .../spring-boot-aws/template.yaml | 32 ++++++++ 11 files changed, 297 insertions(+) create mode 100644 spring-boot-modules/spring-boot-aws/README.md create mode 100644 spring-boot-modules/spring-boot-aws/pom.xml create mode 100644 spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/Application.java create mode 100644 spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/controller/ProfileController.java create mode 100644 spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/data/User.java create mode 100644 spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/AsynchronousLambdaHandler.java create mode 100644 spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/HttpV2LambdaHandler.java create mode 100644 spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/LambdaHandler.java create mode 100644 spring-boot-modules/spring-boot-aws/src/main/resources/application.properties create mode 100644 spring-boot-modules/spring-boot-aws/src/test/java/com/baeldung/ProfileIntegrationTest.java create mode 100644 spring-boot-modules/spring-boot-aws/template.yaml diff --git a/spring-boot-modules/spring-boot-aws/README.md b/spring-boot-modules/spring-boot-aws/README.md new file mode 100644 index 0000000000..7d843af9ea --- /dev/null +++ b/spring-boot-modules/spring-boot-aws/README.md @@ -0,0 +1 @@ +### Relevant Articles: diff --git a/spring-boot-modules/spring-boot-aws/pom.xml b/spring-boot-modules/spring-boot-aws/pom.xml new file mode 100644 index 0000000000..bdb23f9fe2 --- /dev/null +++ b/spring-boot-modules/spring-boot-aws/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + com.baeldung + spring-boot-aws + 0.0.1-SNAPSHOT + spring-boot-data-3 + spring-boot-boot + + + com.baeldung.spring-boot-modules + spring-boot-modules + 1.0.0-SNAPSHOT + + + 1.9.1 + + + + + org.springframework.boot + spring-boot-starter-web + + + com.amazonaws.serverless + aws-serverless-java-container-springboot2 + ${springboot2.aws.version} + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + false + + + + package + + shade + + + + + org.apache.tomcat.embed:* + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + + \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/Application.java b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/Application.java new file mode 100644 index 0000000000..da3fe275b5 --- /dev/null +++ b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/Application.java @@ -0,0 +1,12 @@ +package com.baeldung.aws; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/controller/ProfileController.java b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/controller/ProfileController.java new file mode 100644 index 0000000000..674c4827ba --- /dev/null +++ b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/controller/ProfileController.java @@ -0,0 +1,21 @@ +package com.baeldung.aws.controller; + +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.aws.data.User; + +@RestController +@RequestMapping("/api/v1/") +public class ProfileController { + + @GetMapping(value = "users", produces = MediaType.APPLICATION_JSON_VALUE) + public List getUser() { + return List.of(new User("John", "Doe", "john.doe@baeldung.com"), new User("John", "Doe", "john.doe-2@baeldung.com")); + } + +} diff --git a/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/data/User.java b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/data/User.java new file mode 100644 index 0000000000..c5ebd1251f --- /dev/null +++ b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/data/User.java @@ -0,0 +1,38 @@ +package com.baeldung.aws.data; + +public class User { + + private String name; + private String surname; + private String emailAddress; + + public User(String name, String surname, String emailAddress) { + this.name = name; + this.surname = surname; + this.emailAddress = emailAddress; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSurname() { + return surname; + } + + public void setSurname(String surname) { + this.surname = surname; + } + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } +} diff --git a/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/AsynchronousLambdaHandler.java b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/AsynchronousLambdaHandler.java new file mode 100644 index 0000000000..30ffd2b17f --- /dev/null +++ b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/AsynchronousLambdaHandler.java @@ -0,0 +1,25 @@ +package com.baeldung.aws.handler; + +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.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.baeldung.aws.Application; + +public class AsynchronousLambdaHandler implements RequestHandler { + private SpringBootLambdaContainerHandler handler; + + public AsynchronousLambdaHandler() throws ContainerInitializationException { + handler = (SpringBootLambdaContainerHandler) new SpringBootProxyHandlerBuilder().springBootApplication(Application.class) + .asyncInit() + .buildAndInitialize(); + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequest input, Context context) { + return handler.proxy(input, context); + } +} diff --git a/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/HttpV2LambdaHandler.java b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/HttpV2LambdaHandler.java new file mode 100644 index 0000000000..a6fbe29695 --- /dev/null +++ b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/HttpV2LambdaHandler.java @@ -0,0 +1,26 @@ +package com.baeldung.aws.handler; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.baeldung.aws.Application; + +public class HttpV2LambdaHandler implements RequestHandler { + private static SpringBootLambdaContainerHandler handler; + + static { + try { + handler = SpringBootLambdaContainerHandler.getHttpApiV2ProxyHandler(Application.class); + } catch (ContainerInitializationException ex) { + throw new RuntimeException("Unable to load spring boot application", ex); + } + } + + @Override + public AwsProxyResponse handleRequest(HttpApiV2ProxyRequest input, Context context) { + return handler.proxy(input, context); + } +} diff --git a/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/LambdaHandler.java b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/LambdaHandler.java new file mode 100644 index 0000000000..887cbd3a26 --- /dev/null +++ b/spring-boot-modules/spring-boot-aws/src/main/java/com/baeldung/aws/handler/LambdaHandler.java @@ -0,0 +1,26 @@ +package com.baeldung.aws.handler; + +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.RequestHandler; +import com.baeldung.aws.Application; + +public class LambdaHandler implements RequestHandler { + private static SpringBootLambdaContainerHandler handler; + + static { + try { + handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class); + } catch (ContainerInitializationException ex) { + throw new RuntimeException("Unable to load spring boot application", ex); + } + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequest input, Context context) { + return handler.proxy(input, context); + } +} diff --git a/spring-boot-modules/spring-boot-aws/src/main/resources/application.properties b/spring-boot-modules/spring-boot-aws/src/main/resources/application.properties new file mode 100644 index 0000000000..b7e7157b35 --- /dev/null +++ b/spring-boot-modules/spring-boot-aws/src/main/resources/application.properties @@ -0,0 +1,3 @@ +spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase +spring.datasource.username=myusername +spring.datasource.password=mypassword \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-aws/src/test/java/com/baeldung/ProfileIntegrationTest.java b/spring-boot-modules/spring-boot-aws/src/test/java/com/baeldung/ProfileIntegrationTest.java new file mode 100644 index 0000000000..adab61e916 --- /dev/null +++ b/spring-boot-modules/spring-boot-aws/src/test/java/com/baeldung/ProfileIntegrationTest.java @@ -0,0 +1,36 @@ +package com.baeldung; + +import java.io.IOException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +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.baeldung.aws.handler.LambdaHandler; + +@SpringBootApplication +public class ProfileIntegrationTest { + + MockLambdaContext lambdaContext = new MockLambdaContext(); + + @Test + void whenTheUsersPathIsInvokedViaLambda_thenShouldReturnAList() throws IOException { + LambdaHandler lambdaHandler = new LambdaHandler(); + AwsProxyRequest req = new AwsProxyRequestBuilder("/api/v1/users", "GET").build(); + AwsProxyResponse resp = lambdaHandler.handleRequest(req, lambdaContext); + Assertions.assertNotNull(resp.getBody()); + Assertions.assertEquals(200, resp.getStatusCode()); + } + + @Test + void whenWrongPathPathIsInvokedViaLambda_thenShouldNotFound() throws IOException { + LambdaHandler lambdaHandler = new LambdaHandler(); + AwsProxyRequest req = new AwsProxyRequestBuilder("/api/v1/users/plus-one-level", "GET").build(); + AwsProxyResponse resp = lambdaHandler.handleRequest(req, lambdaContext); + Assertions.assertEquals(404, resp.getStatusCode()); + } +} diff --git a/spring-boot-modules/spring-boot-aws/template.yaml b/spring-boot-modules/spring-boot-aws/template.yaml new file mode 100644 index 0000000000..09880594c8 --- /dev/null +++ b/spring-boot-modules/spring-boot-aws/template.yaml @@ -0,0 +1,32 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Globals: + Function: + Timeout: 30 + +Resources: + ProfileApiFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: . + Handler: com.baeldung.aws.handler.LambdaHandler::handleRequest + Runtime: java11 + AutoPublishAlias: production + SnapStart: + ApplyOn: PublishedVersions + Architectures: + - x86_64 + MemorySize: 2048 + Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object + Variables: + JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1 # More info about tiered compilation https://aws.amazon.com/blogs/compute/optimizing-aws-lambda-function-performance-for-java/ + Policies: + - AWSSecretsManagerGetSecretValuePolicy: + SecretArn: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${DatabaseSecretName} + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /{proxy+} + Method: ANY \ No newline at end of file