-
Notifications
You must be signed in to change notification settings - Fork 560
Custom event types
Serverless Java Container supports API Gateway and the Application Load Balancer (ALB)'s proxy event types by default. However, the library is written to support any custom event type.
Before we describe how to support custom event types, it is important to understand how the Serverless Java Container library works behind the scenes. Below you can see a UML diagram of the class relationships. The primary object is the LambdaContainerHandler
class. The container handler exposes a typed proxy()
method as well as a stream-compatible proxyStream()
method. To enable the handler class to receive different event types and communicate with multiple implementations, it uses four different generics:
-
RequestType
: The incoming event type. In most cases, this will be an instance ofAwsProxyRequest
. -
ResponseType
: The expected output of the handler. Normally, this isAwsProxyResponse
. -
ContainerRequestType
: The request type object that the framework-specific implementation receives. -
ContainerResponseType
: The response type the framework-specific implementation returns.
Make a note of these generic type names becuase they will recurr throughout this document. The constructor for the LambdaContainerHandler
class also receives implementations of the RequestReader
and ResponseWriter
clases. As the name suggests, these classes are in charge of receiving a RequestType
object and returning a ContainerRequestType
object (guess what the ResponseWriter
does?). The core library of the Serverless Java Container framework includes a simple implementation of a servlet-compatible handler, request reader, and response writer. This allows us to support most frameworks such as Jersey, Spring, Spark, and Struts with minimal changes to the code - as long as the RequestType
and ResponseType
objects are compatible with AWS proxy specifications.
To make the relationship between the various classes clearer, let's go through a practical example. Consider the code below:
1 public class StreamLambdaHandler implements RequestStreamHandler {
2 private static SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
3 static {
4 try {
5 handler = SpringLambdaContainerHandler.getAwsProxyHandler(PetStoreSpringAppConfig.class);
6 } catch (ContainerInitializationException e) {
7 // if we fail here. We re-throw the exception to force another cold start
8 e.printStackTrace();
9 throw new RuntimeException("Could not initialize Spring framework", e);
10 }
11 }
12
13 @Override
14 public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
15 throws IOException {
16 handler.proxyStream(inputStream, outputStream, context);
17 }
18 }
Here, we obviously declare our handler class and implement Lambda's RequestStreamHandler
. The RequestStreamHandler
interface is declared in Lambda's Java core library and defines the handleRequest
method as implemented on line 14
of our sample.
Here we declare a static class member for the Spring implementation of the Serverless Java Container framework. The SpringLambdaContainerHandler
class extends the AwsLambdaServletContainerHandler
class, which in turn extends LambdaContainerHandler
. The AwsLambdaServletContainerHandler
specifies the ContainerRequestType
and ContainerResponseType
as AwsProxyHttpServletRequest
and AwsHttpServletResponse
. The LambdaContainerHandler
class gives us access to the proxy()
and proxyStream()
methods. With the container request and response types already specified by the subclass, we only need to specify two additional generics, the event types that Lambda receives and returns: RequestType
and ResponseType
. In this case, they are set to the built-in models AwsProxyRequest
and AwsProxyResponse
.
In this static block, we use the getAwsProxyHandler()
static method of the SpringLambdaContainerHandler
class to instatiate a default handler. In the UML diagram, you can see that framework-specific implementations define a constructor that takes multiple parameters:
-
Class<RequestType> requestTypeClass
: The type of the request. This is necessary because of type erasure. -
Class<ResponseType> responseTypeClass
: Same as above. -
RequestReader<RequestType, AwsProxyHttpServletRequest>
: An implementation of theRequestReader
interface that can receive our request type - in this caseAwsProxyRequest
- and transform it into anAwsProxyHttpServletRequest
, ourContainerRequestType
as specified by theAwsLambdaServletContainerHandler
. -
ResponseWriter<AwsHttpServletResponse, ResponseType>
: Same as above for theResponseWriter
inteface. -
SecurityContextWriter<RequestType>
: The security context is used by the JAX-RS specifications to provide resource implementations with principal information. Behind the scenes, the framework uses the JAX-RS object in all its implementations, including in the servlet methods. The Serverless Java Container framework providers a defaultSecurityContextWriter
implementation that given anAwsProxyRequest
can return a JAX-RSSecurityContext
object. -
ExceptionHandler<ResponseType>
: A top level exception handler object. The framework uses this object to intercept exceptions that are allowed to reach theLambdaContainerHandler
proxy()
method and transform them into a valid response object. The framework includes a defaultAwsProxyExceptionHandler
for theAwsProxyResponse
type.
When we call the getAwsProxyHandler()
static method, the class calls its constructor with the default parameters to handle AWS' proxy event types:
new SpringLambdaContainerHandler<>(
AwsProxyRequest.class,
AwsProxyResponse.class,
new AwsProxyHttpServletRequestReader(),
new AwsProxyHttpServletResponseWriter(),
new AwsProxySecurityContextWriter(),
new AwsProxyExceptionHandler()
);
The constructor in each framework-specific is also in charge of initializing the underlying framework and preparing it to receive requests. Since the initialization process is different for each framework, we won't cover it in this section of the documentation. In Spring's case, initializing an application may throw an exception. The Serverless Java Container framework wraps the underlying exception in a ContainerInitializationException
that we catch on line 6
.
Here we implement the handleRequest()
mehod as defined in the RequestStreamHandler
interface. This is the main entry point that Lambda itself calls whenever it receives a new event. Because our handler is already initialized - the static block is executed first - we can simply call the proxyStream
method with all of the incoming parameters to handle the request.
The proxyStream()
method uses Jackson's ObjectMapper
object to transform the incoming InputStream
into the expected RequestType
, in this case an AwsProxyRequest
object. With the request object read, it calls the proxy()
method of the LambdaContainerHandler
object to pass the request to the underlying framework.
The proxy()
method first uses the readers passed in the constructor - RequestReader
and SecurityContextWriter
- to generate the ContainerRequestType
and SecurityContext
objects. In Spring's case, the ContainerRequestType
is an AwsProxyHttpServletRequest
because Spring can natively receive Servlet-compatible objects. With the request objects ready for the framework, generic LambdaContainerHandler
calls the a method in the framework-specific implementation to pass handle the request object.
When the framework returns a response - or writes the respone OutputStream
- the Serverless Java Container library uses the ResponseWriter
passed in the constructor to transform the ContainerResponseType
into a ResponseType
. The default Servlet implementation provided with the framework uses a latch to turn asynchronous responses into a sync invocation. In Spring's case, the ContainerResponseType
is an AwsServletResponse
and, in our code, the ResponseType
is an AwsProxyResponse
because the default object only supports the AWS proxy specifications.
Now that we have all the background information, you can hopefully start to see how support for custom event types would work. Out of the four key generic types, we need to customize the outward-facing ones: RequestType
and ResponseType
. Since the underlying frameworks do not change - I assume you are still planning to use one of the supported ones - the ContainerRequestType
and ContainerResponseType
do not need to change; you can keep leveraging the framwork's AwsProxyHttpServletRequest
and AwsHttpServletResponse
.
To accept the new event types. You will need to first create a new implementation of the RequestReader
and ResponseWriter
abstract classes. Specifically, you will want a RequestReader
that can receive your CustomEvent
and return an AwsProxyHttpServletRequest
(or any other implementation of HttpServletRequest
):
public class CustomRequestReader extends RequestReader<CustomEvent, AwsProxyHttpServletRequest> {
@Override
public AwsProxyHttpServletRequest readRequest(CustomEvent request, SecurityContext securityContext, Context lambdaContext, ContainerConfig config) {
...
}
@Override
protected Class<? extends CustomEventType> getRequestClass() {
return CustomEvent.class;
}
}
Similarly, you will want to create a ResponseWriter
implementation that can receive an AwsHttpServletResponse
object and transform it into your CustomEventResponse
value:
public class CustomResponseWriter extends ResponseWriter<AwsHttpServletResponse, CustomEventResponse> {
@Override
public CustomEventResponse writeResponse(AwsHttpServletResponse containerResponse, Context lambdaContext) {
...
}
}
With these two new objects, you can now initialize a framework-specific implementation of the library using the constructor rather than the simple getAwsProxyHandler()
method. For example, in Spring's case we would write the following handler class:
public class StreamLambdaHandler implements RequestStreamHandler {
private static SpringLambdaContainerHandler<CustomEvent, CustomEventResponse> handler;
static {
try {
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(MySpringApplication.class);
handler = new SpringLambdaContainerHandler<>(
CustomEvent.class,
CustomEventResponse.class,
new CustomRequestReader(),
new CustomResponseWriter(),
// this assumes you are still happy to use the default security context writers
// and exception handler. Obviously, you can create custom implementations of
// these objects too.
new AwsProxySecurityContextWriter(),
new AwsProxyExceptionHandler(),
applicationContext
);
} catch (ContainerInitializationException e) {
// if we fail here. We re-throw the exception to force another cold start
e.printStackTrace();
throw new RuntimeException("Could not initialize Spring framework", e);
}
}
@Override
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
throws IOException {
handler.proxyStream(inputStream, outputStream, context);
}
}