Introduction
In the part 8 we introduced concepts behind Spring Cloud Function (AWS). In this article we'll take a look into how to write AWS Lambda function with Java 21 runtime and Spring Cloud Function AWS using Spring Boot 3.2 version. 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 Spring Cloud Function AWS 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.
There are multiple ways to write AWS Lambda with Spring Cloud Function AWS using Spring Boot 3.2 :
Broadly speaking AWS Lambda with AWS Serverless Java Container introduced in the article Develop application with AWS Serverless Java Container can also be considered as Spring Cloud Function AWS application because of dependency spring-cloud-function-serverless-web which aws-serverless-java-container-springboot3 requires. This is the collaboration effort between Spring and AWS Serverless developers. It provides Spring Cloud Function on AWS Lambda functionality.
We can also define Spring Beans (annotated with @ Bean) directly in the main Spring Boot Application class (annotated with @SpringBootApplication) and map the method name annotated with @ Bean (which is the default bean name) to exactly match the Lambda function name in the Infrastructure as a Code (i.e. AWS SAM template). Example of such approach can be found here.
Another approach is to use org.springframework.cloud.function.adapter.aws.web.WebProxyInvoker::handleRequest Lambda handler and normal Spring Boot RestController (annotated with @RestController). Example of such approach can be found here.
In this article I'd like to show more classical approach how to convert Java 8 Function interface into AWS Lambda function.
In the sample application we'll create and retrieve products and use DynamoDB as the NoSQL database. You can find the DynamoProductDao.java implementation here. We also put Amazon API Gateway in front of it as defined in AWS SAM template.
In the pom.xml we need to define these dependencies among others:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
</dependency>
which will convert Spring Cloud Function into AWS Lambda and expose it as web application.
This is our Spring Boot main class which we also have to have to additionally define in the SAM template in the environment variables section:
Globals:
Function:
.....
Environment:
Variables:
MAIN_CLASS: software.amazonaws.Application
Next we implement our GetProductByIdHandler which implements java.util.function.Function interface and has to be annotated with Spring @Component annotation.
@Component
public class GetProductByIdHandler implements
Function<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
@Autowired
private DynamoProductDao productDao;
...
public APIGatewayProxyResponseEvent apply(APIGatewayProxyRequestEvent requestEvent) {
String id = requestEvent.getPathParameters().get("id");
Optional<Product> optionalProduct = productDao.getProduct(id);
return new APIGatewayProxyResponseEvent().
withStatusCode(HttpStatusCode.OK).
withBody(....);
}
@Component is an annotation that allows Spring to detect our custom beans automatically. By default, Spring takes the simple name of the type (Java class) declaring the bean, changes the first letter to lowercase, and uses the resulting value to name the bean.
We can implement this Lambda function also without certain AWS dependencies by returning Product or Optional directly instead of wrapping it into APIGatewayProxyResponseEvent object.
Next we define the Lambda function mapping in SAM template
GetProductByIdFunction:
Type: AWS::Serverless::Function
Properties:
Environment:
Variables:
SPRING_CLOUD_FUNCTION_DEFINITION: getProductByIdHandler
FunctionName: GetProductByIdWithSpringBoot32SCF
...
Events:
GetRequestById:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /products/{id}
Method: get
The relevant part is here the environment variable SPRING_CLOUD_FUNCTION_DEFINITION which is the Spring bean name which in our case corresponds to the Lambda function class name with the first letter changed to lowercase. In our example Lambda function with environment variable SPRING_CLOUD_FUNCTION_DEFINITION = getProductByIdHandler is mapped to the Lambda Java class GetProductByIdHandler.
Note that since AWS does not allow dots . and/or hyphens - in the name of the environment variable, we can benefit from Spring Boot support and simply substitute dots with underscores and hyphens with camel case. So for example spring.cloud.function.definition becomes spring_cloud_function_definition. We can also write the variable name i.e. spring.cloud.function.definition and spring.cloud.function.routing-expression using all capital letters.
Also in the SAM template in the global Lambda function environment variables we define the generic Spring Cloud Function AWS Lambda handler
Globals:
Function:
Handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
which routes all requests to the correct Lambda implementation (we defined separate Lambda Handler Java class for each Lambda function implementation).
You can learn more about Spring Cloud Function routing and filtering capabilities in this article.
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
Conclusion
In this article we took a look into how to write AWS Lambda function with Java 21 runtime with Spring Cloud Function AWS using Spring Boot 3 version. In comparison to AWS Serverless Java Container and AWS Lambda Web Adapter, Spring Cloud Function AWS doesn't strictly require but supports reusing Spring Boot Rest Controller. In our main example of using Spring Cloud Function AWS we exposed Java 8 Function as Lambda function.
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 and also apply various priming techniques for the DynamoDB invocation.