In the previous section, we already finished the generic Gradle setup. In this section, we will learn how to write Lambda functions and create the very core part of our project that will be the entry point for all our Lambda functions.
In our project, we will have more than one specific AWS Lambda function, one for each REST endpoint and several more for auxiliary services. These functions will share some common code and dependencies; therefore, is convenient to create a subproject under our root project. In Gradle, subprojects act like different projects but they can inherit the build configuration from their root project. In any case, these projects will be compiled independently and produce different JAR files in their respective build directories.
In our project structure, one subproject will include the common code we will need for every single Lambda function, and this project will be required as a dependency by other subprojects that implement the Lambda function. As a naming convention, the core Lambda subproject will be called lambda, while the individual Lambda function that will be deployed will be named by the lambda- prefix.
We can start implementing this core AWS Lambda subproject and create a new directory under our root directory called with the its name:
$ mkdir lambda
Then, let's create a new build. gradle file for the newly created subproject:
$ touch lambda/build.gradle
By default, Gradle will not recognize the new subproject just because we created a new directory under the root directory. To make Gradle recognize it as a subproject, we must add a new include directive to the settings.gradle file. This command will add the new line to settings.gradle:
$ echo $"include 'lambda'" >> settings.gradle
After this point, our subproject can inherit the directives from the root project so we will not have to repeat the most of those.
Now we can define the required dependencies for our main Lambda library. At this point, we will need only the aws-lambda-java-core and jackson-databind packages. While the former is the standard AWS library for Lambda functions, the latter is used for JSON serialization and deserialization purposes, which we will be using heavily. In order to add these dependencies, just add these lines in the lambda/build.gradle file:
dependencies {
compile 'com.amazonaws:aws-lambda-java-core:1.1.0'
compile 'com.fasterxml.jackson.core:jackson-databind:2.6.+'
}
Previously, we mentioned that AWS Lambda is invoking a specific method for every Lambda function to inject the event data and accept this method's response as the Lambda response. To determine which method to invoke, AWS Lambda leverages interfaces. aws-lambda-java core includes the RequestStreamHandler interface in the com.amazonaws.services.lambda.runtime package. In our base Lambda package, we will create a method that implements this interface.
Now let's create our first package and implement the LambdaHandler<I, O> method inside it:
$ mkdir -p lambda/src/main/java/com/serverlessbook/lambda
$ touch lambda/src/main/java/com/serverlessbook/lambda/
LambdaHandler.java
Let's start implementing our class:
package com.serverlessbook.lambda;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.ParameterizedType;
public abstract class LambdaHandler<I, O> implements RequestStreamHandler {
@Override
public void handleRequest(InputStream input, OutputStream output,
Context context) throws IOException {
}
public abstract O handleRequest(I input, Context context);
}
As you may have noted, this class is using generics. It is expected that the implemented handleRequest abstract method in the inheriting classes accept one POJO (Plain Old Java Object) and return another POJO. On the other hand, in the overridden handleRequest method gets AWS Lambda event data as InputStream and it should return OutputStream including the output JSON. Our base LambdaHandler method will implement methods that convert JSON into InputStream and OutputStream into JSON. The I and O type references are the key points in this case because using this information, our base class will know which POJO classes it should use when it carries out the transformation.
If you have ever read the AWS Lambda documentation, you might have seen the
RequestHandler class in the AWS Lambda library, which exactly does what we will do in the base class. However, Lambda's built-in JSON serialization does not meet the requirements for our project because it does not support advanced features of the Jackson JSON library. That's why we are implementing our own JSON serializer. If you are building a simple Lambda function that does not require these advanced options, you can check out
https://docs.aws.amazon.com/lambda/latest/dg/java-handler-io-type-pojo.html and use the built-in serializer.
Before we go on implementing the base Lambda handler method, I suggest that you look at the TDD (Test Driven Development) approach and write a test class for the planned implementation. Having the test class will explain better which type of implementation we need and will draw a clear picture about the next step.
Before we start implementing the test, first, we have to add Junit as a dependency to our project. Open build.gradle in the root project and add these files to the end:
allprojects {
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.11'
}
}
Then, let's create our first test file:
$ mkdir -p lambda/src/test/java/com/serverlessbook/lambda
$ touch lambda/src/test/java/com/serverlessbook/lambda/
LambdaHandlerTest.java
We can then to start implementing it writing the following code to LambdaHandlerTest file we've just created. First of all, inside the test class we will create two stub POJO's and a LambdaHandler class to run the test against:
public class LambdaHandlerTest {
protected static class TestInput {
public String value;
}
protected static class TestOutput {
public String value;
}
protected static class TestLambdaHandler extends LambdaHandler<TestInput,
TestOutput> {
@Override
public TestOutput handleRequest(TestInput input, Context context) {
TestOutput testOutput = new TestOutput();
testOutput.value = input.value;
return testOutput;
}
}
}
Here, we have the sample TestInput and TestOutput classes, which are simple POJO classes with one variable each and one TestLambdaHandler class that implements the LambdaHandler class with type references to these POJO classes. As you may have noted, the stub class does not do too much and simply returns a TestOutput object with the same value it gets.
Finally, we can add the test method that will exactly emulate the AWS Lambda runtime and carry out a black-box text over our TestLambdaHandler method:
@Test
public void handleRequest() throws Exception {
String jsonInputAndExpectedOutput = "{\"value\":\"testValue\"}";
InputStream exampleInputStream = new
ByteArrayInputStream(jsonInputAndExpectedOutput.getBytes(
StandardCharsets.UTF_8));
OutputStream exampleOutputStream = new OutputStream() {
private final StringBuilder stringBuilder = new StringBuilder();
@Override
public void write(int b) {
stringBuilder.append((char) b);
}
@Override
public String toString() {
return stringBuilder.toString();
}
};
TestLambdaHandler lambdaHandler = new TestLambdaHandler();
lambdaHandler.handleRequest(exampleInputStream, exampleOutputStream, null);
assertEquals(jsonInputAndExpectedOutput, exampleOutputStream.toString());
}
To run the test, we can execute this command:
$ ./gradlew test
Once you run the command, you will see that test will fail. It is normal for our test to fail because we did not complete the implementation of our LambdaHandler method and this is how Test Driven Development works: first, write the test, and then implement it until the test returns to green.
I think it is time to move on to implementation. Open the LambdaHandler class again and add a field with Jackson's ObjectMapper type and create the default constructor to initiate this object. You can add the following code to beginning of the class:
final ObjectMapper mapper;
protected LambdaHandler() {
mapper = new ObjectMapper();
}
AWS Lambda does not create an object from the handler class for every new request. Instead, it creates an instance of the class for the first request (called the 'heat up' stage) and uses the same instance for other requests. This created object will stay in the memory for about 20 minutes if there is no consequent request for that Lambda function. It is good to know about this undocumented fact because it means that we can cache objects among different requests using object properties, like we do here for ObjectMapper. In this case ObjectMapper will not be created for every request, and it will be 'cached' in the memory. However, you can think of the handler object like Servlets and you should pay attention to thread safety before you decide to use object properties.
Now we need helper methods in the handler for serialization and deserialization. First, we need a method to get the Class object for the I type reference:
@SuppressWarnings("unchecked")
private Class<I> getInputType() {
return (Class<I>) ((ParameterizedType)
getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
We can use the deserializer and serializer methods:
private I deserializeEventJson(InputStream inputStream, Class<I> clazz) throws
IOException {
return mapper.readerFor(clazz).readValue(inputStream);
}
private void serializeOutput(OutputStream outputStream, O output) throws
IOException {
mapper.writer().writeValue(outputStream, output);
}
Finally, we can implement the handler method:
@Override
public void handleRequest(InputStream input, OutputStream output,
Context context) throws IOException {
I inputObject = deserializeEventJson(input, getInputType());
O handlerResult = handleRequest(inputObject, context);
serializeOutput(output, handlerResult);
}
It seems we are good to go. Let's run the test again:
$ ./gradlew test
Congratulations! We completed an important step and built the base class for our Lambda functions.