From 6321c034262b68b629d9cdaf4ade7ce1ccdcb11b Mon Sep 17 00:00:00 2001 From: MaxExplode Date: Sun, 14 Aug 2022 09:26:42 +0800 Subject: [PATCH] spring boot lambda with dynamo db --- README.md | 82 ++++++++ build.gradle | 29 +++ pom.xml | 186 ++++++++++++++++++ src/assembly/bin.xml | 27 +++ src/main/java/org/example/Application.java | 19 ++ .../java/org/example/StreamLambdaHandler.java | 39 ++++ .../controller/InventoryController.java | 34 ++++ .../java/org/example/domain/Inventory.java | 48 +++++ .../org/example/service/InventoryService.java | 47 +++++ src/main/resources/application.properties | 3 + .../java/org/example/InventoryDataTest.java | 38 ++++ .../org/example/StreamLambdaHandlerTest.java | 90 +++++++++ template.yml | 30 +++ 13 files changed, 672 insertions(+) create mode 100644 README.md create mode 100644 build.gradle create mode 100644 pom.xml create mode 100644 src/assembly/bin.xml create mode 100644 src/main/java/org/example/Application.java create mode 100644 src/main/java/org/example/StreamLambdaHandler.java create mode 100644 src/main/java/org/example/controller/InventoryController.java create mode 100644 src/main/java/org/example/domain/Inventory.java create mode 100644 src/main/java/org/example/service/InventoryService.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/org/example/InventoryDataTest.java create mode 100644 src/test/java/org/example/StreamLambdaHandlerTest.java create mode 100644 template.yml diff --git a/README.md b/README.md new file mode 100644 index 0000000..ead620f --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# simple-spring-boot-lambda serverless API +The simple-spring-boot-lambda project, created with [`aws-serverless-java-container`](https://github.com/awslabs/aws-serverless-java-container). + +The starter project defines a simple `/ping` resource that can accept `GET` requests with its tests. + +The project folder also includes a `template.yml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with the [SAM CLI](https://github.com/awslabs/aws-sam-cli). + +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +## Building the project +You can use the SAM CLI to quickly build the project +```bash +$ mvn archetype:generate -DartifactId=simple-spring-boot-lambda -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-jersey-archetype -DarchetypeVersion=1.8 -DgroupId=org.example -Dversion=1.0-SNAPSHOT -Dinteractive=false +$ cd simple-spring-boot-lambda +$ sam build +Building resource 'SimpleSpringBootLambdaFunction' +Running JavaGradleWorkflow:GradleBuild +Running JavaGradleWorkflow:CopyArtifacts + +Build Succeeded + +Built Artifacts : .aws-sam/build +Built Template : .aws-sam/build/template.yaml + +Commands you can use next +========================= +[*] Invoke Function: sam local invoke +[*] Deploy: sam deploy --guided +``` + +## Testing locally with the SAM CLI + +From the project root folder - where the `template.yml` file is located - start the API with the SAM CLI. + +```bash +$ sam local start-api + +... +Mounting com.amazonaws.serverless.archetypes.StreamLambdaHandler::handleRequest (java8) at http://127.0.0.1:3000/{proxy+} [OPTIONS GET HEAD POST PUT DELETE PATCH] +... +``` + +Using a new shell, you can send a test ping request to your API: + +```bash +$ curl -s http://127.0.0.1:3000/ping | python -m json.tool + +{ + "pong": "Hello, World!" +} +``` + +## Deploying to AWS +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 is completed, 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 +------------------------------------------------------------------------------------------------------------- +SimpleSpringBootLambdaApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets +------------------------------------------------------------------------------------------------------------- +``` + +Copy the `OutputValue` into a browser or use curl to test your first request: + +```bash +$ curl -s https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/ping | python -m json.tool + +{ + "pong": "Hello, World!" +} +``` diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..729e767 --- /dev/null +++ b/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'java' + +repositories { + jcenter() + mavenLocal() + mavenCentral() +} + +dependencies { + implementation ( + 'org.springframework.boot:spring-boot-starter-web:2.6.5', + 'com.amazonaws.serverless:aws-serverless-java-container-springboot2:[1.0,)', + 'io.symphonia:lambda-logging:1.0.3' + ) + + testImplementation("junit:junit:4.13.2") +} + +task buildZip(type: Zip) { + from compileJava + from processResources + into('lib') { + from(configurations.compileClasspath) { + exclude 'tomcat-embed-*' + } + } +} + +build.dependsOn buildZip diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9c5f973 --- /dev/null +++ b/pom.xml @@ -0,0 +1,186 @@ + + + 4.0.0 + + org.example + simple-spring-boot-lambda + 1.0-SNAPSHOT + jar + + Serverless Spring Boot 2 API + https://github.com/awslabs/aws-serverless-java-container + + + org.springframework.boot + spring-boot-starter-parent + 2.6.5 + + + + + dynamodb-local-oregon + DynamoDB Local Release Repository + https://s3-us-west-2.amazonaws.com/dynamodb-local/release + + + + + 11 + 11 + + + + + + software.amazon.awssdk + bom + 2.17.248 + pom + import + + + + + + + com.amazonaws.serverless + aws-serverless-java-container-springboot2 + 1.8.2 + + + + software.amazon.awssdk + dynamodb-enhanced + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + + com.amazonaws + DynamoDBLocal + [1.12,2.0) + test + + + junit + junit + 4.12 + test + + + + + + shaded-jar + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + false + + + + package + + shade + + + + + org.apache.tomcat.embed:* + + + + + + + + + + + assembly-zip + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-install-plugin + 3.0.0-M1 + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.2.0 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}${file.separator}lib + runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + zip-assembly + package + + single + + + ${project.artifactId}-${project.version} + + src${file.separator}assembly${file.separator}bin.xml + + false + + + + + + + + + diff --git a/src/assembly/bin.xml b/src/assembly/bin.xml new file mode 100644 index 0000000..1e08505 --- /dev/null +++ b/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/src/main/java/org/example/Application.java b/src/main/java/org/example/Application.java new file mode 100644 index 0000000..2d9b284 --- /dev/null +++ b/src/main/java/org/example/Application.java @@ -0,0 +1,19 @@ +package org.example; + +import org.example.controller.InventoryController; +import org.example.service.InventoryService; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; + + +@SpringBootApplication +// We use direct @Import instead of @ComponentScan to speed up cold starts +// @ComponentScan(basePackages = "org.example.controller") +@Import({ InventoryController.class, InventoryService.class }) +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/StreamLambdaHandler.java b/src/main/java/org/example/StreamLambdaHandler.java new file mode 100644 index 0000000..8a69189 --- /dev/null +++ b/src/main/java/org/example/StreamLambdaHandler.java @@ -0,0 +1,39 @@ +package org.example; + + +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.IOException; +import java.io.InputStream; +import java.io.OutputStream; + + +public class StreamLambdaHandler implements RequestStreamHandler { + private static SpringBootLambdaContainerHandler handler; + static { + try { + handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class); + // For applications that take longer than 10 seconds to start, use the async builder: + // handler = new SpringBootProxyHandlerBuilder() + // .defaultProxy() + // .asyncInit() + // .springBootApplication(Application.class) + // .buildAndInitialize(); + } catch (ContainerInitializationException e) { + // if we fail here. We re-throw the exception to force another cold start + e.printStackTrace(); + throw new RuntimeException("Could not initialize Spring Boot application", e); + } + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + handler.proxyStream(inputStream, outputStream, context); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/controller/InventoryController.java b/src/main/java/org/example/controller/InventoryController.java new file mode 100644 index 0000000..1d06ebc --- /dev/null +++ b/src/main/java/org/example/controller/InventoryController.java @@ -0,0 +1,34 @@ +package org.example.controller; + +import org.example.domain.Inventory; +import org.example.service.InventoryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.util.List; + +@RestController +@EnableWebMvc +public class InventoryController { + + @Autowired + private InventoryService inventoryService; + + @GetMapping("/{id}") + public Inventory getInventory(@PathVariable Long id) { + return inventoryService.getItem(id); + } + + @GetMapping + public List getInventoryList() { + return inventoryService.getItems(); + } + + @PostMapping + public void createInventory(@RequestBody Inventory item) { + inventoryService.creteItem(item); + } + + +} diff --git a/src/main/java/org/example/domain/Inventory.java b/src/main/java/org/example/domain/Inventory.java new file mode 100644 index 0000000..79c2b54 --- /dev/null +++ b/src/main/java/org/example/domain/Inventory.java @@ -0,0 +1,48 @@ +package org.example.domain; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbBean +public class Inventory { + + private Long inventoryId; + private String itemName; + private Double itemPrice; + private Integer itemQuantity; + + @DynamoDbPartitionKey + @DynamoDbAttribute("inventory_id") + public Long getInventoryId() { + return inventoryId; + } + + public void setInventoryId(Long inventoryId) { + this.inventoryId = inventoryId; + } + + public String getItemName() { + return itemName; + } + + public void setItemName(String itemName) { + this.itemName = itemName; + } + + public Double getItemPrice() { + return itemPrice; + } + + public void setItemPrice(Double itemPrice) { + this.itemPrice = itemPrice; + } + + public Integer getItemQuantity() { + return itemQuantity; + } + + public void setItemQuantity(Integer itemQuantity) { + this.itemQuantity = itemQuantity; + } +} diff --git a/src/main/java/org/example/service/InventoryService.java b/src/main/java/org/example/service/InventoryService.java new file mode 100644 index 0000000..3e934a0 --- /dev/null +++ b/src/main/java/org/example/service/InventoryService.java @@ -0,0 +1,47 @@ +package org.example.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.example.domain.Inventory; +import org.springframework.stereotype.Service; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class InventoryService { + + private static final DynamoDbEnhancedClient DB_ENHANCED_CLIENT = + DynamoDbEnhancedClient.create(); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private static final DynamoDbTable inventory = DB_ENHANCED_CLIENT + .table("inventory", TableSchema.fromBean(Inventory.class)); + + public Inventory getItem(Long id) + { + Inventory key = new Inventory(); + key.setInventoryId(id); + return inventory.getItem(key); + } + + public List getItems() + { + return inventory.scan().items().stream().collect(Collectors.toList()); + } + + public void creteItem(Inventory item) + { + try { + System.out.println("item : " + OBJECT_MAPPER.writeValueAsString(item)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + inventory.putItem(item); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..ec1cb97 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,3 @@ +# Reduce logging level to make sure the application works with SAM local +# https://github.com/awslabs/aws-serverless-java-container/issues/134 +logging.level.root=WARN \ No newline at end of file diff --git a/src/test/java/org/example/InventoryDataTest.java b/src/test/java/org/example/InventoryDataTest.java new file mode 100644 index 0000000..21cb6fb --- /dev/null +++ b/src/test/java/org/example/InventoryDataTest.java @@ -0,0 +1,38 @@ +package org.example; + +import com.amazonaws.services.dynamodbv2.local.main.CommandLineInput; +import com.amazonaws.services.dynamodbv2.local.main.ServerRunner; +import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.example.domain.Inventory; +import org.junit.BeforeClass; +import org.junit.Test; + +public class InventoryDataTest { + static DynamoDBProxyServer dynamoDBProxyServer; + + @BeforeClass + public static void setup() throws Exception + { + String port = "8000"; + dynamoDBProxyServer = ServerRunner.createServer(new CommandLineInput( + new String[]{"-inMemory", "-port", port} )); + + dynamoDBProxyServer.start(); + } + + @Test + public void testInsert() throws JsonProcessingException { + Inventory inventory + = new Inventory(); + inventory.setInventoryId(123L); + inventory.setItemName("abc"); + inventory.setItemQuantity(5); + inventory.setItemPrice(50.5); + + ObjectMapper objectMapper = new ObjectMapper(); + System.out.println(objectMapper.writeValueAsString(inventory)); + + } +} diff --git a/src/test/java/org/example/StreamLambdaHandlerTest.java b/src/test/java/org/example/StreamLambdaHandlerTest.java new file mode 100644 index 0000000..98cd3e7 --- /dev/null +++ b/src/test/java/org/example/StreamLambdaHandlerTest.java @@ -0,0 +1,90 @@ +package org.example; + + +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.AwsProxyResponse; +import com.amazonaws.services.lambda.runtime.Context; + +import org.junit.BeforeClass; +import org.junit.Test; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.*; + + +public class StreamLambdaHandlerTest { + + private static StreamLambdaHandler handler; + private static Context lambdaContext; + + @BeforeClass + public static void setUp() { + handler = new StreamLambdaHandler(); + lambdaContext = new MockLambdaContext(); + } + + @Test + public void ping_streamRequest_respondsWithHello() { + InputStream requestStream = new AwsProxyRequestBuilder("/ping", HttpMethod.GET) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) + .buildStream(); + ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); + + handle(requestStream, responseStream); + + AwsProxyResponse response = readResponse(responseStream); + assertNotNull(response); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode()); + + assertFalse(response.isBase64Encoded()); + + assertTrue(response.getBody().contains("pong")); + assertTrue(response.getBody().contains("Hello, World!")); + + assertTrue(response.getMultiValueHeaders().containsKey(HttpHeaders.CONTENT_TYPE)); + assertTrue(response.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON)); + } + + @Test + public void invalidResource_streamRequest_responds404() { + InputStream requestStream = new AwsProxyRequestBuilder("/pong", HttpMethod.GET) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) + .buildStream(); + ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); + + handle(requestStream, responseStream); + + AwsProxyResponse response = readResponse(responseStream); + assertNotNull(response); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatusCode()); + } + + private void handle(InputStream is, ByteArrayOutputStream os) { + try { + handler.handleRequest(is, os, lambdaContext); + } catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + private AwsProxyResponse readResponse(ByteArrayOutputStream responseStream) { + try { + return LambdaContainerHandler.getObjectMapper().readValue(responseStream.toByteArray(), AwsProxyResponse.class); + } catch (IOException e) { + e.printStackTrace(); + fail("Error while parsing response: " + e.getMessage()); + } + return null; + } +} diff --git a/template.yml b/template.yml new file mode 100644 index 0000000..b31a65f --- /dev/null +++ b/template.yml @@ -0,0 +1,30 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: AWS Serverless Spring Boot 2 API - org.example::simple-spring-boot-lambda +Globals: + Api: + EndpointConfiguration: REGIONAL + +Resources: + SimpleSpringBootLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: org.example.StreamLambdaHandler::handleRequest + Runtime: java8 + CodeUri: . + MemorySize: 512 + Policies: AWSLambdaBasicExecutionRole + Timeout: 30 + Events: + ProxyResource: + Type: Api + Properties: + Path: /{proxy+} + Method: any + +Outputs: + SimpleSpringBootLambdaApi: + Description: URL for application + Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/ping' + Export: + Name: SimpleSpringBootLambdaApi