DEV Community

Vadym Kazulkin for AWS Community Builders

Posted on • Edited on

Spring Boot 3 application on AWS Lambda - Part 3 Develop application with AWS Serverless Java Container

Introduction

In the part 2 of the series we introduced AWS Serverless Java Container. In this article we'll take a look into how to write AWS Lambda function with AWS Serverless Java Container using Spring Boot 3 version and Java 21 runtime. In the time of writing this article, the newest version of Spring Boot is 3.2 which became available end of November 2023. To use the newer version of Spring Boot (i.e. 3.3) it will maybe be enough to update the version in pom.xml.

How to write AWS Lambda with AWS Serverless Java Container using Spring Boot 3.2

For the sake of explanation we'll use our Spring Boot 3.2 sample application and use Java 21 runtime for our Lambda functions.

Image description

In this application we'll create and retrieve products and use DynamoDB as the NoSQL database. You can find the DynamoProductDao implementation here. We also put Amazon API Gateway in front of it as defined in AWS SAM template.

Spring Boot Product Controller annotated with @RestController and
@EnableWebMvc defines getProductById and createProduct methods.

@RequestMapping(path = "/products/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public Optional<Product> getProductById(@PathVariable("id") String id) {
        return productDao.getProduct(id);
    }

@RequestMapping(path = "/products/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE)
    public void createProduct(@PathVariable("id") String id, @RequestBody Product product) {
        product.setId(id);
        productDao.putProduct(product);
    }
Enter fullscreen mode Exit fullscreen mode

The mapping of the controller methods to the defintion of the Lambda function happens in the SAM template.

For example getProductById method is mapped to the following Lambda function :

  GetProductByIdFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: GetProductByIdWithSpringBoot32
      ....
      Events:
        GetRequestById:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /products/{id}
            Method: get      
Enter fullscreen mode Exit fullscreen mode

We see the same HTTP method "get" and same path "/products/{id}" in the Lambda funtion event and in the request mapping of the getProductById method. It works similar for the mapping of the createProduct method in the rest controller.

The key dependency to make it work and translate between Spring Boot 3 (web annotation) model and AWS Lambda is the dependency to the artifact aws-serverless-java-container-springboot3 defined in the pom.xml. It's based on Serverless Java Container which natively supports API Gateway's proxy integration models for requests and responses, and we can create and inject custom models for methods that use custom mappings.

    <dependency>
      <groupId>com.amazonaws.serverless</groupId>
      <artifactId>aws-serverless-java-container-springboot3</artifactId>
      <version>2.0.0</version>
    </dependency>
Enter fullscreen mode Exit fullscreen mode

If we look into the whole dependency tree, we'll discover another dependency spring-cloud-function-serverless-web which aws-serverless-java-container-springboot3 requires which is the collaboration effort between Spring and AWS Serverless developers. It provides Spring Cloud Function on AWS Lambda functionallity. We'll look deeper into the capabilities of Spring Cloud Function on AWS Lambda in one of my upcoming articles.

The simplest way to wire everything together is to define in the AWS SAM template generic SpringDelegatingLambdaContainerHandler handler from the artifact aws-serverless-java-container-springboot3 and define the main class of our SpringBoot application (the class annotated with @SpringBootApplication) :

Handler: com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler 
Environment:
  Variables:
    MAIN_CLASS: com.amazonaws.Application
Enter fullscreen mode Exit fullscreen mode

SpringDelegatingLambdaContainerHandler plays the role of proxy, receives all requests and forwards them to the correct method of our Spring Boot Product Rest Controller.

Another way to wire everything together is to implement our own StreamLambdaHandler which is the implementation of the com.amazonaws.services.lambda.runtime.RequestStreamHandler interface.

Globals:
  Function:
    Handler: software.amazonaws.example.product.handler.StreamLambdaHandler::handleRequest
Enter fullscreen mode Exit fullscreen mode

StreamLambdaHandler as custom generic proxy receives then all requests and forwards them to the correct method of our Spring Boot Product Rest Controller.

In the StreamLambdaHandler we first instantiate SpringBootLambdaContainerHandler handler

SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler =
SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class);
Enter fullscreen mode Exit fullscreen mode

and then proxy request through it. This approach is preferable in case we intend to implement own logic there, like we'll see with Lambda SnapStart priming in the next article.

  @Override
  public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
            throws IOException {
    handler.proxyStream(inputStream, outputStream, context);
 }
Enter fullscreen mode Exit fullscreen mode

In that step the input streams will be forwarded to the intended method of the Product Controller. As the last step we need to define this StreamLambdaHandler.java in the AWS SAM template.

Then we need to deploy the application with sam deploy -g and to retrieve the existing product we have to invoke the following:

curl -H "X-API-Key: a6ZbcDefQW12BN56WEA7"
https://{$API_GATEWAY_URL}/prod/products/1
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article we took a look into how to write AWS Lambda functions with Java 21 runtime with AWS Serverless Java Container using Spring 3.2 version. As we explored, we can re-use the Spring Boot Rest Controller. The can either re-use existing SpringDelegatingLambdaContainerHandler or to write our custom RequestStreamHandler implementation (in our case StreamLambdaHandler) which proxies the invocation to the write method of the Spring Boot Rest Controller annotated with @RequestMapping or @GET, @PUT and so on.

In the next article of the series we'll measure the cold and warm start times for this sample application including enabling SnapStart on the Lambda function but also also applying various priming techniques like priming the DynamoDB invocation and priming the whole web request.

Top comments (3)

Collapse
 
mac10046 profile image
Abdeali

Hi the articles series is nice, helpful and worth reading it. But would also like to know response time of the API on first call and later calls once the cold start is done.

Thanks for the GitHub repo as well ❤️

Collapse
 
vkazulkin profile image
Vadym Kazulkin • Edited

I can measure it (API Gateway or HTTP API overall latency), but it basically adds the same latency range on top of each execution undepending of the runtime/language, cold or warm start. I did these kind of measurements with Java 11 in this article dev.to/aws-builders/measuring-java... and the additional latency added by API Gateway should be still the same.

Collapse
 
kvin97 profile image
Kavindu Vindika

It can be painful with the cold starts having spring boot as the framework. We had an application written using spring boot having lambda as the infrastructure. But it hits a huge cold start time for the first invocation which isn’t tolerable ! So better be mindful when running a heavy framework inside lambdas.