Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
1. Overview
AWS Lambda is a serverless computing service provided by Amazon. Itβs a powerful tool that helps us build scalable event-driven applications.
The serverless nature of AWS Lambda allows us to focus on our business logic, while AWS takes care of dynamic allocation and provisioning of servers. Itβs also a cost-effective solution as we only pay for the actual execution time and memory consumption of our code.
In this tutorial, weβll explore how to create a basic AWS Lambda function using Java. Weβll cover the necessary dependencies, different ways of creating our Lambda function, building the deployment file, and testing our Lambda function locally using LocalStack.
To follow this tutorial, weβll need an active AWS account.
2. Dependencies
Letβs start by adding the Lambda core dependency to our projectβs pom.xml file:
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.2.3</version>
</dependency>
Next, weβll need to add the Maven Shade Plugin:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
The Maven Shade Plugin is essential when building AWS Lambda functions with Java. It allows us to package our application and its dependencies into a single, self-contained JAR file, also known as an βuberβ or βfatβ JAR.
The plugin extracts the content of all our dependencies and puts them with the classes of our project, which is how AWS Lambda expects us to deploy our code.
We can create our fat JAR in the target directory of our project by executing:
mvn clean package
When using Gradle, we can create our fat JAR using the Gradle Shadow Plugin.
3. Creating a Handler
The entry point for any AWS Lambda function is a handler method. It processes the incoming request and returns a response.
When creating a Lambda function, we have to specify our handler. We do this using the format package.ClassName. Weβll look at how to specify this configuration in the next sections where we test and deploy our Lambda function.
We have a few different options when it comes to defining our handler method and weβll explore them in this section.
3.1. Implementing the RequestHandler Interface
The most common and recommended way to define a handler is by implementing the RequestHandler interface and overriding its handleRequest() method:
class LambdaHandler implements RequestHandler<Request, Response> {
@Override
public Response handleRequest(Request request, Context context) {
LambdaLogger logger = context.getLogger();
logger.log("Processing question from " + request.name(), LogLevel.INFO);
return new Response("Subscribe to Baeldung Pro: baeldung.com/members");
}
}
record Request(String name, String question) {}
record Response(String answer) {}
The Request and Response are simple records that represent the input and output of our Lambda function. We also specify these types as generic parameters in the RequestHandler interface.
The handleRequest() method takes our Request record and a Context object as parameters. The Context parameter provides useful information about the Lambda execution environment, including a LambdaLogger that we can use for logging.
3.2. Implementing the RequestStreamHandler Interface
Another approach to define a handler is to implement the RequestStreamHandler interface:
class LambdaStreamHandler implements RequestStreamHandler {
@Override
public void handleRequest(InputStream input, OutputStream output, Context context) {
ObjectMapper mapper = new ObjectMapper();
Request request = mapper.readValue(input, Request.class);
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output))) {
writer.write("Hello " + request.name() + ", Baeldung has great Java content for you!");
writer.flush();
}
}
record Request(String name) {}
}
Here, we implement the RequestStreamHandler interface and override the handleRequest() method. Using ObjectMapper, we deserialize the raw request data from the InputStream and write our response to the OutputStream.
This interface is useful when working with raw input and output streams.
3.3. Custom Handler Method
Lastly, we can define a custom handler method:
class CustomLambdaHandler {
public Response handlingRequestFreely(Request request, Context context) {
LambdaLogger logger = context.getLogger();
logger.log(request.name() + " has invoked the lambda function", LogLevel.INFO);
return new Response("Subscribe to Baeldung Pro: baeldung.com/members");
}
record Request(String name) {}
record Response(String answer) {}
}
In this approach, we create a CustomLambdaHandler class with a handlingRequestFreely() method that takes the Request record and Context object as parameters, similar to the RequestHandler example. The only difference is that weβre not implementing any specific interface.
Unlike the previous two approaches, when creating a custom handler method, we need to use the format package.ClassName::methodName to configure our handler. For example, if our CustomLambdaHandler class is in the package com.baeldung.lambda, then weβll specify the handler as com.baeldung.lambda.CustomLambdaHandler::handlingRequestFreely when creating our Lambda function.
4. Testing Lambda Function Locally Using LocalStack
During development, itβs often convenient to test our Lambda functions locally before deploying them to AWS. LocalStack is a popular tool that allows us to run an emulated AWS environment locally on our machine.
Weβll test our LambdaHandler class, which we created earlier by implementing the RequestHandler interface.
First, letβs start a LocalStack container using Docker:
docker run \
--rm -it \
-p 127.0.0.1:4566:4566 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ./target:/opt/code/localstack/target \
localstack/localstack
We map the required port and mount our projectβs target directory, which contains our fat JAR, into the container. Itβs important to note that there are other ways to install Localstack as well.
Next, weβll get into our containerβs shell and create our Lambda function:
awslocal lambda create-function \
--function-name baeldung-lambda-function \
--runtime java21 \
--handler com.baeldung.lambda.LambdaHandler\
--role arn:aws:iam::000000000000:role/lambda-role \
--zip-file fileb:///opt/code/localstack/target/java-lambda-function-0.0.1.jar
We specify Java 21 as the runtime, our handler, and the location of our JAR file in the container using the zip-file parameter.
With our function created, letβs now invoke it:
awslocal lambda invoke \
--function-name baeldung-lambda-function \
--payload '{ "name": "John Doe", "question": "How do I view articles ad-free and in dark mode on Baeldung?" }' output.txt
We pass our function name and JSON request payload. The response from our Lambda function is saved in the specified output.txt file, which contains:
{
"answer": "Subscribe to Baeldung Pro: baeldung.com/members"
}
In case of any errors, the error details will also be logged in our output.txt file.
Running our Lambda functions locally with LocalStack allows us to catch issues early in the development process.
5. Deploying Lambda Function
Now that weβve created our Lambda function and tested it locally, letβs look at how we can deploy it to our AWS environment.
Weβll use AWS CloudFormation, which allows us to define and manage our Infrastructure as Code (IaC). Weβll deploy the same Lambda function that we tested in the previous section.
5.1. Creating AWS CloudFormation Template
First, weβll need to store our fat JAR file in an Amazon S3 bucket. This is necessary as CloudFormation references the JAR file from the specified S3 bucket during the deployment process.
Next, letβs create a generic CloudFormation template for our Lambda function:
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda function deployment with Java 21 runtime
Parameters:
LambdaHandler:
Type: String
Description: The handler for the Lambda function
S3BucketName:
Type: String
Description: The name of the S3 bucket containing the Lambda function JAR file
S3Key:
Type: String
Description: The S3 key (file name) of the Lambda function JAR file
Resources:
BaeldungLambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: baeldung-lambda-function
Handler: !Ref LambdaHandler
Role: !GetAtt LambdaExecutionRole.Arn
Code:
S3Bucket: !Ref S3BucketName
S3Key: !Ref S3Key
Runtime: java21
Timeout: 10
MemorySize: 512
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
In our CloudFormation template, we define a Lambda function named baeldung-lambda-function. We attach the managed policy AWSLambdaBasicExecutionRole to our Lambda function through the IAM role LambdaExecutionRole, granting it the necessary permissions to execute and write logs to Amazon CloudWatch.
The Parameters in our CloudFormation template allow us to define input values that we can pass to our template during stack creation. We use the !Ref function to dynamically reference these parameter values in our template to define our lambda properties.
We define a very basic Timeout of 10 seconds and MemorySize of 512 MB in our template, but it can be updated as per requirement.
5.2. Creating CloudFormation Stack
Now that weβve defined our template, we need to create our CloudFormation stack using the AWS CLI:
aws cloudformation create-stack \
--stack-name baeldung-cloudformation-java-21-lambda-function \
--template-body file://java-21-lambda-function-template.yaml \
--capabilities CAPABILITY_IAM \
--parameters \
ParameterKey=LambdaHandler,ParameterValue=com.baeldung.lambda.LambdaHandler\
ParameterKey=S3BucketName,ParameterValue=baeldung-lambda-tutorials-bucket \
ParameterKey=S3Key,ParameterValue=java-lambda-function-0.0.1.jar
In our create-stack command, we provide three parameters:
- stack-name: to specify the name of our CloudFormation stack
- template-body: to specify the path to our CloudFormation template file
- parameters: to specify the parameter values for our Lambda function
We also provide the value CAPABILITY_IAM in the capabilities parameter. This is required when our template creates a new IAM resource β for example, the IAM role for our Lambda function in our example.
5.3. Triggering Lambda Function
Once our Lambda function is deployed successfully, we can invoke it via the AWS CLI:
aws lambda invoke --function-name my-lambda-function \
--cli-binary-format raw-in-base64-out \
--payload '{ "name": "John Doe", "question": "How do I view articles ad-free and in dark mode on Baeldung?" }' output.txt
The above command is a little different from the one we executed in our LocalStack container. However, it will behave the same and output the response to the output.txt file.
In a real-world scenario, instead of directly invoking our Lambda functions via the CLI, our Lambda functions are typically triggered by events from various AWS services, such as API Gateway and S3.
For example, we can configure API Gateway to invoke our Lambda function whenever a specific API endpoint is called, enabling us to build serverless APIs.
Another use case is triggering Lambda functions with S3 events. We can execute our business logic whenever an object is created, modified, or deleted in a specific S3 bucket. This is useful for scenarios like image processing, where we want to perform actions on newly uploaded images.
By using this event-driven nature of AWS Lambda, we can run our code to automatically react to the changes in our AWS environment.
6. Considerations for Using Java as Lambda Runtime
Before we conclude this tutorial, there are a few considerations to keep in mind before choosing Java as the runtime for our AWS Lambda function.
One of the main concerns with using Java for Lambda for time-sensitive applications is the cold start time. When a Lambda function is invoked after a period of inactivity, there is a delay in starting up the JVM and loading the necessary classes. This overhead increases the time it takes for our lambda function to complete.
Another consideration is the memory usage of Java applications. Java tends to consume more memory compared to other languages, such as Node.js or Python. This higher memory consumption can lead to increased costs if not optimized properly. Itβs important to fine-tune the memory settings of our Lambda function to strike a balance between performance and cost.
Recently, GraalVM has emerged as a potential solution. It compiles our Java application into native executables, significantly reducing startup time and optimizing memory.
7. Conclusion
In this article, weβve explored creating an AWS Lambda function using Java.
We discussed the required dependencies and plugin needed to create our executable Lambda function. We followed this by looking at the different ways we can define our handler method.
Finally, we looked at how we can test our Lambda function locally using LocalStack. Once we verified that our function executed correctly, we deployed it to our real AWS environment using AWS CloudFormation.
