Categories

  • Serverless
  • Howto

Most of the time I’ve been using Node.js for building Lambda functions running on AWS. Recently, I wanted to experiment with Java for building a serverless application. In this blog post I describe the necessary steps for building a very simple microservice.

The goal

One of the common use cases for AWS Lambda is to build a RESTful webservice. Such a service combines the power of multiple serverless services offered by AWS. The API endpoints are exposed by the API Gateway service. Whenever an endpoint is called by a consumer the requests trigger the execution of a Lambda function which returns the response. The Lambda function can use a variety of services for persisting data. In this example DynamoDB is used.

The following picture shows the high-level architecture for this kind of microservice.

Serverless Microservice Architecture Overview

So let’s get started!

Create the project

In this example Maven is used for dependency management and building the application. To generate a simple project the following command is used:

mvn archetype:generate -DgroupId=com.example.app \
                       -DartifactId=todo-app \
                       -DarchetypeArtifactId=maven-archetype-simple \
                       -DarchetypeVersion=1.4 \
                       -DinteractiveMode=false

This creates an empty project with the basic configuration. Next, this configuration needs to be tweaked to achieve three things:

  • configure Java version
  • get the dependencies for the AWS SDK
  • package the build result as a fat JAR

The pom.xml that has been generated already defines the properties maven.compiler.source and maven.compiler.target. These need to be set to 1.8 to use the Java 8 compiler and language features. This is necessary this AWS Lambda does only support the Java 8 runtime at the moment.

Next, the dependencies for for the AWS SDK must be added to the <dependencies> section of the pom.xml.

<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-lambda-java-core</artifactId>
  <version>1.2.0</version>
</dependency>
<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-lambda-java-events</artifactId>
  <version>2.2.5</version>
</dependency>

Finally, for configuring Maven to build a fat JAR with all the dependencies the maven-shade-plugin needs to be configured as a build plugin:

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

Additionally, the packaging format must configured:

<packaging>jar</packaging>

To install all the dependencies, building the application and running the tests the following command can be used:

mvn clean package

At this point, this should finish without any errors.

Implementing a handler

Any Lambda function in AWS is associated with a handler function. This function is executed whenever an event is received. In general, every service sends its own kinds of events. Therefore, the data structures never are the same. In other runtimes, like Node.js or Ruby, the event is provided as an object derived from the JSON document used for serializing the event. For Java the library aws-lambda-java-events contains implementations of classes for working easily with the different kinds of events.

For a Lambda handler function the input type and output type are specified in the method signature of the handler. For this post the Lambda functions are dealing with requests send from the API Gateway and need to provide responses which can be processed by it in turn. Therefore, the handler expects an event of type APIGatewayProxyRequestEvent and returns a APIGatewayProxyResponseEvent.

The following snippet of code shows how a handler for processing requests from API Gateway is implemented.

package com.example.apps.todo.handler;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;

public class TaskRequestHandler {
    public APIGatewayProxyResponseEvent listTasks(APIGatewayProxyRequestEvent request, Context context) {
        return new APIGatewayProxyResponseEvent().withStatusCode(200).withBody("It works!");
    }
}

Reading data from DynamoDB

As said before, this simple microservice stores its data to DynamoDB. DynamoDB is a serverless NoSQL database. It’s an ideal fit for these kinds of simple use cases.

For working with DynamoDB the AWS SDK for Java is needed. It is split up into separate libraries for the different services. This web service only needs to work with DynamoDB. Therefore, only this dependency is included:

<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-java-sdk-dynamodb</artifactId>
  <version>1.11.499</version>
</dependency>

The library contains the DynamoDBMapper which provides annotations for the model objects for mapping them to a DynamoDB table:

@DynamoDBTable(tableName="todo-app-tasks")
public class Task {
    private String id;
    private String title;
    private String description;
    private boolean done;

    @DynamoDBHashKey(attributeName="id")
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @DynamoDBAttribute(attributeName="title")
    public String getTitle() {
        return this.title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @DynamoDBAttribute(attributeName="description")
    public String getDescription() {
        return this.description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @DynamoDBAttribute(attributeName="done")
    public boolean isDone() {
        return this.done;
    }

    public void setDone(boolean done) {
        this.done = done;
    }

}

It also provides some high-level functions to read from and write to the table. These methods are used to build a simple DAO implementation:

public class TaskDaoImpl implements TaskDao {

    private AmazonDynamoDB client;
    private DynamoDBMapper mapper;

    public TaskDaoImpl() {
        this.client = AmazonDynamoDBClientBuilder.standard().build();
        this.mapper = new DynamoDBMapper(this.client);
    }

    public List<Task> listTasks() {
        return this.mapper.scan(Task.class, new DynamoDBScanExpression());
    }

    public Task getTask(String id) {
        return this.mapper.load(Task.class, id);
    }

    public void saveTask(Task task) {
        if(task.getId() == null) {
            task.setId(UUID.randomUUID().toString());
        }
        this.mapper.save(task);
    }

    public void deleteTask(String id) {
        Task task = this.getTask(id);
        if(task != null) {
            this.mapper.delete(task);
        }
    }

}

Returning JSON documents

Of course, a real REST API probably needs to return some data at some point in time. Nowadays the data oftentimes is returned in JSON format. Therefore, it is necessary to convert any model objects into their respective JSON representations. This is done by using the Jackson library.

First, the dependencies must be declared in the pom.xml:

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.9.8</version>
</dependency>

Next, the ObjectMapper of Jackson can be used to serialize a model object to JSON:

try {
    ObjectMapper mapper = new ObjectMapper();
    String jsonInString = mapper.writeValueAsString(tasks);
    // Do something with the serialized data
} catch(JsonProcessingException e) {
    return new APIGatewayProxyResponseEvent().withStatusCode(500);
}

This JSON representation of the model object can then be send as the response for the request:

Map<String, String> headers = new HashMap<String, String>();
headers.put("Content-Type", "application/json");
return new APIGatewayProxyResponseEvent().withStatusCode(200).withHeaders(headers).withBody(jsonInString);

SAM Configuration

Now we have implemented a simple handler function returning a JSON response. Next, all the AWS services needed to run the application need to be configured and composed. For this simple example, the API Gateway and a Lambda function are needed.

Although all the configuration can be done manually in the AWS Console it is a good practice to script it. For this, the Serverless Application Model (SAM) provided by AWS can be used. It makes it easy to describe the resources, their configuration and dependencies in a YAML file. This YAML file is called a template. Based on this template stacks will be created. A stack groups all the resources defined in a template. The stack is launched from a template using the AWS service CloudFormation.

The following snippet shows the SAM template which is needed for configuring a simple Lambda function and API endpoint:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:

  ListTasksFunction:
    Type:
    Properties:
      FunctionName: ListTasksFunction
      Runtime: java8
      Timeout: 60
      CodeUri: target/todo-app-1.0-SNAPSHOT.jar
      Handler: com.example.apps.todo.handler.TaskRequestHandler::listTasks
      Policies:
        DynamoDBReadPolicy:
          Ref: TasksTable
      Events:
        List:
          Type: Api
          Properties:
            Path: /tasks
            Method: GET

Build and Deploy

Use the following command for building and deploying the application:

mvn clean package

aws cloudformation package --template-file sam-template.yaml \
                           --output-template-file sam-template-output.yaml \
                           --s3-bucket com.example.sam.deploy
aws cloudformation deploy --template-file sam-template-output.yaml \
                          --stack-name todo-app \
                          --capabilities CAPABILITY_IAM

Summary

This blog post did show how to build a simple Java implementation of a Lambda function processing requests from API Gateway. The GitHub repository jenseickmeyer/todo-app-java contains the complete implementation of the simple CRUD operations needed for this exemplary microservice.