• Guide Content

AWS Lambda Java

AWS Lambda has been used by many developers for building and deploying serverless architecture or event-driven architecture applications. AWS Lambda functions can be developed using many languages like Go, Python, Java, NodeJS, or more.

In this article, we will be going through the Java option of AWS Lambda. We will see how a function can be developed using Java and what Lambda features can be enabled with it.

To build a Lambda function with Java, you need to first write code using the handler methods supported by the AWS Java SDK. Then, you need to build the function using build tools like Maven and Gradle. Enable the logging, tracing, do error handling, and finally, deploy the code to the Lambda service. Let’s go through each of these steps in detail.

Build and Deployment

Similar to any other Java application, a Lambda Java function also needs to be built and deployed. To build the Java function, we can use Maven or Gradle. A Deployment package for Java can be a zip file or a jar file. We can deploy functions using the AWS CLI, AWS Console, or the AWS Serverless Application Model (SAM).

Build

AWS Lambda provides a few core libraries that are required for building a Java function:

  • aws-lambda-java-core – A required library to use handler method interfaces and the context object that the runtime passes to the handler.
  • aws-lambda-java-events – Defines the event sources. These event sources are the input types for events from services that invoke Lambda functions.
  • aws-lambda-java-log4j2 – An appender library for Log4j 2 that can be used to add the request ID of the current invocation to the function logs.

There are a few other libraries you would need, for example:

  • aws-xray-recorder-sdk-core and aws-xray-recorder-sdk-aws-sdk-core for enabling distributed tracing using the AWS X-Ray service (or instead, use Lumigo).
  • junit-jupiter-api and junit-jupiter-engine for JUnit testing.

You will also need a Maven or Gradle plugin to compile and build the code. Below is an example for Maven:

  • maven-compiler-plugin – used to compile the code
  • maven-shade-plugin – used to zip the code
  • maven-surefire-plugin – used to run unit testing and generate a report

Deployment

As mentioned above, we would need to generate the zip or jar file using a Maven plugin or Gradle task. Here’s an example of how to do it in Gradle:

 

#gradle task

task buildZipPackage(type: Zip) {
   from compileJava
   from processResources
   into('lib') {
      from configurations.runtimeClasspath
   }
   dirMode = 0755
   fileMode = 0755
}

This build configuration produces a deployment package in the build/distributions folder. The compileJava task compiles the function’s classes. The processResources task copies libraries from the build’s classpath into a folder named lib.
For maven, we would need to use the maven-shade-plugin:

#pom.xml

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-shade-plugin</artifactId>
   <version>3.2.2</version>
   <configuration>
      <createDependencyReducedPom>false</createDependencyReducedPom>
   </configuration>
   <executions>
      <execution>
         <phase>package</phase>
         <goals>
            <goal>shade</goal>
         </goals>
      </execution>
   </executions>
</plugin>

If the size of the deployment package (zip or jar) is less than 50 MB, you can directly upload the package to Lambda using AWS CLI, Console, or API. If the size is more, it has to be uploaded to the S3 bucket first and then point the Lambda to that upload.

The best way to deploy a package to Lambda is through AWS SAM or Serverless Framework. AWS SAM is supported by AWS and the serverless framework is widely used open-source software.

AWS SAM is an extension of AWS CloudFormation that provides a simplified syntax for defining serverless applications. The following example template defines a function with a deployment package in the build/distributions directory:

 

#template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: An AWS Lambda application that calls the Lambda API.
Resources:
   function:
      Type: AWS::Serverless::Function
      Properties:
         CodeUri: build/distributions/java-basic.zip
         Handler: example.Handler
         Runtime: java8
         Description: Java function
         MemorySize: 1024
         Timeout: 5
         # Function's execution role
         Policies:
         - AWSLambdaBasicExecutionRole
         - AWSLambdaReadOnlyAccess
         - AWSXrayWriteOnlyAccess
         Tracing: Active


Using the sam build and sam deploy commands, we can deploy the function code to the Lambda service.

The sam build command processes this SAM template file, application code, and any applicable language-specific files and dependencies. Dependencies are specified in a manifest file that is included in the application, such as package.json for Node.js functions. It copies your application source code to folders under .aws-sam/build to be zipped and uploaded to Lambda.

The sam deploy command deploys the code to the AWS cloud. It creates the S3 bucket for the deployment package. It takes the deployment artifacts built with the sam build command, packages it, and uploads it to the S3 bucket created.

Tips from the experts

  1. Minimize cold start latency

    Java functions tend to have higher cold start latency due to JVM initialization. To reduce this, minimize the use of heavy libraries, keep your deployment package small, and consider using provisioned concurrency to keep function instances warm, especially for latency-sensitive applications.
  2. Use custom runtime and dependency management

    If the default AWS Lambda Java runtime doesn't meet your needs, consider using custom runtimes. This allows you to use optimized JVMs, such as GraalVM for ahead-of-time compilation, which can significantly improve startup times and reduce memory usage.
  3. Leverage the AWS Lambda Power Tuning tool

    Use the AWS Lambda Power Tuning tool to identify the optimal memory size for your Java functions. This tool helps balance performance and cost by running experiments to find the most efficient memory configuration for your specific workload.
  4. Efficient logging and monitoring

    Since Java can generate verbose logs, ensure that logging is optimized by setting appropriate log levels and using asynchronous logging frameworks like Log4j2 with async appenders. Use AWS CloudWatch Logs insights for efficient log query and analysis, and set up alarms for key function metrics.
  5. Leverage Lambda SnapStart (if available)

    For Java functions with high cold start latency, consider using Lambda SnapStart (if supported). This feature pre-warms your function by taking a snapshot of the initialized runtime environment, reducing startup time by restoring the snapshot rather than initializing from scratch.
  6. Secure and manage configurations efficiently

    Use AWS Systems Manager Parameter Store or AWS Secrets Manager to manage environment-specific configurations, secrets, and credentials. Access these securely within your Lambda functions without hardcoding them, and ensure these services are set up with the least privilege necessary.
Aviad Mor
CTO
Aviad Mor is the Co-Founder & CTO at Lumigo. Lumigo’s SaaS platform helps companies monitor and troubleshoot microservices applications while providing actionable insights that prevent business disruptions. Aviad has over a decade of experience in technology leadership, heading the development of core products in Check Point from inception to wide adoption.

Function Implementation

A Lambda function’s structure is very simple. We just need to implement a handler method that will be triggered whenever a function is invoked. However, you may need to import several other libraries, define global variables, connection URLs, etc.. to be used by the handler method.

Handler

The Handler method in function code processes events. When a function is invoked, Lambda executes the handler method. Once the processing is completed, it returns a response and becomes available to handle another event.

Here’s an example of a handler method:

 

// Handler value: example.Handler
public class Handler implements RequestHandler<Map<String,String>, String>{
   Gson gson = new GsonBuilder().setPrettyPrinting().create();
   @Override
   public String handleRequest(Map<String,String> event, Context context)
   {
      LambdaLogger logger = context.getLogger();
      String response = new String("200 OK");

      // log execution details
      logger.log("ENVIRONMENT VARIABLES: " + gson.toJson(System.getenv()));
      logger.log("CONTEXT: " + gson.toJson(context));

      // process event
      logger.log("EVENT: " + gson.toJson(event));
      logger.log("EVENT TYPE: " + event.getClass().toString());
      return response;
   }
}


It accepts two inputs: event and context.

Event

An Event can be of any type. The developer needs to define the type of object that event maps to in the handler method’s signature. In the above example, Java deserializes the event into a type that implements the Map<String,String> interface. So It can accept input like this:

 

{
   "customerId": 1081,
   "firstName": “Matt”,
   "lastName": “Robins”,
   "status": “Active”
}

Context

Context provides methods that provide information about the function invocation, metadata, and execution environment. For example, what is the function name, function version, and time remaining for a timeout.

Handler Types

The aws-lambda-java-core library defines two interfaces for handler methods. These interfaces simplify handle configuration and validate the handler method signature at compile time.

  • amazonaws.services.lambda.runtime.RequestHandler – it is a generic type that takes two parameters: the input type and the output type. Both types must be objects.
  • amazonaws.services.lambda.runtime.RequestStreamHandler – It is used for custom deserialization. With this interface, Lambda passes an input and output stream. The handler reads bytes from the input stream, writes to the output stream, and returns void.

Logging and Tracing

By default, Lambda functions come with CloudWatch Logs for logging, which creates logGroup and logStreams for a function. For logging, we can use any library or module that supports stdout and stderr.

The aws-lambda-java-core library provides a logger class named LambdaLogger that can be accessed from the context object. LambdaLogger supports multiline logs as well.

Here’s an example:

 

@Override
   public String handleRequest(Object event, Context context)
   {
      LambdaLogger logger = context.getLogger();
      String response = new String("Logging Successful");
      // log execution details
      logger.log("ENV VARIABLES: " + gson.toJson(System.getenv()));
      logger.log("CONTEXT: " + gson.toJson(context));
      // process event
      logger.log("EVENT: " + gson.toJson(event));
      return response;
   }

 

You can view the logs either through the AWS Console or the CLI.

Error Handling

Lambda functions only support throwing runtime exceptions. They don’t allow checked exceptions. The function will throw the error back to the invoker with a specific message format that contains an error message, error type, and stack trace:

 

{
   "errorMessage":"Input must contain alphanumeric value.",
   "errorType":"java.lang.InputTypeException",
   "stackTrace": [
   "example.HandlerDisplay.handleRequest(HandlerDisplay.java:21)",
   "example.HandlerDisplay.handleRequest(HandlerDisplay.java:12)"
   ]
   }

Following is an example of Java code showing how to do error handling:

 

@Override
   public Integer handleRequest(List<Integer> event, Context context)
   {
      LambdaLogger logger = context.getLogger();
      // process event
      Pattern p = Pattern.compile("^[a-zA-Z0-9]*$");
      if ( ! p.matcher(event.get(0)).find())
      {
      throw new InputTypeException("Input must contain alphanumeric value.");
      }
      logger.log("EVENT: " + gson.toJson(event));
      logger.log("EVENT TYPE: " + event.getClass().toString());
      response = “Hello ” + event.get(0);
      return response;
   }

 

For runtime errors and other function errors, the Lambda service does not return an error code. It returns a 2xx series status code indicating that the Lambda service processed the request. Instead of an error code, Lambda sends back the error with the response header  X-Amz-Function-Error.

Lambda also integrates with the AWS X-Ray service to enable tracing and debugging Lambda applications. By simply adding the X-Ray SDK library to the build configuration, you can record errors and latency for any call that your function makes to an AWS service. It creates a map showing the flow of the request for each invocation.

To create an X-Ray map and trace, enable the service using the AWS console. It will add the required permissions to have Lambda functions have access to the X-Ray daemon. To record the details about each call that your function makes to other resources and services like S3, DynamoDB, you need to add the X-Ray SDK for Java to your build configuration.

Summary

AWS Lambda has become the most common service used to build serverless applications. And with support for Java, the most used language, it has become more powerful. Using Java, developers can easily build Lambda functions with the rich features available in Java libraries. The AWS SDK is continuously updated with the latest Java features to make it a more robust platform for building serverless applications.